Yahoo Finance에서 주식 정보 가져오기 (4)

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 chrono crate을 이용한 format_timestamp 함수와 tokio main 매크로에 대해 알아보겠습니다.

Ⅰ. format_timestamp 함수에 대해 알아보기

fn format_timestamp(timestamp: i64) -> String {
match Utc.timestamp_opt(timestamp, 0) {
chrono::LocalResult::Single(datetime) => datetime.format("%Y-%m-%d %H:%M:%S").to_string(),
_ => "Invalid timestamp".to_string(),
}
}

1. 입력값

timestamp: i64 : 이 값은 유닉스 타임스탬프(1970-01-01 00:00:00 UTC 기준의 초 단위 정수) 입니다.

2. Utc.timestamp_opt(timestamp, 0)

  • chrono::Utc는 UTC(협정 세계시) 타임존을 나타냅니다.
  • timestamp_opt(secs, nsecs)는 주어진 초(secs)와 나노초(nsecs)를 UTC 시각으로 변환하려고 시도합니다.
  • 반환값은 -chrono::LocalResult> enum인데, 이에는 세 가지 경우가 있습니다.
    – Single(datetime) → 정상적으로 변환됨
    – None → 변환 불가
    – Ambiguous(, ) → 모호한 시간 (주로 로컬 타임존에서 섬머타임 전환 시 발생, UTC에서는 거의 없음)

3. match 표현식

  • chrono::LocalResult::Single(datetime)일 경우에는 변환된 datetime을 format(“%Y-%m-%d %H:%M:%S”)로 지정한 문자열 포맷(연-월-일 시:분:초)으로 변환하고,
  • 그 밖의 경우(None, Ambiguous)는 “Invalid timestamp”라는 문자열을 반환합니다.

Ⅱ. tokio main 매크로에 대해 알아보기

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let symbols = [
"005930.KS", // 삼성전자
"AAPL", // Apple
"TSLA", // Tesla
"LIT", // Global X Lithium & Battery Tech ETF
"BRK-B", // Berkshire Hathaway Class B
"AMZN", // Amazon
"O", // Realty Income Corporation
"TQQQ", // ProShares UltraPro QQQ
"XOM", // Exxon Mobil
"WMT", // Walmart
];

println!("Fetching stock data...\n");

let results = fetch_multiple_stocks(&symbols).await;

// 테이블 헤더 출력
println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
"No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
println!("{}", "-".repeat(95));

for (i, result) in results.iter().enumerate() {
let row_number = i + 1;
match result {
Ok(stock) => {
println!("{:<3} {:<10} {:<35} {:<15.2} {:<8} {:<20}",
row_number,
stock.symbol,
if stock.long_name.len() > 35 {
format!("{}...", &stock.long_name[..32])
} else {
stock.long_name.clone()
},
stock.regular_market_price,
stock.currency,
format_timestamp(stock.regular_market_time)
);
}
Err(e) => {
println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
row_number,
symbols[i],
"Error fetching data",
"N/A",
"N/A",
"N/A"
);
}
}
}

Ok(())
}

1. Tokio 런타임

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
  • #[tokio::main]은 Tokio 비동기 런타임을 사용하는 매크로.
  • async fn main() → 메인 함수 자체가 비동기 함수로 실행됨.
  • 반환 타입이 Result<(), Box<dyn std::error::Error + Send + Sync>>으로서 성공했을 때는 (), 다시 말해 빈 튜플을 반환하므로 아무 값도 안돌려주고, 오류가 발생했을 때 다양한 에러 타입을 동적으로 담아 반환합니다.

2. 주식 심볼 배열

let symbols = [
"005930.KS", // 삼성전자
"AAPL", // Apple
"TSLA", // Tesla
"LIT", // Global X Lithium & Battery Tech ETF
"BRK-B", // Berkshire Hathaway Class B
"AMZN", // Amazon
"O", // Realty Income Corporation
"TQQQ", // ProShares UltraPro QQQ
"XOM", // Exxon Mobil
"WMT", // Walmart
];
  • fetch_multiple_stocks함수의 인수로 전달하기 위해 여러 주식의 티커(symbol)를 배열로 정의합니다.

3. 주식 데이터 가져오기

println!("Fetching stock data...\n");

