Post
Rust iterator 흐름 이해하기: into_iter(), map(), collect()
Rust에서 자주 함께 등장하는 into_iter(), map(), collect()를 소유권과 lazy evaluation 관점에서 한 번에 정리.
2026년 3월 23일
Rust를 보다 보면 into_iter(), map(), collect()가 거의 한 세트처럼 등장합니다. 처음에는 메서드가 셋이나 붙어서 복잡해 보이지만, 실제로는 흐름이 단순합니다.
into_iter()로 값을 꺼낸다.map()으로 값을 바꾼다.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<_>>();
이 흐름은 아래처럼 읽으면 됩니다.
into_iter()로 원본 컬렉션에서 값을 꺼낸다.map()으로 각 값을 가공한다.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 감각이 훨씬 또렷해집니다.