이터레이터와 제너레이터
iterable·iterator 프로토콜과 next 반환 계약을 이해하고 generator와 yield로 중단 가능한 순회를 만듭니다.
6장에서는 forEach(), map(), filter() 같은 고차 함수로 배열을 반복 처리했습니다. 하지만 이 메서드들은 배열에 있는 것이라, Map, Set, 문자열처럼 반복할 수 있는 여러 구조를 같은 방식으로 순회하려면 공통 규칙이 필요합니다. 그 규칙이 이터레이터(iterator)이고, 이 규칙을 따르는 객체를 쉽게 만드는 특별한 함수가 제너레이터(generator)입니다.
앞 글 비동기 프로그래밍의 await처럼 제너레이터도 실행을 멈췄다가 이어 갈 수 있습니다. 제너레이터는 값을 쓰는 쪽이 요청한 만큼만 만들어 내고, 그사이 함수의 실행 상태를 기억합니다. 다만 일반 function* 제너레이터가 그 자체로 비동기 작업을 하거나 다른 스레드에서 실행되는 것은 아닙니다.
이터레이터 프로토콜
반복을 이해하려면 세 가지를 구분해야 합니다. 반복을 시작할 수 있는 객체, 지금 어디까지 순회했는지 기억하는 객체, 그리고 다음 값을 하나 요청하는 호출입니다. 자바스크립트는 이를 이터러블(iterable), 이터레이터, next() 호출로 나눕니다.
iterable·iterator·generator 대화 — 상태 추적
next 요청마다 generator가 재개→yield 중단하며 value/done과 현재 실행 위치가 바뀐다. 정상 종료와 return 조기 종료/finally를 구분한 고정 소비자/생성기 snapshot이 유용하다.
SEQUENCE · ITERATION PROTOCOL
iterable·iterator·generator 대화
제너레이터 객체를 순회하는 예제다. next는 본문을 재개하고 yield 결과를 받는다. 정상 완료와 for…of 본문의 조기 종료는 서로 다른 경로다.
1 · iterator 획득
consumer가 iterable의 [Symbol.iterator]()를 호출해 generator object를 받는다.
2 · 각 반복
next()가 body를 resume하고, yield(value)가 { value, done: false }로 돌아온다.
A · 정상 완료
generator가 return 또는 끝에 도달하면 다음 결과가 done: true다. return() 호출은 필요하지 않다.
B · consumer 조기 종료
loop body의 break / return / throw는 IteratorClose를 거쳐 iterator의 return()을 호출하고 generator에 있는 finally 정리 코드를 실행한다.
yield 값과 최종 return 값은 다르다
done: false의 value는 for…of 반복 원소가 된다. done: true의 최종 value는 반복 원소로 사용하지 않는다. 제너레이터 객체 하나가 iterable과 iterator 역할을 함께 한다.
next()는 반복 때마다 사용하지만 return()은 정상 경로의 필수 단계가 아니다. consumer가 반복을 조기에 끝낼 때 IteratorClose가 요청한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 조기 return의 finally 정리는 이미 시작했으며 finally 정리 코드를 가진 본문 generator의 소비자 조기 종료에 한정합니다. 시작 전 return/throw나 임의 iterator에 같은 정리를 보장하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | consumer가 iterable의 [Symbol.iterator]()를 호출해 generator object를 받는다. |
| 원도표 설명 | next()가 body를 resume하고, yield(value)가 { value, done: false }로 돌아온다. |
| 원도표 설명 | generator가 return 또는 끝에 도달하면 다음 결과가 done: true다. return() 호출은 필요하지 않다. |
| 원도표 설명 | loop body의 break / return / throw는 IteratorClose를 거쳐 iterator의 return()을 호출하고 generator에 있는 finally 정리 코드를 실행한다. |
| 원도표 설명 | next()는 반복 때마다 사용하지만 return()은 정상 경로의 필수 단계가 아니다. consumer가 반복을 조기에 끝낼 때 IteratorClose가 요청한다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| iterator 준비 | iterator: 시작/대기 / done: 아직 결과 없음 | iterable에서 iterator를 얻고 next를 호출한다 | iterable과 실행 iterator는 역할이 다르다 |
| yield 재개 | 현재: yield에 멈춤 / done: false | next가 중단 지점까지 실행해 value와 done:false를 반환한다 | 다음 호출까지 generator 본문은 진행하지 않는다 |
| 종료와 finally | 정상 완료 done: true / 조기 return 정리: 이미 시작했고 finally 정리 코드를 가진 generator에서 finally 실행 | 정상 종료는 done:true를 반환하며, 시작한 generator의 조기 return은 finally를 수행한다 | 종료 방식과 시작 전 return/throw의 범위를 분리한다 |
iterable·iterator·generator 대화 — 상태 추적
next 요청마다 generator가 재개→yield 중단하며 value/done과 현재 실행 위치가 바뀐다. 정상 종료와 return 조기 종료/finally를 구분한 고정 소비자/생성기 snapshot이 유용하다.
SEQUENCE · ITERATION PROTOCOL
iterable·iterator·generator 대화
제너레이터 객체를 순회하는 예제다. next는 본문을 재개하고 yield 결과를 받는다. 정상 완료와 for…of 본문의 조기 종료는 서로 다른 경로다.
1 · iterator 획득
consumer가 iterable의 [Symbol.iterator]()를 호출해 generator object를 받는다.
2 · 각 반복
next()가 body를 resume하고, yield(value)가 { value, done: false }로 돌아온다.
A · 정상 완료
generator가 return 또는 끝에 도달하면 다음 결과가 done: true다. return() 호출은 필요하지 않다.
B · consumer 조기 종료
loop body의 break / return / throw는 IteratorClose를 거쳐 iterator의 return()을 호출하고 generator에 있는 finally 정리 코드를 실행한다.
yield 값과 최종 return 값은 다르다
done: false의 value는 for…of 반복 원소가 된다. done: true의 최종 value는 반복 원소로 사용하지 않는다. 제너레이터 객체 하나가 iterable과 iterator 역할을 함께 한다.
next()는 반복 때마다 사용하지만 return()은 정상 경로의 필수 단계가 아니다. consumer가 반복을 조기에 끝낼 때 IteratorClose가 요청한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 조기 return의 finally 정리는 이미 시작했으며 finally 정리 코드를 가진 본문 generator의 소비자 조기 종료에 한정합니다. 시작 전 return/throw나 임의 iterator에 같은 정리를 보장하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | consumer가 iterable의 [Symbol.iterator]()를 호출해 generator object를 받는다. |
| 원도표 설명 | next()가 body를 resume하고, yield(value)가 { value, done: false }로 돌아온다. |
| 원도표 설명 | generator가 return 또는 끝에 도달하면 다음 결과가 done: true다. return() 호출은 필요하지 않다. |
| 원도표 설명 | loop body의 break / return / throw는 IteratorClose를 거쳐 iterator의 return()을 호출하고 generator에 있는 finally 정리 코드를 실행한다. |
| 원도표 설명 | next()는 반복 때마다 사용하지만 return()은 정상 경로의 필수 단계가 아니다. consumer가 반복을 조기에 끝낼 때 IteratorClose가 요청한다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| iterator 준비 | iterator: 시작/대기 / done: 아직 결과 없음 | iterable에서 iterator를 얻고 next를 호출한다 | iterable과 실행 iterator는 역할이 다르다 |
| yield 재개 | 현재: yield에 멈춤 / done: false | next가 중단 지점까지 실행해 value와 done:false를 반환한다 | 다음 호출까지 generator 본문은 진행하지 않는다 |
| 종료와 finally | 정상 완료 done: true / 조기 return 정리: 이미 시작했고 finally 정리 코드를 가진 generator에서 finally 실행 | 정상 종료는 done:true를 반환하며, 시작한 generator의 조기 return은 finally를 수행한다 | 종료 방식과 시작 전 return/throw의 범위를 분리한다 |
위 그림은 for...of가 제너레이터 객체를 순회하는 과정을 소비자, 이터러블, 제너레이터, 본문 사이의 대화로 보여 줍니다. 소비자인 for...of는 먼저 이터러블의 [Symbol.iterator]()를 호출해 이터레이터를 얻습니다. 그다음 반복마다 next()를 요청하면 제너레이터 본문이 이어서 실행되다가 yield에서 멈추고, { value, done: false } 형태의 결과가 돌아옵니다. done이 false인 동안 이 요청이 반복됩니다.
끝나는 경로는 두 가지입니다. 정상 종료는 next()를 호출했을 때 본문이 끝까지 실행되거나 return을 만나, 최종 값과 done: true가 돌아오는 경우이며 이때 따로 return()을 호출할 필요는 없습니다. 조기 종료는 반복 본문에서 break, return, throw로 일찍 빠져나오는 경우로, 이때 for...of가 이터레이터의 return()을 호출해 제너레이터의 finally 정리 코드를 실행합니다.
그림 아래의 설명처럼 yield로 내보낸 값과 최종 return 값은 다르게 다뤄집니다. done: false인 결과의 값은 for...of의 반복 원소가 되지만, done: true인 결과의 최종 값은 반복 원소로 쓰이지 않습니다. 또 제너레이터 객체 하나가 이터러블과 이터레이터 역할을 함께 합니다.
이터러블 객체
이터러블은 Symbol.iterator라는 이름의 메서드를 자기 자신이나 프로토타입 체인에 가진 객체입니다. 이 메서드를 호출하면 이터레이터 객체를 돌려줍니다. 배열(Array), 문자열(String), Map, Set, 함수 안의 arguments 객체는 자바스크립트에 내장된 이터러블이고, DOM의 NodeList처럼 브라우저가 제공하는 객체 가운데에도 이터러블이 있습니다.
for...of는 이터러블을 순회할 때 안에서 이 규칙을 그대로 사용합니다.
const myArray = [1, 2, 3];
const myString = "hello";
const mySet = new Set([1, 2, 3]);
// 이터러블은 for...of로 순회 가능
for (const item of myArray) {
console.log(item); // 1, 2, 3
}
for (const char of myString) {
console.log(char); // h, e, l, l, o
}
for (const item of mySet) {
console.log(item); // 1, 2, 3
}문자열은 글자 하나씩, Set은 넣은 순서대로 값을 하나씩 꺼냅니다. 반면 { a: 1 } 같은 일반 객체에는 Symbol.iterator 메서드가 없어 이터러블이 아니므로, for...of에 넣으면 TypeError가 납니다.
이터레이터 객체
이터레이터는 next() 메서드를 가진 객체이며, next()는 호출할 때마다 결과 객체를 돌려줘야 합니다. 결과 객체의 value는 이번에 꺼낸 값이고, done은 순회가 끝났는지를 나타내는 불리언입니다. 규칙상 두 속성은 생략할 수 있고 생략한 done은 false로 처리되지만, 숫자 같은 값을 결과 객체 없이 그대로 돌려주는 next()는 규칙에 맞지 않습니다.
const numbers = [10, 20, 30];
// 배열의 Symbol.iterator 메서드를 호출하여 이터레이터 객체를 얻습니다.
const iterator = numbers[Symbol.iterator]();
console.log(iterator.next()); // 결과: { value: 10, done: false }
console.log(iterator.next()); // 결과: { value: 20, done: false }
console.log(iterator.next()); // 결과: { value: 30, done: false }
console.log(iterator.next()); // 결과: { value: undefined, done: true } (더 이상 요소가 없음)이터러블에서 Symbol.iterator를 직접 호출해 이터레이터를 얻고, next()를 한 번씩 호출하면 for...of가 하는 일을 손으로 따라갈 수 있습니다. 요소를 모두 꺼낸 뒤의 next()는 done: true를 돌려주며, for...of는 이 결과를 받으면 반복을 끝냅니다. done: true인 결과에도 제너레이터의 return 값처럼 최종 값이 있을 수 있지만, 그 값은 반복 원소가 되지 않습니다.
직접 이터러블 만들기
직접 만든 객체도 Symbol.iterator 메서드를 구현하면 for...of로 순회할 수 있습니다. 다음 range 객체는 1부터 5까지의 수를 나타내며, Symbol.iterator 메서드가 next()를 가진 이터레이터 객체를 만들어 돌려줍니다. 속성 이름을 대괄호로 감싼 [Symbol.iterator]는 계산된 속성 이름(computed property name)으로, 심벌을 속성 이름으로 쓸 때 이렇게 적습니다.
// 숫자 범위를 나타내는 사용자 정의 이터러블 객체
const range = {
from: 1,
to: 5,
[Symbol.iterator]: function() { // Symbol.iterator 메서드를 구현합니다.
let current = this.from; // 클로저처럼 from과 to를 기억합니다.
const last = this.to;
return { // 이터레이터 객체를 반환합니다.
next() { // 이터레이터는 next() 메서드를 가져야 합니다.
if (current <= last) {
return { done: false, value: current++ };
} else {
return { done: true };
}
}
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// 직접 이터레이터 사용도 가능
const rangeIterator = range[Symbol.iterator]();
console.log(rangeIterator.next()); // { done: false, value: 1 }range의 Symbol.iterator 메서드는 호출될 때마다 current를 from부터 새로 시작하는 이터레이터를 만들어 돌려줍니다. 그래서 for...of가 사용한 이터레이터와 마지막 줄에서 직접 얻은 rangeIterator는 서로 다른 객체이고, 직접 호출한 next()도 다시 1부터 시작합니다. 이 예제는 from과 to가 정수라고 가정합니다.
이 이터레이터에는 next()만 있고 Symbol.iterator가 없으므로, for (const n of rangeIterator)처럼 이터레이터 자체를 for...of에 넣으면 이터러블이 아니라는 TypeError가 납니다. 반복을 시작할 수 있는 이터러블 range와, 순회 위치만 기억하는 이터레이터를 구별해 두세요. 배열이나 제너레이터가 돌려주는 내장 이터레이터는 Symbol.iterator가 자기 자신을 돌려주도록 되어 있어 for...of에 바로 넣을 수 있습니다.
제너레이터
제너레이터는 이터레이터를 훨씬 쉽게 만들 수 있게 해 주는 특별한 함수입니다. 제너레이터 함수는 실행을 잠시 멈췄다가 다시 이어 갈 수 있으며, yield 키워드로 값을 하나씩 내보냅니다.
제너레이터 함수 선언
제너레이터 함수는 function* 문법으로 선언합니다. 일반 함수와 달리 제너레이터 함수를 호출하면 본문이 바로 실행되지 않고, 제너레이터 객체(generator object)가 반환됩니다. 이 객체는 이터레이터이자 이터러블입니다.
function* simpleGenerator() {
console.log("제너레이터 시작");
yield 1; // 첫 번째 값을 생성하고 여기서 일시 중지
console.log("첫 번째 yield 후");
yield 2; // 두 번째 값을 생성하고 일시 중지
console.log("두 번째 yield 후");
return 3; // 마지막 값 (done: true)
console.log("return 후 (실행 안 됨)"); // return 이후 코드는 실행되지 않음
}
const generator = simpleGenerator(); // 함수 호출 시 제너레이터 객체 반환
console.log("제너레이터 함수 호출됨, 하지만 아직 실행 안 됨");
console.log(generator.next()); // '제너레이터 시작' 출력, { value: 1, done: false } 반환
console.log(generator.next()); // '첫 번째 yield 후' 출력, { value: 2, done: false } 반환
console.log(generator.next()); // '두 번째 yield 후' 출력, { value: 3, done: true } 반환
console.log(generator.next()); // { value: undefined, done: true } 반환simpleGenerator()를 호출해도 본문은 실행되지 않으므로 “제너레이터 함수 호출됨, 하지만 아직 실행 안 됨”이 가장 먼저 출력됩니다. 첫 next()에서야 본문이 시작되어 “제너레이터 시작”을 출력하고, 첫 yield에서 1을 내보내며 멈춥니다. 이후 next()를 호출할 때마다 멈춘 곳에서 다음 yield까지 실행되고, return 3에 이르면 { value: 3, done: true }로 끝납니다. 끝난 제너레이터의 next()는 계속 { value: undefined, done: true }를 돌려줍니다.
yield 키워드
yield는 제너레이터 함수 안에서만 쓸 수 있습니다. yield를 만나면 함수의 실행이 멈추고, yield 뒤의 값이 결과 객체의 value로 반환됩니다. 다음에 next()가 호출되면 함수는 멈췄던 yield 바로 다음부터 실행을 이어 갑니다.
for...of와 제너레이터
제너레이터 객체는 이터러블이므로 for...of에 바로 넣을 수 있습니다. 앞의 range처럼 next()를 가진 객체를 직접 만들지 않아도, 반복문 안에서 yield만 하면 같은 일을 하는 이터러블이 됩니다.
function* countGenerator() {
for (let i = 1; i <= 5; i++) {
yield i;
}
}
for (const num of countGenerator()) {
console.log(num); // 1, 2, 3, 4, 5
}next()로 값 전달하기
next(value)처럼 인수를 넘기면, 그 값은 제너레이터 안에서 직전에 멈춰 있던 yield 식의 결과가 됩니다. 이렇게 바깥에서 제너레이터 안으로 값을 넣어 동작을 제어할 수 있습니다.
function* multiplyGenerator() {
console.log("제너레이터 시작");
const num1 = yield; // 첫 번째 yield는 값을 받기 위해 대기
console.log(`첫 번째 받은 값: ${num1}`);
const num2 = yield; // 두 번째 yield는 값을 받기 위해 대기
console.log(`두 번째 받은 값: ${num2}`);
return num1 * num2;
}
const multiplier = multiplyGenerator();
console.log(multiplier.next()); // 결과: 제너레이터 시작, { value: undefined, done: false }
console.log(multiplier.next(5)); // 결과: 첫 번째 받은 값: 5, { value: undefined, done: false }
console.log(multiplier.next(10)); // 결과: 두 번째 받은 값: 10, { value: 50, done: true }
console.log(multiplier.next()); // 결과: { value: undefined, done: true }첫 next()는 본문을 시작해 “제너레이터 시작”을 출력하고 첫 yield에서 멈춥니다. 이때는 값을 받을 이전 yield가 없으므로, 첫 호출을 next(99)처럼 인수와 함께 해도 99는 쓰이지 않고 버려집니다. 두 번째 호출의 5는 첫 yield의 결과로 num1에, 세 번째 호출의 10은 두 번째 yield의 결과로 num2에 들어가며, 마지막에 return num1 * num2가 { value: 50, done: true }를 돌려줍니다.
next()에 넘긴 값은 이번 호출에서 새로 멈출 yield가 아니라, 직전에 멈춰 있던 yield로 들어간다는 점이 핵심입니다. 그래서 제너레이터는 yield로 값을 내보내면서, 다음 next() 호출에서 바깥의 값을 받아들일 수 있습니다. 최종 return 값인 50은 next()로 직접 읽을 때만 보이고, for...of로 순회하면 반복 원소에 들어가지 않습니다.
yield*로 위임하기
yield*는 다른 이터러블이나 제너레이터에 순회를 맡길 때 씁니다. yield* 자리에서 위임받은 쪽이 내보내는 값을 모두 차례로 내보낸 뒤, 원래 제너레이터의 다음 줄로 돌아옵니다.
function* generatorA() {
yield 1;
yield 2;
}
function* generatorB() {
yield 3;
yield* generatorA(); // generatorA의 모든 yield 값을 내보냄
yield 4;
}
for (const value of generatorB()) {
console.log(value); // 3, 1, 2, 4
}generatorB가 3을 내보낸 뒤 yield* 자리에서 generatorA의 1과 2를 모두 내보내고, 그다음에야 자기 코드로 돌아와 4를 내보내므로 출력 순서가 3, 1, 2, 4가 됩니다.
제너레이터를 쓰는 곳
제너레이터는 값을 필요할 때 하나씩 만들어 내므로, 끝이 없는 데이터나 무한 스크롤처럼 값을 미리 다 만들어 둘 수 없는 경우에 알맞습니다. 다음 제너레이터는 while (true)로 끝없이 반복하지만, yield에서 매번 멈추므로 호출한 쪽이 next()를 부를 때만 다음 수를 하나 만듭니다.
function* infiniteSequence() {
let i = 0;
while (true) { // 무한 루프
yield i++;
}
}
const seq = infiniteSequence();
console.log(seq.next().value); // 0
console.log(seq.next().value); // 1
// 필요한 만큼만 값을 가져오므로 무한히 생성 가능필요한 만큼만 next()를 호출하므로 끝이 없는 제너레이터도 안전하게 쓸 수 있습니다. 하지만 [...seq]나 종료 조건 없는 for...of처럼 끝까지 꺼내려는 코드에 넣으면 영원히 끝나지 않아 실행 흐름을 계속 붙잡게 됩니다. 이 예제는 숫자가 아주 커졌을 때의 정밀도까지 보장하지는 않습니다.
async/await가 생기기 전에는 제너레이터로 비동기 코드를 동기 코드처럼 쓰는 코루틴(coroutine) 패턴이 쓰였고, co 같은 라이브러리가 이를 도왔습니다.
// 개념 예시: 실행기(runner)와 실제 /api 서버가 있어야 동작합니다.
function* fetchUserAndPosts() {
const user = yield fetch('/api/user').then(res => res.json());
const posts = yield fetch(`/api/posts?userId=${user.id}`).then(res => res.json());
return { user, posts };
}
// 이런 제너레이터는 실행기가 yield된 Promise를 기다렸다가
// 그 결과를 next()로 다시 넣어 주는 일을 반복해야 합니다.
// 이 과정을 언어에 넣은 것이 async/await입니다.이 함수를 호출하기만 해서는 본문이 실행되지 않고, 단순히 next()를 반복한다고 Promise가 끝날 때까지 기다려 주지도 않습니다. 실행기가 yield로 나온 Promise를 기다린 뒤 그 결과를 next(결과)로 넣어 줘야 user에 실제 값이 들어갑니다. 앞 글의 await가 바로 이 일을 언어 차원에서 해 줍니다.
이 밖에도 제너레이터는 정해진 상태를 차례로 오가는 유한 상태 기계(finite state machine)를 표현하는 데 쓸 수 있습니다. 직접 만든 객체를 for...of로 순회하게 만들 때도, 앞의 range처럼 next()를 직접 구현하는 것보다 *[Symbol.iterator]() { ... }처럼 제너레이터 메서드로 쓰는 편이 훨씬 간단합니다.
더 깊이: 이터레이터에도 map과 filter가 생겼다
예전에는 이터레이터의 값을 map()이나 filter()로 다루려면 먼저 [...iter]로 배열을 만들어야 했고, 끝이 없는 제너레이터에는 이 방법을 쓸 수 없었습니다. ECMAScript 2025에는 이 문제를 푸는 이터레이터 도우미(iterator helpers)가 들어왔습니다. 모든 내장 이터레이터와 제너레이터 객체가 물려받는 Iterator.prototype에 map(), filter(), take(), drop(), flatMap(), reduce(), toArray(), forEach(), some(), every(), find() 같은 메서드가 생긴 것입니다.
이 가운데 map(), filter(), take(), drop(), flatMap()은 배열 메서드와 달리 결과를 미리 만들지 않고, 새 이터레이터를 돌려준 뒤 값을 요청받을 때마다 원래 이터레이터에서 하나씩 꺼내 처리합니다. 그래서 1부터 1씩 커지는 수를 끝없이 내보내는 제너레이터 naturals()가 있다면 naturals().filter(n => n % 2 === 0).take(3).toArray()는 앞의 짝수 세 개만 계산해 [2, 4, 6] 같은 배열을 만들고 멈춥니다. 반면 toArray()나 reduce()처럼 값을 모두 모으는 메서드를 끝이 없는 이터레이터에 바로 쓰면, 앞에서 본 펼치기처럼 끝나지 않습니다.
직접 만든 이터레이터 객체는 Iterator.prototype을 물려받지 않을 수 있으므로, Iterator.from(객체)로 감싸면 같은 메서드를 쓸 수 있습니다. MDN의 take() 설명에 따르면 이 메서드들은 2025년 3월부터 주요 브라우저의 최신 버전에서 함께 쓸 수 있습니다. 더 오래된 환경까지 지원해야 한다면 브라우저에 없는 기능을 코드로 채워 주는 폴리필(polyfill)을 검토합니다. 폴리필을 넣는 방법은 14장의 Babel과 Polyfill에서 다룹니다.
일찍 끝낸 순회와 멈춰 둔 순회
for...of를 break로 일찍 끝내면 이터레이터의 return()이 호출되고, 제너레이터는 이때 finally 블록을 실행합니다. 반면 next()를 직접 한 번 호출한 것만으로는 순회를 닫았다는 뜻이 아닙니다.
function* stepsForIteration() {
try {
yield 1;
yield 2;
} finally {
console.log("정리");
}
}
const closedForIteration = stepsForIteration();
for (const value of closedForIteration) {
console.log("값", value);
break;
}
console.log("닫힌 결과", closedForIteration.next().done);출력은 값 1, 정리, 닫힌 결과 true입니다. 첫 yield에서 1을 받은 뒤 break로 빠져나오면 for...of가 return()을 호출하고, 제너레이터는 멈춰 있던 try 블록을 빠져나가며 finally의 “정리”를 출력합니다. 닫힌 제너레이터이므로 그 뒤의 next()는 done: true를 돌려줍니다.
그림의 조기 종료 화살표가 바로 이 return() 요청입니다. 정상적으로 done: true를 받아 끝난 경우나 next() 자체가 오류를 던진 경우에는 for...of가 return()을 따로 호출하지 않으므로, 모든 종료에서 같은 정리 호출이 일어난다고 생각하면 안 됩니다.
정리
- 이터러블은
Symbol.iterator메서드로 이터레이터를 돌려주는 객체이고, 이터레이터는{ value, done }을 돌려주는next()를 가진 객체입니다. for...of는 이터러블에서 이터레이터를 얻어done이true가 될 때까지next()를 호출하며,done: true의 최종 값은 반복 원소로 쓰지 않습니다.- 제너레이터 함수(
function*)는 호출하면 본문 대신 제너레이터 객체를 돌려주고,next()마다 다음yield까지 실행했다가 멈춥니다. next(value)의 인수는 직전에 멈춰 있던yield식의 결과가 되므로, 첫next()의 인수는 쓰이지 않습니다.yield*는 다른 이터러블에 순회를 맡기고, 끝이 없는 제너레이터는 필요한 만큼만 꺼내 써야 합니다.break로 순회를 일찍 끝내면return()이 호출되어 제너레이터의finally가 실행되지만, 멈춰 둔 순회는 정리되지 않습니다.
연습 문제
1. 직접 끝까지 요청하기
앞 절의 stepsForIteration으로 새 제너레이터 const pausedForIteration = stepsForIteration();를 만듭니다. for...of 대신 console.log(pausedForIteration.next().value);를 두 번, 이어서 console.log(pausedForIteration.next().done);을 한 번 호출합니다. 출력 순서와 “정리”가 나오는 위치를 예상해 보세요.
해설 보기
1, 2, 정리, true 순서입니다. 처음 두 호출은 각각 yield 1과 yield 2에서 멈춥니다. 세 번째 호출에서 본문이 try 블록 끝까지 진행하면서 finally의 “정리”를 출력하고 끝나므로, 그 결과의 done인 true가 마지막에 출력됩니다.
next()를 한 번만 호출하고 더 요청하지 않으면 제너레이터는 yield 1에서 멈춘 채로 남아 “정리”가 실행되지 않습니다. 파일이나 연결 같은 자원을 다루는 제너레이터라면, 순회를 멈춰 둔 쪽이 return()을 호출해 닫아 주는 처리를 따로 둬야 합니다.
2. yield*의 결과 값
다음 코드는 무엇을 출력할까요? inner의 return 값은 어디로 갈까요?
function* inner() {
yield "a";
return "inner 끝";
}
function* outer() {
const result = yield* inner();
yield result;
}
console.log([...outer()]);해설 보기
["a", "inner 끝"]이 출력됩니다. yield* inner()는 inner가 yield한 "a"를 바깥으로 그대로 내보내고, inner가 끝나면 그 return 값인 "inner 끝"이 yield* 식의 결과가 되어 result에 들어갑니다.
inner의 return 값은 반복 원소가 아니므로 위임 중에는 내보내지지 않습니다. outer가 yield result로 다시 내보냈기 때문에 배열에 들어간 것입니다.
3. 두 번 순회하기
다음 두 줄의 출력은 각각 무엇일까요? 두 결과가 다른 이유를 이터러블과 이터레이터의 차이로 설명해 보세요.
console.log([...range], [...range]);
const counter = countGenerator();
console.log([...counter], [...counter]);해설 보기
첫 줄은 [1, 2, 3, 4, 5] [1, 2, 3, 4, 5], 둘째 줄은 [1, 2, 3, 4, 5] []입니다. 스프레드 문법은 순회할 때마다 Symbol.iterator를 호출해 이터레이터를 새로 얻습니다.
range의 Symbol.iterator는 호출할 때마다 1부터 시작하는 새 이터레이터를 만들므로 두 번 모두 처음부터 순회합니다. 반면 제너레이터 객체의 Symbol.iterator는 자기 자신을 돌려주므로, 첫 전개가 counter를 끝까지 소비한 뒤 두 번째 전개는 이미 끝난 제너레이터에서 아무 값도 얻지 못합니다.
반복 규칙과 조기 종료는 MDN의 반복 프로토콜 문서에서, next()로 값을 전달하는 방식은 MDN의 Generator.prototype.next() 문서에서 확인할 수 있습니다.
다음 글 모듈 시스템에서는 코드를 여러 파일로 나누고 import와 export로 필요한 기능만 주고받는 방법을 살펴봅니다.