let results = fetch_multiple_stocks(&symbols).await;
  • “Fetching stock data…” → 데이터 가져오기 시작 알림 출력.
  • fetch_multiple_stocks 함수는 비동기 함수로, 각 심볼에 대해 API 호출을 시도하여 Vec<Result<StockData, Error>> 를 반환함

4. 표 헤더 출력

println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
"No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
println!("{}", "-".repeat(95));
  • {:<n} 포맷은 왼쪽 정렬(Left Align) 하여 n 자리 확보.
  • 따라서 표 형태로 깔끔하게 정렬됨.
  • 이어서 “-“.repeat(95)로 구분선 출력.

5. 각 결과 출력

for (i, result) in results.iter().enumerate() {
let row_number = i + 1;
match result {
Ok(stock) => {
println!("{:<3} {:<10} {:<35} {:<15.2} {:<8} {:<20}",
row_number,
stock.symbol,
if stock.long_name.len() > 35 {
format!("{}...", &stock.long_name[..32])
} else {
stock.long_name.clone()
},
stock.regular_market_price,
stock.currency,
format_timestamp(stock.regular_market_time)
);
}
Err(e) => {
println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
row_number,
symbols[i],
"Error fetching data",
"N/A",
"N/A",
"N/A"
);
}
}
}
  • enumerate()를 쓰면 (index, result) 튜플이 넘어옴. row_number = i + 1로 1씩 증가하는 번호를 출력.
    • match result:
      ① 성공(Ok(stock))하면
      – stock(StockData 구조체)를 이용해 주식 데이터를 출력하는데, stock은 result가 성공했을 때 값입니다.
      – stock.symbol 값을 symbol로 출력하고,
      – stock.long_name은 너무 길면 앞의 32자만 출력하고, + “…” 붙여서 가독성을 유지하고,
      – stock.regular_market_price는 소수점 둘째 자리까지 표시하고,({:.2}),
      – 화폐 단위(currency)를 출력하고,
      – stock.regular_market_time은 format_timestamp 함수로 변환하여 표시함

      ② 실패(Err)하면
      – 주어진 symbols[i] 심볼과 함께 “Error fetching data”, “N/A” 값들을 표시.

6. 종료

Ok(())
  • main 함수가 정상 종료됨을 의미.

구구단 만들기

구구단을 만들어 보면서 for 반복문과 if 조건문, 그리고 인쇄시 오른쪽 정렬, 가운데 정렬 등 정렬, 공백 추가 등을 알아보겠습니다. 간단한 것인데도 Rust가 다른 언어와 다르다는 것을 다시 한번 더 느꼈습니다.

1. 구구단 만들기(실패)

fn main() {
    let mut i:i32;
    let mut j:i32;
    for i in 2..=9 {
        for j in 1..=9 {
            println!("{} * {} = {}",i,j,i*j) ;
        }
    }
}

위와 같이

let mut i:i32;
let mut j:i32;

라고 하니 warning: unused variable: i와 j 경고가 발생합니다.

그러나, 결과는 아래와 같이 잘 표시됩니다.

다시 말해 for 반복문에서 변수 i와 j를 사용하면 별도로 변수 선언이 필요하지 않다는 것을 알 수 있습니다.

그리고, for 반복문에서는 변수 앞에 mut를 붙이지 않아도 된다는 것을 알 수 있습니다.

