블로그 목록
frontend

이터레이터 헬퍼로 지연 평가 체이닝하기 — 큰 배열에 map().filter() 그만

로그 파일 한 줄씩 파싱하는 코드를 짜다가 걸렸다. 수십만 줄짜리 배열에 .map().filter().slice(0, 10)을 붙였더니, 앞 10개만 필요한데도 map이 전체를 다 돌고, filter도 전체를 다 돌았다. 중간 배열이 두 개나 생기고 나서야 10개를 잘라내는 셈이다.

for 루프로 풀면 되긴 하는데, 체이닝의 가독성을 포기하기가 아까웠다. 그러다 이터레이터 헬퍼(Iterator Helpers)를 알게 됐다. 2025년에 Baseline에 들어와서 이제 최신 브라우저와 Node 22+에서 그냥 쓸 수 있다.

배열 메서드 vs 이터레이터 메서드

핵심 차이는 지연 평가다. 배열의 .map()은 즉시 새 배열을 만든다. 반면 이터레이터의 .map()은 값을 실제로 꺼낼 때까지 아무것도 하지 않는다.

const nums = Array.from({ length: 1_000_000 }, (_, i) => i);

// 기존: map이 100만 개, filter가 100만 개를 돈다
const a = nums
  .map((n) => n * 2)
  .filter((n) => n % 3 === 0)
  .slice(0, 5);

// 이터레이터 헬퍼: 딱 필요한 만큼만 돈다
const b = nums
  .values()               // 배열 → 이터레이터
  .map((n) => n * 2)
  .filter((n) => n % 3 === 0)
  .take(5)                // 5개 나오면 즉시 멈춤
  .toArray();

b는 5개를 채우는 순간 소비를 멈춘다. 100만 번이 아니라 필요한 몇 번만 map/filter가 호출된다. .values()로 이터레이터를 얻고, 마지막에 .toArray()로 배열로 되돌린다.

무한 생성기와도 잘 붙는다

지연 평가라서 끝이 없는 제너레이터에도 그대로 쓸 수 있다. 배열 메서드로는 애초에 불가능한 케이스다.

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

const firstFiveSquares = naturals()
  .map((n) => n * n)
  .take(5)
  .toArray(); // [1, 4, 9, 16, 25]

take 말고도 drop, flatMap, reduce, some, every, find, forEach가 이터레이터에 그대로 올라와 있다. Map이나 Set의 .values(), .entries()에도 바로 이어 붙일 수 있어서 중간에 [...set]으로 펼칠 필요가 없다.

정리

무조건 이걸 쓰라는 건 아니다. 수십 개짜리 작은 배열은 기존 .map().filter()가 더 읽기 편하고 성능 차이도 없다. 이터레이터 헬퍼가 빛나는 지점은 딱 두 가지다 — 데이터가 크고 앞부분만 필요할 때, 그리고 제너레이터처럼 끝을 모르는 스트림을 다룰 때. 그 경우엔 중간 배열을 안 만들고 take에서 딱 멈춰주니 lodash의 _.chain 없이도 지연 파이프라인이 된다.