DaleSchool

이터레이터: 반복을 Rust답게

중급20분

학습 목표

  • iter(), into_iter(), iter_mut()의 차이를 이해한다
  • map(), filter(), collect() 등 이터레이터 어댑터를 사용할 수 있다
  • for 루프를 이터레이터 체인으로 리팩토링할 수 있다
  • 이터레이터가 지연 평가됨을 설명할 수 있다

동작하는 코드

Phase 1에서 for 루프로 벡터를 순회했었죠? 같은 작업을 이터레이터로 할 수 있어요.

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

    // for 루프 방식
    let mut doubled_loop = Vec::new();
    for n in &numbers {
        doubled_loop.push(n * 2);
    }

    // 이터레이터 체인 방식
    let doubled_iter: Vec<i32> = numbers.iter().map(|n| n * 2).collect();

    println!("for 루프: {:?}", doubled_loop);
    println!("이터레이터: {:?}", doubled_iter);
    // 둘 다 [2, 4, 6, 8, 10]
}

이터레이터 체인은 "무엇을 하고 싶은지"를 선언적으로 표현해요. 모듈 22에서 배운 클로저가 여기서 빛을 발해요!

직접 해보기

  1. numbers.iter().filter(|n| **n > 3).collect::<Vec<_>>()를 실행해보세요. 결과가 뭔가요?
  2. mapfilter를 합쳐서 "3보다 큰 수를 2배로" 만들어보세요.

iter() vs into_iter() vs iter_mut()

세 메서드의 차이는 소유권이에요. Phase 2에서 배운 개념이 여기서도 그대로 적용돼요!

| 메서드 | 요소 타입 | 원본 사용 | 비유 | | ------------- | -------------------- | ------------- | ---------------- | | iter() | &T (불변 참조) | 가능 ✅ | 책을 읽기만 | | iter_mut() | &mut T (가변 참조) | 가능 (수정됨) | 책에 메모를 적음 | | into_iter() | T (소유권 이동) | 불가 ❌ | 책을 가져감 |

fn main() {
    // iter() — 빌려서 읽기
    let names = vec!["민수", "영희", "철수"];
    let lengths: Vec<usize> = names.iter().map(|n| n.len()).collect();
    println!("길이: {:?}", lengths);
    println!("원본: {:?}", names); // ✅ 아직 사용 가능

    // iter_mut() — 빌려서 수정
    let mut scores = vec![80, 90, 75];
    scores.iter_mut().for_each(|s| *s += 5);
    println!("보정 후: {:?}", scores); // [85, 95, 80]

    // into_iter() — 소유권 이동
    let words = vec![String::from("hello"), String::from("world")];
    let upper: Vec<String> = words.into_iter().map(|w| w.to_uppercase()).collect();
    println!("대문자: {:?}", upper);
    // println!("{:?}", words); // ❌ 소유권이 이동됨
}

: for item in &vecvec.iter()와 같고, for item in vecvec.into_iter()와 같아요!

주요 이터레이터 어댑터

map — 변환

각 요소를 다른 값으로 변환해요.

fn main() {
    let names = vec!["alice", "bob", "carol"];
    let greeting: Vec<String> = names
        .iter()
        .map(|name| format!("안녕, {name}!"))
        .collect();
    println!("{:?}", greeting);
    // ["안녕, alice!", "안녕, bob!", "안녕, carol!"]
}

filter — 걸러내기

조건을 만족하는 요소만 남겨요.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let evens: Vec<&i32> = numbers
        .iter()
        .filter(|n| *n % 2 == 0)
        .collect();
    println!("짝수: {:?}", evens); // [2, 4, 6, 8, 10]
}

enumerate — 인덱스와 함께

요소에 인덱스를 붙여줘요. for 루프에서 인덱스가 필요할 때 유용해요.

fn main() {
    let fruits = vec!["사과", "바나나", "포도"];

    // for 루프 + 인덱스 변수
    let mut i = 0;
    for fruit in &fruits {
        println!("{i}: {fruit}");
        i += 1;
    }

    // enumerate — 훨씬 깔끔!
    for (i, fruit) in fruits.iter().enumerate() {
        println!("{i}: {fruit}");
    }
}

zip — 두 컬렉션을 쌍으로

두 이터레이터를 하나로 합쳐요.

fn main() {
    let names = vec!["민수", "영희", "철수"];
    let scores = vec![95, 87, 92];

    let report: Vec<String> = names
        .iter()
        .zip(scores.iter())
        .map(|(name, score)| format!("{name}: {score}점"))
        .collect();

    println!("{:?}", report);
    // ["민수: 95점", "영희: 87점", "철수: 92점"]
}

fold — 하나의 값으로 접기