오히려 mut를 붙여서 for mut i in 2..=9 {라고 하면

warning: variable does not need to be mutable라는 경고가 발생합니다.

2. 구구단 만들기(부분 성공)

let 문 2개를 지우고 실행하니

fn main() {
    for i in 2..=9 {
        for j in 1..=9 {
            println!("{} * {} = {}",i,j,i*j) ;
        }
    }
}

아래와 같이 출력결과가 잘 표시됩니다. 그러나, 2 * 1 = 2에서 결과값 2가 오른쪽 정렬이 되면 좋겠습니다.

3. 구구단 만들기(성공)

이럴 때 사용할 수 있는 것이 표시될 너비를 지정하는 것으로

{:2}라고 지정하면 됩니다.

따라서 코드는 아래와 같습니다.

fn main() {
    for i in 2..=9 {
        for j in 1..=9 {
            println!("{} * {} = {:2}",i,j,i*j) ;
        }
    }
}

출력 결과가 아래와 같이 깔끔합니다.

4. 구구단 한 줄에 3개씩 표시하기

i를 3칸씩 떨어져서 실행하고,

결과 표시를 i, i+1, i+2를 이용해 표시하는데, 중간에 공백 10칸을 넣은 후 i+1과 i+2를 오른쪽 정렬로 넣기 위해 {:>10}이라고 하면 됩니다.

오른쪽 정렬은 >, 왼쪽 정렬은 <, 가운데 정렬은 ^를 사용합니다.

완성된 코드는 아래와 같습니다.

fn main() {
    for i in (2..=9).step_by(3) {
        for j in 1..=9 {
            println!("{} * {} = {:2}{:>10} * {} = {:2}{:>10} * {} = {:2}",
                      i,j,i*j, i+1,j,(i+1)*j, i+2,j,(i+2)*j);
        }
    }
}

출력된 결과는 아래와 같습니다.

5. 구구단이 한 단이 끝난 다음 공백 2줄 추가하기

공백 2줄을 추가하기 위해

print!(“\n\n”) ;

또는

println!();
println!();

을 사용합니다.

전체 코드는 아래와 같습니다.

fn main() {
    for i in (2..=9).step_by(3) {
        for j in 1..=9 {
            println!("{} * {} = {:2}{:>10} * {} = {:2}{:>10} * {} = {:2}",
                      i,j,i*j, i+1,j,(i+1)*j, i+2,j,(i+2)*j);
        }
        print!("\n\n");
    }
}

6. 구구단 명 표시하기(실패)

구구단명을 표시하려면 구구단명을 가운데 정렬(^사용)로 표시하고, 단 이름과 단 이름사이에 공백을 10개 추가해야 한다고 생각했는데, 8로 해야 제대로 보입니다. 따라서 {:>8}로 하고 값이 없으므로 “”,를 추가했습니다.

완성된 코드는 아래와 같고

fn main() {
    for i in (2..=9).step_by(3) {
        println!("{:^10}{:8}{:^10}{:8}{:^10}",
                    i.to_string()+"단","",
                    (i+1).to_string()+"단","",
                    (i+2).to_string()+"단");
                    
        for j in 1..=9 {
            println!("{} * {} = {:2}{:>10} * {} = {:2}{:>10} * {} = {:2}",
                      i,j,i*j, i+1,j,(i+1)*j, i+2,j,(i+2)*j);
        }
        println!();
        println!();
    }
}

결과는 아래와 같습니다. 9단까지 표시돼야 하는데, 3개씩 표시하다 보니 10단까지 표시됐습니다.

7. 구구단 명 표시하기(성공)

10단이면, 다시 말해 (i+2)가 9보다 크면 인쇄하지 않고, 9이하일 때만 인쇄해야 하므로 아래와 같이 if 문을 사용해야 합니다.

fn main() {
    for i in (2..=9).step_by(3) {
        if (i+2) <= 9 {
            println!("{:^10}{:8}{:^10}{:8}{:^10}",
                        i.to_string()+"단","",
                        (i+1).to_string()+"단","",
                        (i+2).to_string()+"단");

            for j in 1..=9 {
                println!("{} * {} = {:2}{:>10} * {} = {:2}{:>10} * {} = {:2}",
                          i,j,i*j, i+1,j,(i+1)*j, i+2,j,(i+2)*j);
            }
                        
        } else {
            println!("{:^10}{:8}{:^10}",
                        i.to_string()+"단","",
                        (i+1).to_string()+"단");

            for j in 1..=9 {
                println!("{} * {} = {:2}{:>10} * {} = {:2}",
                          i,j,i*j, i+1,j,(i+1)*j);
            }
                        
        }
                        
        println!();
        println!();
    }
}

9단까지만 잘 표시됐습니다.

Rust의 이터레이터(Iterator)

Rust에서 이터레이터(iterator)는 값을 순회(iterate)할 수 있도록 해주는 강력하고 유연한 추상화입니다. 이터레이터는 반복 가능한 값을 하나씩 꺼내면서 작업을 수행할 때 사용되며, 지연 평가(lazy evaluation)를 통해 성능도 뛰어납니다.

Rust에서 지연 평가(lazy evaluation)는 계산이 필요한 시점까지 연산을 연기하는 전략입니다. 즉, 값을 즉시 계산하는 대신, 해당 값이 필요할 때까지 계산을 미루는 방식입니다. 이를 통해 불필요한 계산을 방지하고 성능을 향상시킬 수 있습니다.

1. 기본 개념

Rust에서 이터레이터는 Iterator 트레잇을 구현한 타입입니다. 이 트레잇은 next() 메서드를 정의합니다.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
  • next()는 Option Enum 형식으로 반환하므로, Some(value)를 반환하다가, 더 이상 값이 없으면 None을 반환합니다.
  • for 루프는 내부적으로 이 next()를 호출하여 동작합니다.

가. 예제 1: 기본 사용

fn main() {
    let v = vec![10, 20, 30];
    let mut iter = v.iter(); // 불변 참조로 이터레이터 생성

    while let Some(x) = iter.next() {
        println!("값: {x}");
    }
}
let v = vec![10, 20, 30]
  • v는 정수형 벡터입니다.
  • 즉, Vec 타입이고 [10, 20, 30]이라는 세 개의 요소를 가지고 있습니다.
let mut iter = v.iter()
  • v.iter()는 벡터 v의 각 요소에 대한 불변 참조 (&i32)를 반환하는 이터레이터를 생성합니다.
  • 즉, iter는 &10, &20, &30을 순서대로 반환할 준비가 된 상태입니다.
  • iter는 가변 변수로 선언되었습니다(mut) → .next()를 호출할 때 이터레이터 내부 상태를 바꾸기 때문입니다.

while let Some(x) = iter.next() { … }
  • .next()는 이터레이터에서 다음 값을 하나씩 꺼냅니다.
  • 반환값은 Option<&i32>입니다.
  • 값이 있으면 Some(&값)
  • 끝나면 None
  • while let Some(x)는 Some일 때 루프를 돌고, None이면 종료됩니다.
println!(“값: {x}”);
  • x는 &i32 타입이므로 10, 20, 30이 참조 형태로 출력됩니다.
  • println!은 참조를 자동으로 역참조해서 출력해주기 때문에 따로 *x를 쓰지 않아도 됩니다.

나. 예제 2: for 루프 사용

fn main() {
    let v = vec![1, 2, 3];

    for num in v.iter() {
        println!("num = {num}");
    }
}

while문과 아래가 다릅니다.

for num in v.iter()
  • v.iter()는 불변 참조 이터레이터를 생성합니다.
    • 즉, &1, &2, &3을 순서대로반환합니다.
  • for 루프는 이터레이터의 .next()를 자동으로 반복 호출하여 값을 하나씩 꺼냅니다.
  • 변수 num의 타입은 &i32입니다 (참조).
  • v.iter()는 벡터를 소유하지 않고 참만 하므로, v는 이루프 이후에도 여전히 사용 가능합니다.
  • println!(“num = {num}”);에 따라 1,2,3이 출력됩니다.

2. 소비(consuming) 어댑터

이터레이터를 사용해 데이터를 소모하는 메서드입니다.

  • .sum(): 합계 반환
  • .count(): 요소 개수
  • .collect(): 컬렉션으로 변환
fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let sum: i32 = v.iter().sum();
    println!("합계: {}", sum);
}

