okja.me icon okja.me

Post

Rust iterator 흐름 이해하기: into_iter(), map(), collect()

Rust에서 자주 함께 등장하는 into_iter(), map(), collect()를 소유권과 lazy evaluation 관점에서 한 번에 정리.

2026년 3월 23일

Writing Notes
#rust#iterator#into-iter#collect#map

Rust를 보다 보면 into_iter(), map(), collect()가 거의 한 세트처럼 등장합니다. 처음에는 메서드가 셋이나 붙어서 복잡해 보이지만, 실제로는 흐름이 단순합니다.

  1. into_iter()로 값을 꺼낸다.
  2. map()으로 값을 바꾼다.
  3. collect()로 다시 원하는 컬렉션에 담는다.

이 글은 이 세 가지를 따로 외우기보다 하나의 데이터 파이프라인으로 이해하는 데 초점을 둡니다.

먼저 큰 흐름부터 보기

아래 코드는 세 메서드가 어떻게 이어지는지 가장 잘 보여줍니다.

let numbers = vec![1, 2, 3];

let doubled: Vec<_> = numbers
    .into_iter()
    .map(|x| x * 2)
    .collect();

이 코드를 문장으로 풀면 이렇습니다.

  • numbers에서 값을 하나씩 꺼내고
  • 각 값을 두 배로 바꾼 뒤
  • 최종 결과를 다시 Vec으로 모은다

핵심은 Rust가 이 과정을 중간 컬렉션 없이 iterator 체인으로 처리한다는 점입니다.

into_iter()는 값을 가져간다

into_iter()는 컬렉션을 iterator로 바꾸면서 내부 요소를 소유권과 함께 꺼냅니다.

let v = vec![String::from("a"), String::from("b")];

for s in v.into_iter() {
    println!("{}", s);
}

// println!("{:?}", v); // 더 이상 사용할 수 없음

여기서 중요한 점은 v를 빌려 보는 게 아니라, v 자체를 소비한다는 것입니다. 그래서 반복이 끝난 뒤 원래 컬렉션 v는 더 이상 사용할 수 없습니다.

비슷한 메서드와 비교하면 차이가 더 분명합니다.

메서드꺼내는 값원본 컬렉션
iter()&T계속 사용 가능
iter_mut()&mut T계속 사용 가능
into_iter()T사용 불가

정리하면 iter()는 빌려서 읽는 방식이고, into_iter()는 아예 값을 가져오는 방식입니다.

map()은 각 요소를 변환하지만 바로 실행되지는 않는다

map()은 iterator의 각 요소에 함수를 적용해 새로운 iterator를 만듭니다.

let v = vec![1, 2, 3];

let iter = v.into_iter().map(|x| x * 2);

여기서 많은 초보자가 한 번 헷갈립니다. 위 코드는 아직 실제 계산을 끝낸 상태가 아닙니다. 단지 “각 요소에 x * 2를 적용하라”는 규칙이 연결된 iterator를 만든 것뿐입니다.

Rust iterator는 기본적으로 lazy합니다. 즉, 실제로 소비되기 전까지는 동작하지 않습니다.

let v = vec![1, 2, 3];

v.into_iter().map(|x| {
    println!("{}", x);
    x * 2
});

위 코드는 출력이 일어나지 않습니다. collect(), sum(), for 같은 소비자가 없기 때문입니다.

map()을 한 줄로 요약하면 이렇습니다.

  • 입력 하나를 받아
  • 다른 값으로 바꾸고
  • 그 결과를 다음 iterator 단계로 넘긴다

예를 들어 숫자를 문자열로 바꾸는 것도 가능합니다.

let v = vec![1, 2, 3];

let strings: Vec<String> = v
    .into_iter()
    .map(|x| x.to_string())
    .collect();

collect()는 iterator 결과를 다시 모은다

collect()는 iterator에서 흘러나오는 값들을 모아 원하는 컬렉션으로 만들어 줍니다.

let v = vec![1, 2, 3];

let new_vec: Vec<i32> = v.into_iter().collect();

여기서 중요한 포인트는 어떤 타입으로 모을지 타입이 결정한다는 점입니다.

let v = vec![1, 2, 3];

let new_vec = v.into_iter().collect::<Vec<i32>>();

혹은 변수 타입으로도 알려줄 수 있습니다.

let new_vec: Vec<i32> = v.into_iter().collect();

타입 정보를 주지 않으면 컴파일러가 “무엇으로 모아야 하는지” 모호해할 수 있습니다.

let v = vec![1, 2, 3];

// let c = v.into_iter().collect(); // 타입 추론 실패 가능

collect()Vec만 만드는 메서드가 아닙니다. FromIterator를 구현한 타입이라면 다양한 결과를 만들 수 있습니다.

let chars = vec!['h', 'e', 'l', 'l', 'o'];
let s: String = chars.into_iter().collect();
use std::collections::HashMap;

let pairs = vec![("a", 1), ("b", 2)];
let map: HashMap<_, _> = pairs.into_iter().collect();

collect()는 “iterator를 끝내고 결과를 담는 단계”라고 보면 이해가 쉽습니다.

세 메서드를 함께 읽는 법

이제 다시 처음 예제로 돌아가 보겠습니다.

let result = vec![1, 2, 3]
    .into_iter()
    .map(|x| x * 2)
    .collect::<Vec<_>>();

이 흐름은 아래처럼 읽으면 됩니다.

  1. into_iter()로 원본 컬렉션에서 값을 꺼낸다.
  2. map()으로 각 값을 가공한다.
  3. collect()로 최종 결과를 원하는 타입에 담는다.

이 패턴은 Rust에서 매우 자주 등장합니다. 특히 데이터를 변환하는 로직을 짧고 명확하게 쓰고 싶을 때 강력합니다.

iter()into_iter()를 함께 구분해 두기

같은 map()이라도 iter()를 쓰느냐 into_iter()를 쓰느냐에 따라 다루는 값이 달라집니다.

let v = vec![String::from("a")];

let a: Vec<_> = v.iter()
    .map(|s| s.len())
    .collect();

let b: Vec<_> = v.into_iter()
    .map(|s| s.len())
    .collect();

차이는 명확합니다.

  • iter()&String을 다룬다.
  • into_iter()String 자체를 다룬다.

즉 읽기만 할 것인지, 소유권까지 가져와 처리할 것인지에 따라 선택이 갈립니다.

실전에서 자주 보는 형태

마지막으로 가장 흔한 패턴 몇 개만 모아 보면 감이 더 빨리 잡힙니다.

필터 후 모으기

let v = vec![1, 2, 3, 4];

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

변환 후 모으기

let v = vec![1, 2, 3];

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

여러 변환을 이어 붙이기

let result = vec![1, 2, 3]
    .into_iter()
    .map(|x| x * 2)
    .map(|x| x + 1)
    .collect::<Vec<_>>();

중간에 Vec를 계속 만들지 않고 한 줄 흐름으로 이어진다는 점이 Rust iterator의 장점입니다.

마무리

세 메서드를 따로 외우면 헷갈리기 쉽지만, 하나의 파이프라인으로 보면 훨씬 단순합니다.

  • into_iter()는 값을 꺼낸다
  • map()은 값을 바꾼다
  • collect()는 다시 담는다

Rust iterator는 처음엔 낯설지만, 이 패턴에 익숙해지면 반복문보다 더 읽기 쉬운 코드가 많아집니다. 다음에는 filter(), filter_map(), flat_map()까지 이어서 보면 iterator 감각이 훨씬 또렷해집니다.