모든 요소를 하나의 값으로 누적해요. 다른 언어의 reduce와 비슷해요.

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

    // 합계
    let sum = numbers.iter().fold(0, |acc, n| acc + n);
    println!("합계: {sum}"); // 15

    // 문자열 합치기
    let words = vec!["Rust", "는", "멋져요"];
    let sentence = words.iter().fold(String::new(), |mut acc, w| {
        acc.push_str(w);
        acc
    });
    println!("{sentence}"); // Rust는 멋져요
}

: 단순한 합계는 sum()을 쓰는 게 더 간결해요: numbers.iter().sum::<i32>()

for 루프 → 이터레이터 체인 비교

같은 작업을 두 가지 방식으로 비교해볼까요?

과제: 점수 목록에서 60점 이상인 것만 골라 10점 보너스를 더한 합계를 구하기

fn main() {
    let scores = vec![45, 82, 67, 93, 55, 78];

    // for 루프 방식
    let mut total_loop = 0;
    for score in &scores {
        if *score >= 60 {
            total_loop += score + 10;
        }
    }

    // 이터레이터 체인 방식
    let total_iter: i32 = scores
        .iter()
        .filter(|s| **s >= 60)
        .map(|s| s + 10)
        .sum();

    println!("for 루프: {total_loop}");
    println!("이터레이터: {total_iter}");
    // 둘 다 350
}

이터레이터 체인은 각 단계가 뭘 하는지 한눈에 보여요: 필터(60점 이상) → 변환(+10) → 합계.

"왜?" — 지연 평가

이터레이터 어댑터(map, filter 등)는 지연 평가(lazy evaluation) 돼요. .collect().sum() 같은 소비 어댑터를 호출하기 전까지는 아무것도 실행되지 않아요.

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

    // 이 시점에서는 아무 계산도 일어나지 않아요!
    let lazy = numbers.iter().map(|n| {
        println!("처리 중: {n}");
        n * 2
    });

    println!("--- collect 호출 전 ---");

    // collect()를 호출해야 비로소 실행!
    let result: Vec<&i32> = lazy.collect();
    println!("결과: {:?}", result);
}

실행하면 "--- collect 호출 전 ---"이 먼저 출력되고, 그 다음에 "처리 중: ..."이 출력돼요.

이 덕분에 불필요한 계산을 피할 수 있어요. 예를 들어 filter 뒤에 take(3)을 붙이면, 조건을 만족하는 3개를 찾자마자 나머지는 처리하지 않아요.

연습 1 (쉬움). 아래 for 루프를 이터레이터 체인으로 바꿔보세요.

fn main() {
    let words = vec!["hello", "world", "rust"];

    // 이것을 이터레이터 체인으로 바꿔보세요
    let mut uppercased = Vec::new();
    for word in &words {
        uppercased.push(word.to_uppercase());
    }
    println!("{:?}", uppercased);
}

연습 2 (보통). 학생 점수 벡터에서 최고점과 최저점의 차이를 이터레이터 메서드로 구해보세요.

fn score_range(scores: &[i32]) -> Option<i32> {
    // 여기를 완성하세요
    // 힌트: iter().max()와 iter().min()을 사용
    // 둘 다 Option<&i32>를 반환해요
}

fn main() {
    let scores = vec![72, 95, 68, 88, 91];
    match score_range(&scores) {
        Some(range) => println!("점수 범위: {range}"), // 27
        None => println!("점수가 없어요"),
    }
}

연습 3 (도전). 문자열 벡터에서 5글자 이상인 단어의 개수를 세고, 해당 단어들을 대문자로 변환한 벡터도 만들어보세요. 이터레이터 체인을 사용하세요.

fn main() {
    let words = vec!["hi", "hello", "rust", "programming", "code", "iterator"];
    // 5글자 이상인 단어 개수: ???
    // 5글자 이상 + 대문자: ???
}

Q1. iter()into_iter()의 차이는?

  • A) iter()가 더 빠르다
  • B) into_iter()는 불변 참조를 반환한다
  • C) iter()는 빌려서 읽고, into_iter()는 소유권을 가져간다
  • D) 차이가 없다

Q2. 아래 코드의 출력은?

fn main() {
    let nums = vec![1, 2, 3, 4, 5];
    let result: Vec<i32> = nums
        .iter()
        .filter(|n| **n > 2)
        .map(|n| n * 10)
        .collect();
    println!("{:?}", result);
}
  • A) [10, 20, 30, 40, 50]
  • B) [1, 2, 3, 4, 5]
  • C) [30, 40, 50]
  • D) [3, 4, 5]

Q3. 이터레이터의 "지연 평가"란?

  • A) 이터레이터는 느리게 동작한다
  • B) 이터레이터는 항상 모든 요소를 먼저 계산한다
  • C) collect() 등 소비 어댑터를 호출해야 비로소 계산이 실행된다
  • D) map()filter()는 즉시 결과를 반환한다