여기서 특이한 점은 sum 다음에 i32라고 타입이 명시되어 있다는 점입니다.

: i32를 빼고 실행하면 “type annotations needed”라고 에러가 발생하는데,

iter().sum()의 반환 형식을 지정하지 않으면 에러 발생

sum이 &i32를 받아서 더할 수는 있지만, 반환값의 형식을 추론할 수 없기 때문에 안정성과 명확성을 추구하는 Rust가 에러를 발생시키는 것입니다.

3. 변형(transforming) 어댑터

이터레이터에서 새로운 이터레이터를 생성하지만 실제 순회는 for, collect 등으로 실행되기 전까지 지연 평가됩니다.

  • .map(): 각 요소를 변형
  • .filter(): 조건에 맞는 요소만 남김

가. map() 예제

fn main() {
    let v = vec![1, 2, 3, 4];

    let doubled: Vec<i32> = v.iter()
        .map(|x| x * 2)
        .collect();

    println!("{doubled:?}"); // [2, 4, 6, 8]
}
v.iter()
  • 벡터 v에 대해 불변 참조 이터레이터를 생성합니다.
  • 반환 타입은 impl Iterator<Item = &i32> → 각 요소는 &1, &2, &3, &4.
.map(|x| x * 2)
  • map은 이터레이터의 각 항목에 closure를 적용해 새로운 이터레이터를 만듭니다.
  • 여기서 x는 &i32이므로, x * 2는 실제로는 *x * 2와 같은 의미입니다.
  • 즉, 값은 다음과 같이 변합니다:
  • &1 → 1 * 2 → 2
  • &2 → 2 * 2 → 4
  • x는 &i32이기 때문에 직접 곱하려면 *x * 2라고 해야 하지만, Rust는 x * 2를 보면 자동으로 역참조(*x) 해주기 때문에 생략 가능합니다.

