이터레이터·제너레이터 — function*·yield
for...of 의 내부 동작과 게으른 시퀀스의 정석.
7편의 for...of 가 어떻게 동작하나? 답은 이터레이터 프로토콜. 모든 "순회 가능한" 객체는 Symbol.iterator 메서드를 가집니다. 24편은 그 메커니즘과 더 강력한 사촌 제너레이터 함수(function*) 까지.
이터레이터 프로토콜 — 한 메서드 한 형식
// 이터레이터 = next() 메서드가 있는 객체
// next() 는 { value, done } 반환
const iter = {
i: 0,
next() {
if (this.i < 3) return { value: this.i++, done: false };
return { value: undefined, done: true };
}
};
iter.next(); // { value: 0, done: false }
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }
iter.next(); // { value: undefined, done: true }
이터러블 — Symbol.iterator 가진 객체
// 객체가 Symbol.iterator 메서드를 가지면 for...of 사용 가능
const range = {
from: 1, to: 5,
[Symbol.iterator]() {
let i = this.from;
const end = this.to;
return {
next() {
return i <= end
? { value: i++, done: false }
: { value: undefined, done: true };
}
};
}
};
for (const n of range) console.log(n); // 1, 2, 3, 4, 5
// spread 와 구조 분해도 작동
[...range]; // [1, 2, 3, 4, 5]
const [first, ...rest] = range; // first=1, rest=[2,3,4,5]
제너레이터 — 위 코드의 짧은 버전
function* rangeGen(from, to) {
for (let i = from; i <= to; i++) {
yield i; // 멈췄다가 다시 호출되면 여기부터
}
}
for (const n of rangeGen(1, 5)) console.log(n); // 1, 2, 3, 4, 5
// 제너레이터 함수 — function* 키워드
// 호출하면 즉시 실행 안 됨 — 제너레이터 객체(이터러블+이터레이터) 반환
const g = rangeGen(1, 3);
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }
제너레이터의 핵심. 일반 함수는 한 번 실행되면 끝까지. 제너레이터는 yield 에서 멈추고, 다음 next() 때 그 자리부터 재개. 함수 실행을 "일시정지·재개" 할 수 있는 유일한 도구.
무한 시퀀스 — 게으른 평가
function* fibo() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const f = fibo();
for (let i = 0; i < 10; i++) console.log(f.next().value);
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
// 또는 take 헬퍼
function* take(iter, n) {
let i = 0;
for (const v of iter) {
if (i++ >= n) return;
yield v;
}
}
[...take(fibo(), 5)]; // [0, 1, 1, 2, 3]
yield* — 다른 이터러블 위임
function* nums() {
yield 1;
yield 2;
}
function* letters() {
yield "a";
yield "b";
}
function* combined() {
yield* nums(); // nums 의 값을 모두 yield
yield "---";
yield* letters();
}
[...combined()]; // [1, 2, "---", "a", "b"]
제너레이터로 코루틴 — 양방향 통신
function* echo() {
const x = yield "첫 번째 응답";
console.log("받음:", x);
const y = yield "두 번째 응답";
console.log("받음:", y);
}
const g = echo();
g.next(); // { value: "첫 번째 응답", done: false }
g.next("Hi"); // "받음: Hi" → { value: "두 번째 응답", done: false }
g.next("World"); // "받음: World" → { value: undefined, done: true }
호출자가 next() 인자로 제너레이터에 값을 다시 줌. 옛 redux-saga 가 이 메커니즘으로 비동기 흐름을 구현했습니다.
async iterator · for await...of
// 비동기 스트림 — next() 가 Promise 반환
async function* fetchPages(url) {
let next = url;
while (next) {
const res = await fetch(next);
const page = await res.json();
yield page.items;
next = page.nextUrl;
}
}
// for await...of 로 소비
for await (const items of fetchPages("/api/posts?page=1")) {
for (const item of items) console.log(item);
}
// LLM 스트리밍·페이지네이션·파일 청크 등에 자연스러움
이터러블 → 배열·Set·Map
// 이터러블이면 Array.from·spread·new Set·new Map 모두 작동
function* gen() { yield 1; yield 2; yield 3; }
Array.from(gen()); // [1, 2, 3]
[...gen()]; // [1, 2, 3]
new Set(gen()); // Set(3) {1, 2, 3}
new Map([["a", 1], ["b", 2]][Symbol.iterator]()); // Map
제너레이터의 일상 용도 — 4가지
- 지연 평가 — 큰/무한 시퀀스를 메모리에 다 안 만들고 필요할 때만.
- 커스텀 이터러블 — 트리 순회·페이지네이션 등.
- async iterator — 스트림 (LLM·SSE·DB 커서).
- 코루틴 — 옛 saga·테스트 모킹. 현대 코드는 async/await 가 대부분 대체.
제너레이터의 비용. 일반 함수보다 약간 느림 (상태 보존). 한 번에 모두 배열로 쓸 수 있는 작은 데이터면 그냥 배열이 단순. 무한·매우 큰 시퀀스·중간에 멈춰야 할 때 진가.
한 줄 정리
- 이터러블 =
[Symbol.iterator]()메서드 → 이터레이터 반환. - 이터레이터 =
next()메서드 →{value, done}반환. - 제너레이터(
function*) 는 이터러블+이터레이터 한 번에. yield로 일시정지·재개.for await...of+async function*= 비동기 스트림 표준.
25편 — 디자인 패턴 5종
Singleton·Observer·Factory·Module·Strategy — JS 식 구현.
📚 쉽게 배우는 자바스크립트 교재
이전: 23편 프로토타입 · 현재: 24편 (고급) · 다음 → 25편 디자인 패턴 · 진행: 24/26
이전: 23편 프로토타입 · 현재: 24편 (고급) · 다음 → 25편 디자인 패턴 · 진행: 24/26