[JS] iterator/generator 지연평가

JN·2025년 4월 19일

iterator(이터레이터)

자바스크립트에서 반복 가능한 데이터를 순차적으로 탐색할 수 있게 하는 객체이다.
이터레이터의 핵심은 next() 메서드를 통해 값을 하나씩 꺼낼 수 있다는 점이다.
덕분에 한 번에 전체 데이터를 메모리에 올리지 않고, 필요한 시점에만 평가하여 처리할 수 있다. 이러한 방식은 지연 평가(lazy evaluation)라고 불린다.

지연 평가는 필요한 시점에 계산을 수행하는 기법이다.
한 번에 모든 값을 계산하지 않고, 요청할 때마다 하나씩 처리해서 메모리 사용을 줄이고 퍼포먼스를 최적화할 수 있다.
반대는 즉시 평가(Eager Evaluation)로, 배열 같은 구조는 모든 데이터를 즉시 계산하고 메모리에 올려놓는다.

이터러블 객체(예: 배열, 문자열 등)는 Symbol.iterator를 통해 이터레이터를 반환하고,
이터레이터는 next() 메서드를 갖고 있어서 호출할 때마다 다음 값을 하나씩 반환한다.

const arr = [10, 20, 30];
const iter = arr[Symbol.iterator]();

console.log(iter.next()); // { value: 10, done: false }
console.log(iter.next()); // { value: 20, done: false }
console.log(iter.next()); // { value: 30, done: false }
console.log(iter.next()); // { value: undefined, done: true }

이처럼 next() 호출 시점에만 평가되므로 지연 평가라고 할 수 있다.

generator(제너레이터)

제너레이터는 이터레이터를 생성하는 함수다.
function* 으로 정의하고 내부에서 yield로 값을 하나씩 지연 생성한다.

function* numberGenerator() {
  console.log('yield 1');
  yield 1;

  console.log('yield 2');
  yield 2;

  console.log('yield 3');
  yield 3;
}

const iter = numberGenerator();

console.log(iter.next()); // yield 1 -> { value: 1, done: false }
console.log(iter.next()); // yield 2 -> { value: 2, done: false }

활용

간단한 제너레이터 활용 예시로 비동기 작업을 Queue 형태로 구현할 수 있다.

function* taskQueue (tasks) {
	for(const task of tasks){
      yield task;
    }
};

const signupQueue = taskQueue([
  ()=>fetch('/user/exist'),
  ()=>fetch('/user/create'),
  ()=>fetch('/user/login'),
]);

const signup=async()=>{
	let next = signupQueue.next();
  	while(!next.done){
    	const result = await next.value(); // 함수를 호출
 	    next=signupQueue.next();
    }
}

또한 배열로 표현하면 메모리 폭발이 날 수 있는 상황도 이터레이터로 안전하게 다룰 수 있다.
예를들어 무한 수열과 같은 데이터도 이터레이터로 구현할 수 있다.

function* infiniteNumbers() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

const iter = infiniteNumbers();

console.log(iter.next().value); // 0
console.log(iter.next().value); // 1
console.log(iter.next().value); // 2

이터레이터 기반의 커스텀 map, filter도 구현 가능하다.

function* map(iterable, fn) {
  for (const item of iterable) {
    yield fn(item);
  }
}

function* filter(iterable, fn) {
  for (const item of iterable) {
    if (fn(item)) yield item;
  }
}

const numbers = function* () {
  yield* [1, 2, 3, 4, 5];
};

const processed = filter(map(numbers(), x => x * 2), x => x > 5);

for (const num of processed) {
  console.log(num); // 6, 8, 10
}

최신 이터레이터 헬퍼 메서드

2025년부터 도입된 새로운 이터레이터 헬퍼 메서드는 arr.values()와 함께 .map(), .filter(), .drop(), .take() 등의 메서드를 체이닝하여 지연 평가 기반의 데이터 변환을 가능하게 해준다.
기존 배열 메서드는 단계마다 배열을 복사해 성능이 떨어졌지만 이터레이터 체이닝은 메모리 낭비 없이 연산을 진행한다.

  • drop(n): 처음 n개 생략
  • take(n): 처음 n개만 취함
  • filter(fn), map(fn), flatMap(fn)
  • reduce(fn, init), some(fn), every(fn), find(fn)
  • toArray(): 이터레이터 결과를 배열로 변환

신규 메서드를 활용한 예시를 알아보자.
백만 개의 숫자를 가진 Array에서 홀수 2개 추출해야 한다고 가정해보자.

이터레이터를 사용하지 않으면 일반적으로 다음과 같이 구현할 수 있다.

const arr = Array.from({ length: 1000000 }, (_, i) => i);

// 필터로 모든 홀수를 구한 뒤, 앞에서 2개만 추출
const result = arr.filter(x => x % 2 === 1).slice(0, 2);

console.log(result); // [1, 3]

이 구현방식의 경우 arr.filter(...)에서 전체 배열을 순회하면서 모든 홀수를 먼저 메모리에 저장한다.
그 이후 slice(0, 2)로 필요한 앞의 2개만 꺼내기 때문에, 불필요한 계산과 메모리 낭비가 발생하게 된다.


// 배열 방식 (비효율적)
const result1 = arr.filter(x => x % 2 === 1).slice(0, 2);

// 이터레이터 헬퍼 방식 (메모리 최적화)
const result2 = arr.values()
  .filter(x => x % 2 === 1)
  .take(2)
  .toArray();

console.log(result2); // [1, 3]

이터레이터 헬퍼 방식은 조건에 맞는 값을 찾으면 연산을 즉시 종료하기 때문에, 백만 개의 데이터를 전부 순회하지 않는다. 이는 대용량 데이터 처리에서 큰 이점을 제공한다.

2025년 3월 31일 기준 사파리에서도 새로운 헬퍼를 지원하기 시작했고, 크롬과 Firefox는 이미 도입되어 있다.
하지만 일부 구형 브라우저에서는 미지원일 수 있으므로, 사용 시에는 호환성을 고려하거나 폴리필을 적용해야 한다.

결론

이터레이터와 제너레이터는 자바스크립트에서 지연 평가를 구현할 수 있는 강력한 도구이다.
여기에 최신 이터레이터 헬퍼 메서드를 조합하면, 대규모 데이터 처리에서 메모리를 최소화하며 코드 가독성까지 챙길 수 있다.
실무에서 상황에 따라 지연 평가 방식도 적극적으로 활용해야겠다.

profile
개발일지📒

0개의 댓글