.collect()
  • 이터레이터 결과를 컨테이너 타입(여기서는 Vec)으로 수집합니다.
  • 이 부분에서 타입 추론이 불가능할 수 있기 때문에, doubled: Vec<i32>로 타입을 명시했습니다.

println!(“{doubled:?}”);
  • {:?}는 벡터를 디버그 형식으로 출력해줍니다.
  • 출력 결과는 [2, 4, 6, 8]입니다.

나. filter() 예제

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let even: Vec<_> = v.into_iter()
        .filter(|x| x % 2 == 0)
        .collect();

    println!("{:?}", even); // [2, 4]
}

다른 것은 같고 filter 부분만 다른데, x를 2로 나눴을 때 나머지가 0인 것을 % 연산자(나머지 연산자)로 구해서 해당되는 것만 collect(수집)하는 것입니다.

4. 소유권과 이터레이터

이터레이터는 다음 세 가지 방식으로 만들 수 있습니다.

메서드설명
.iter()불변 참조 이터레이터
.iter.mut()가변 참조 이터레이터
.into_iter()소유권을 이동하는 이터레이터
fn main() {
    let mut v = vec![1, 2, 3];

    for x in v.iter_mut() {
        *x *= 10;
    }

    println!("{:?}", v); // [10, 20, 30]
}

vector 변수 v를 가변 참조로 선언한 다음,
값을 하나씩 꺼내서 10을 곱한 다음 x에 저장하므로 v 변수가 변경됩니다.
이후 벡터 변수 v를 디버그 포맷으로 출력합니다.

5. 사용자 정의 이터레이터

직접 구조체에 Iterator 트레잇을 구현하여 사용자 정의 이터레이터를 만들 수도 있습니다.

struct Counter {
    count: usize,
}

impl Counter {
    fn new() -> Self {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let c = Counter::new();
    for i in c {
        println!("{}", i);
    }
}
가. 구조체 정의
struct Counter {
    count: usize,
}

usize 타입의 count 필드를 가진 Counter 구조체를 정의합니다.

나. Counter의 new 메소드 구현
impl Counter {
    fn new() -> Self {
        Counter { count: 0 }
    }
}

Counter 구조체의 fn new() 메소드를 정의하는데, count 필드의 값을 0으로 초기화합니다.

다. Counter를 위한 Iterator 트레잇 구현
impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

Counter 구조체를 위해 Iterator 트레잇을 구현하는데
fn new() 메소드에서 반환값은 Item인데 usize 형식이고,
매번 next()가 호출될 때 count를 1씩 증가시키고, 5보다 작거나 같으면 Some(count)을 반환하고, 그렇지 않으면 None을 반환하여 반복을 종료합니다.

라. 메인 함수
fn main() {
    let c = Counter::new();
    for i in c {
        println!("{}", i);
    }
}

Count 구조체의 count 필드를 0으로 초기화한 후 c에 넣고
c를 1씩 증가시키면서 실행하는데 5까지만 출력하고 종료합니다.
Counter::new()로 만든 c는 Iterator를 구현하고 있기 때문에 for 루프에서 사용 가능합니다.

6. 정리

  • Iterator는 next()를 통해 요소를 하나씩 반환합니다.
  • .map, .filter 등은 지연 평가(lazy evaluation) 방식으로 동작합니다.
  • .collect()나 for 루프 등을 통해 실제로 실행됩니다.
  • 반복 가능한 자료형은 대부분 이터레이터를 제공합니다 (Vec, HashMap, Range 등)
  • Rust의 함수형 프로그래밍 스타일을 구성하는 핵심 개념입니다.

벡터(Vector), 문자열(String), 슬라이스(Slice)

Rust에서는 데이터를 저장하고 조작하기 위해 다양한 컬렉션을 제공합니다. 컬렉션에는 벡터, 문자열, 슬라이스와 해시맵이 있는데, 오늘은 그중 자주 쓰이는 벡터, 문자열, 그리고 슬라이스에 대해 알아보고 다음 편에 해시맵(HashMap)에 대해 알아보겠습니다. 문자열은 Rust의 특이한 요소 중 하나입니다.


1. 벡터 (Vector)

Vec는 가변 길이의 배열로, 가장 자주 쓰이는 컬렉션 중 하나입니다.

fn main() {
    let mut v: Vec<i32> = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);

    println!("{:?}", v); // [1, 2, 3]
}
  • 고정된 길이의 array와 대비되고, 같은 데이터 타입이어야 하는 것은 동일합니다.
    데이터 형식으로 Vec안에 i32라고 하나만 지정되어 있어서 여러가지 형식 입력이 가능한 tuple과 다릅니다.
  • Vec::new()로 생성하고 push()로 요소 추가. pop()으로 마지막 요소 삭제
    v.pop(); // 마지막 요소 제거
    println!(“{:?}”, v); // [1, 2]
  • 벡터 v는 mut로 가변 변수로 선언해야 데이터 추가, 삭제, 수정 가능
  • println!(“{:?}”, v)로 Debug 포맷으로 벡터 출력.

가. 벡터 초기화

let v = vec![10, 20, 30];
  • vec!를 이용해 여러 요소를 한꺼번에 입력할 수 있습니다. vec!에서 !는 매크로라고 읽습니다.

나. 벡터 접근

fn main() {
    let v = vec![10, 20, 30];
    let third = v[2];
    println!("세 번째 값: {}", third);
    // println!("세 번째 값: {}", v[3]); // panic 발생

    let maybe = v.get(1); // Option 타입 반환

    if let Some(val) = maybe {
        println!("값: {}", val);
    } else {
        println!("없음"); // None일 때 실행
    }
}
  • 벡터의 값을 추출할 때 변수명 다음에 대괄호를 입력하고 그 안에 인덱스를 입력할 수도 있고, .get을 이용할 때는 소괄호를 이용하는데, 둘의 차이점은 대괄호를 이용할 때는 인덱스가 존재하지 않으면 패닉이 발생하나, get을 이용하면 None이 반환됩니다.
  • 위 코드를 실행하면 println!(“세 번째 값: {}”, third);은 실행되는데,
    println!(“세 번째 값: {}”, v[3]);에서 패닉이 발생하므로 이후 코드는 실행되지 않습니다.
  • 따라서, println!(“세 번째 값: {}”, v[3]);을 Ctrl + /를 눌러 주석처리한 다음 실행하면 뒷 부분 get으로 구한 값까지 표시됩니다.
  • get 다음에 index로 범위를 벗어난 5를 입력하고 실행하면 None이 되므로 else문이 실행되어 “없음”이 표시됩니다.

2. 문자열 (String)

가. 문자열의 정의

fn main() {
    let s1 = String::from("Hello");
    let s2 = "World!".to_string();

    println!("{s1}, {s2}");
}
  • String은 가변 문자열 타입으로 Heap에 저장되며,
  • 일반적인 프로그래밍 언어는 큰 따옴표안에 문자열을 입력하는데, Rust는 ① String::from 다음의 괄호안에 큰 따옴표를 이용해 문자열을 입력하거나, ②큰 따옴표 안에 문자열을 입력한 후 .to_string을 추가해서 입력합니다.
  • String::from없이 큰 따옴표 안에 문자열을 넣으면 String이 아니라 다음에 설명하는 문자열 슬라이스가 되어 성격이 다릅니다.
  • 위 코드를 실행하면

나. 문자열 연결

    let s2 = "World!".to_string();
    let s3 = s1 + ", " + &s2;
    println!("{s3}");
    // println!("{s1}");
  • 문자열 연결은 + 연산자를 사용합니다.
  • let s3 = s1 + “, ” + &s2;에서 s2는 빌림(&)을 사용해서 + 후에도 존재하나, s1은 + 후에 s3로 move되었으므로 더 이상 사용할 수 없습니다.

다. 슬라이스 (Slice)

슬라이스는 컬렉션의 일부를 참조하는 타입입니다.

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..11];

    println!("{}, {}", hello, world);
}
  • &s[a..b]는 a부터 b-1까지의 부분 문자열을 참조합니다. 범위 설정과 마찬가지로 b앞에 =을 추가하면 b가 포함됩니다.
  • 슬라이스는 원본이 유효한 동안만 유효합니다.

3. 문자열 리터럴(&str)과 String 비교

Rust에서 &str과 String은 모두 문자열을 나타내는 데 사용되지만, 그 역할과 특징이 다릅니다. &str은 문자열 슬라이스로, 고정 길이이고 값을 직접 소유하지 않습니다. 반면, String은 힙에 할당되어 동적으로 길이를 변경할 수 있으며 값을 소유합니다.

구분&str(문자열 리터럴 )String
저장프로그램 실행 시 정적 메모리(static memory)에 저장됩니다.힙(heap)에 할당되어 동적으로 크기가 변할 수 있습니다.
소유권소유하지 않고 참조만 합니다.데이터를 소유합니다.
가변성변경할 수 없습니다.문자열 내용을 추가, 수정, 삭제할 수 있습니다.
표현&str 또는 “문자열 리터럴” 형태로 표현됩니다. 
예1) let s = “hello world”;
예2) let s = String::from(“hello world”);
let hello = &s[0..5];
String::from(“문자열”) 또는 to_string()과 같은 메서드를 통해 생성합니다. 
예) let s = String::from(“hello world”);

간단하게 말하자면 “hello world”는 문자열 리터럴이고, type은 &str인데, String::from(“hello world”)은 type이 String입니다.
그런데, &str은 &str의 예2처럼 String을 참조하기도 합니다.

Rust의 String은 UTF-8로 인코딩됩니다.

📌 &str과 String 비교 예제 코드

fn main() {
    let s = String::from("hello world");
    let first_word = first_word(&s);
    print!("첫 번째 단어: {}", first_word);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
  • let s = String::from(“hello world”); : s란 변수에 hello world란 String을 저장합니다.
  • let first_word = first_word(&s); : 변수 s를 참조 형식으로 받아 first_word 함수의 인수로 전달하고, 반환 값을 다시 first_word란 변수에 저장합니다.
  • print!(“첫 번째 단어: {}”, first_word); : 위에서 구한 first_world를 화면에 출력합니다.
  • fn first_word(s: &str) -> &str { : first_word 함수는 인수 s를 &str(String 참조) 타입으로 받고, &str 형식으로 반환합니다.
  • let bytes = s.as_bytes(); : &str인 s를 string slice를 byte slice로 바꿉니다.
  • for (i, &b) in bytes.iter().enumerate() { : 위에서 구한 bytes를 하나씩 옮겨가면서 처리하는데(iter), 인덱스를 같이 반환하도록 enumerate를 같이 사용합니다.
  • if b == b’ ‘ { : b가 b’ ‘, 다시 말해 byte literal ‘ ‘와 같은 경우, 다시 말해 공백을 만나게 되면
  • return &s[0..i]; : 공백 전까지의 글자를 반환합니다.
  • &s[..] : &s가 공백 전까지의 글자이므로 이 글자 전체를 반환합니다. 세미콜론이 없으므로 표현식(expression)이고 반환값입니다.
  • 따라서, 위 코드를 실행하면 hello가 반환됩니다.

🧠 요약

타입설명
Vec<T>가변크기 배열, push, get, pop 지원
StringUTF-8로 인코딩된 힙 문자열
&str슬라이스 타입, 컬렉션 일부 참조
슬라이스소유권 없이 일부분만 안전하게 사용 가능

반복문: loop, while, for

Rust에서는 loop, while, for라는 3가지 반복문을 제공합니다: . 사용법은 다른 언어와 비슷한데, Rust에만 특이한 것은 loop에 라벨을 붙일 수 있고, break 다음에 값을 적어 값을 반환할 수 있으며, for문에서 반복하기 위한 iter 메소드가 있습니다.


🔁 무한 루프: loop

fn main() {
    let mut count = 0;

    loop {
        println!("count: {}", count);
        count += 1;

        if count == 3 {
            break;
        }
    }
}
  • loop는 무한히 반복합니다.
  • break를 통해 루프를 종료할 수 있습니다.
    위 코드를 실행하면 count값이 0에서 1씩 증가하다가 3이면 끝나는데, 화면 출력은 그 전에 되므로 0, 1, 2까지만 출력됩니다.
  • break가 없다면 무한 반복하므로 강제로 종료하려면 Ctrl + C를 눌러야 합니다.

📦 값을 반환하는 loop

fn main() {
    let result = loop {
        let num = 5;
        break num * 2;
    };
    println!("결과: {}", result); // 10
}
  • break 뒤에 값을 적으면 루프가 해당 값을 반환하고 종료합니다.
  • 따라서, 위 코드를 실행하면 화면에 결과: 10이 출력됩니다.

Loop 라벨

여러 개의 loop 사이의 모호함을 제거하기 위해 loop에 라벨을 붙일 수 있습니다.

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}
  • loop 라벨은 ‘로 시작하고, 이름을 붙인 다음 :으로 끝납니다.
  • 그리고 이동할 라벨을 지정할 때는 break 또는 continue 다음에 ‘라벨명; 이라고 적습니다.
    위 코드를 보면 첫번째 break 문에서는 loop 라벨을 지정하지 않았으므로 안쪽 loop를 벗어나서 count += 1;로 이동하는데,
    두번째 break 문은 ‘counting_up이라고 loop 라벨을 지정했으므로 바깥쪽 루프를 벗어나서 println!(“count = {count}”);을 실행하게 됩니다.
  • 따라서, 위 코드를 실행하면
    바깥쪽 loop에서 count 값 0과 remaining 값 10을 출력하고,
    안쪽 loop에서 remaining값을 1 감소시켜 9가 되며 9를 출력한 다음, remaining이 9이므로 break문으로 안쪽 loop를 벗어난 다음,
    바깥쪽 loop에서 count값을 1 증가시켜 1을 만든 다음 1을 출력하고, remaining을 10으로 만들고 .
    안쪽 loop로 들어가 remaining 값 10을 출력하고, remaing을 1감소시켜 9를 만들고 9를 출력한 다음 remaining이 9이므로 다시 안쪽 loop를 벗어나
    바깥쪽 loop에서 count값을 1 증가시켜 2를 만든 다음 2를 출력하고, remaining을 10으로 만들고 .
    안쪽 loop로 다시 들어가 remaining 값 10을 출력하고, count값이 2이므로 break ‘counting_up 문이 실행되어 ‘counting_up에 해당하는 바깥쪽 loop를 끝내고,
    count값 2를 출력하고 실행을 마칩니다.


🔄 조건반복: while

fn main() {
    let mut n = 0;

    while n < 5 {
        println!("n: {}", n);
        n += 1;
    }
}
  • 조건이 true인 동안 반복합니다.
  • 일반적인 조건 반복에 사용됩니다.

🔄 조건 반복: while 2

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}
  • 대괄호로 표시된 [10, 20, 30, 40, 50]은 배열이며 요소들을 불러낼 때는 index를 대괄호 안에 넣으며, index는 0부터 시작합니다.
  • 따라서, 위 코드는 배열 a의 요소를 하나씩 꺼내서 화면에 출력하는 것입니다.

🔂 컬렉션 반복: for

  • 배열, 벡터 등 반복 가능한 값들을 순회할 때 사용합니다.
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

위 ‘while 2’와 달리 index를 사용하지 않고 출력하고 있습니다.

fn main() {
    let numbers = [10, 20, 30];

    for num in numbers.iter() {
        println!("num: {}", num);
    }
}

.iter()를 사용해서 출력할 수도 있습니다.
위 코드를 실행하면 numbers 배열의 값을 하나씩 화면에 출력합니다.

📦 범위 반복

fn main() {
    for i in 1..5 {
        println!("{}", i);
    }

    for i in 1..=5 {
        println!("(inclusive) {}", i);
    }
}
  • rust에서 범위는 ..을 사용해서 표현합니다.
    1..5: 1 이상 5 미만이고,
    1..=5: 5를 포함할 때는 =을 앞에 붙이면 됩니다. 따라서, 1..=5는 1 이상 5 이하입니다.


⏹️ 루프 제어: break, continue

fn main() {
    for i in 0..5 {
        if i == 2 {
            continue; // 2는 건너뜀
        }
        if i == 4 {
            break;    // 4에서 종료
        }
        println!("i: {}", i);
    }
}
  • continue는 이후의 구문을 실행하지 않고 건너뛰는 것이며, break는 반복문은 종료하는 것입니다.
  • 따라서, 위 코드를 실행하면 0, 1, 3만 출력됩니다.

🧠 요약

반복문설명
loop무한 루프, break 필요
while조건 기반 루프
for컬렉션/범위 순회, 안전하고 권장됨