4.5 제너레이터와 이터레이터 심화
제너레이터는 실행을 일시 중단하고 재개할 수 있는 특수한 함수입니다. 이터러블 프로토콜과 결합하면 강력한 데이터 스트리밍과 비동기 제어 흐름을 구현할 수 있습니다.
function* 문법 복습 및 심화
// 기본 제너레이터
function* basicGenerator() {
console.log("1단계");
yield 1;
console.log("2단계");
yield 2;
console.log("3단계");
yield 3;
console.log("완료");
return "done"; // 마지막 값 (value: "done", done: true)
}
const gen = basicGenerator();
console.log(gen.next()); // "1단계" → { value: 1, done: false }
console.log(gen.next()); // "2단계" → { value: 2, done: false }
console.log(gen.next()); // "3단계" → { value: 3, done: false }
console.log(gen.next()); // "완료" → { value: "done", done: true }
console.log(gen.next()); // → { value: undefined, done: true }
// 이터러블 프로토콜: for...of로 사용
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
for (const n of range(1, 10, 2)) {
process.stdout.write(`${n} `); // 1 3 5 7 9
}
console.log();
// 스프레드 연산자와도 호환
console.log([...range(0, 4)]); // [0, 1, 2, 3, 4]
yield* 위임 (Delegation)
yield*는 다른 이터러블(제너레이터 포함)에 실행을 위임합니다.
function* inner() {
yield "A";
yield "B";
yield "C";
return "inner 완료"; // yield*의 반환값으로 사용됨
}
function* outer() {
yield 1;
const result = yield* inner(); // inner의 모든 값을 yield하고 반환값 받음
console.log("inner 반환값:", result); // "inner 완료"
yield 2;
}
console.log([...outer()]); // [1, "A", "B", "C", 2]
// "inner 반환값: inner 완료"
// 트리 구조 순회
function* walkTree(node) {
yield node.value;
for (const child of (node.children ?? [])) {
yield* walkTree(child); // 재귀적 위임
}
}
const tree = {
value: "root",
children: [
{ value: "A", children: [
{ value: "A1", children: [] },
{ value: "A2", children: [] },
]},
{ value: "B", children: [
{ value: "B1", children: [] },
]},
],
};
console.log([...walkTree(tree)]);
// ["root", "A", "A1", "A2", "B", "B1"]
양방향 통신: next(value)로 값 전달
제너레이터는 next(value)로 값을 안으로 전달할 수 있습니다.
function* calculator() {
let result = 0;
while (true) {
const input = yield result; // 값을 바깥으로 내보내고, 다음 next(value)의 value를 받음
if (input === null) break;
const { op, value } = input;
switch (op) {
case "+": result += value; break;
case "-": result -= value; break;
case "*": result *= value; break;
case "/": result /= value; break;
}
}
return result;
}
const calc = calculator();
calc.next(); // 제너레이터 시작 (첫 next의 인수는 무시됨)
calc.next({ op: "+", value: 10 }); // result: 10
calc.next({ op: "*", value: 3 }); // result: 30
calc.next({ op: "-", value: 5 }); // result: 25
const final = calc.next(null); // 종료
console.log(final); // { value: 25, done: true }
대화형 데이터 검증 예제
function* formValidator() {
const name = yield "이름을 입력하세요:";
if (!name?.trim()) {
return { success: false, error: "이름은 필수입니다" };
}
const email = yield "이메일을 입력하세요:";
if (!email?.includes("@")) {
return { success: false, error: "유효한 이메일이 아닙니다" };
}
const age = yield "나이를 입력하세요:";
const ageNum = parseInt(age);
if (isNaN(ageNum) || ageNum < 0 || ageNum > 150) {
return { success: false, error: "유효한 나이가 아닙니다" };
}
return { success: true, data: { name, email, age: ageNum } };
}
// 시뮬레이션
const form = formValidator();
console.log(form.next().value); // "이름을 입력하세요:"
console.log(form.next("Alice").value); // "이메일을 입력하세요:"
console.log(form.next("alice@ex.com").value); // "나이를 입력하세요:"
const result = form.next("30");
console.log(result.value); // { success: true, data: { name: "Alice", email: "alice@ex.com", age: 30 } }
throw()와 return()으로 제너레이터 제어
function* resilientGen() {
try {
yield 1;
yield 2;
yield 3;
} catch (error) {
console.log("에러 잡음:", error.message);
yield -1; // 에러 후 복구
} finally {
console.log("제너레이터 정리");
}
}
const gen = resilientGen();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.throw(new Error("테스트 에러"))); // "에러 잡음: 테스트 에러"
// { value: -1, done: false }
console.log(gen.next()); // "제너레이터 정리"
// { value: undefined, done: true }
// return()으로 조기 종료
function* longRunning() {
try {
yield 1;
yield 2;
yield 3; // 여기까지 오지 않음
} finally {
console.log("finally 실행됨"); // return() 호출 시에도 finally는 실행
}
}
const gen2 = longRunning();
console.log(gen2.next()); // { value: 1, done: false }
console.log(gen2.return("중단")); // "finally 실행됨"
// { value: "중단", done: true }
무한 시퀀스 활용 패턴
제너레이터는 게으른 평가(lazy evaluation)로 무한 시퀀스를 표현합니다.
// 무한 자연수 생성기
function* naturals(start = 1) {
let n = start;
while (true) {
yield n++;
}
}
// 무한 피보나치
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// 무한 소수
function* primes() {
const sieve = new Set();
for (let n = 2; ; n++) {
if (!sieve.has(n)) {
yield n;
for (let i = n * n; i <= 1000000; i += n) {
sieve.add(i);
}
}
}
}
// 이터레이터 유틸리티
function take(n, iter) {
const result = [];
for (const value of iter) {
result.push(value);
if (result.length >= n) break;
}
return result;
}
function* map(fn, iter) {
for (const value of iter) {
yield fn(value);
}
}
function* filter(pred, iter) {
for (const value of iter) {
if (pred(value)) yield value;
}
}
console.log(take(10, fibonacci()));
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
console.log(take(10, primes()));
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
// 무한 시퀀스에 lazy 변환 적용
const evenSquares = filter(
x => x % 2 === 0,
map(x => x * x, naturals())
);
console.log(take(5, evenSquares)); // [4, 16, 36, 64, 100]
비동기 제너레이터 (Async Generator)
async function*로 비동기 이터러블을 만들 수 있습니다. for await...of로 소비합니다.
// 비동기 제너레이터 — 실제 API를 시뮬레이션
async function* paginate(url, pageSize = 10) {
let page = 1;
let hasMore = true;
while (hasMore) {
// 실제 환경: const response = await fetch(`${url}?page=${page}&size=${pageSize}`);
// 시뮬레이션
const items = Array.from({ length: pageSize }, (_, i) => ({
id: (page - 1) * pageSize + i + 1,
name: `Item ${(page - 1) * pageSize + i + 1}`,
}));
hasMore = page < 3; // 3페이지까지만 시뮬레이션
yield { page, items, hasMore };
page++;
}
}
async function loadAllData() {
const allItems = [];
for await (const { page, items } of paginate("/api/items")) {
console.log(`페이지 ${page}: ${items.length}개 로드`);
allItems.push(...items);
}
console.log(`총 ${allItems.length}개 아이템 로드 완료`);
}
loadAllData();
// 페이지 1: 10개 로드
// 페이지 2: 10개 로드
// 페이지 3: 10개 로드
// 총 30개 아이템 로드 완료
스트림 처리 패턴
// 대용량 텍스트 줄별 처리
async function* readLines(text) {
const lines = text.split("\n");
for (const line of lines) {
// 실제 환경에서는 파일 스트림이나 네트워크 스트림
await new Promise(r => setTimeout(r, 0)); // 비동기 시뮬레이션
yield line;
}
}
async function processLog(logText) {
const errors = [];
let lineCount = 0;
for await (const line of readLines(logText)) {
lineCount++;
if (line.includes("ERROR")) {
errors.push({ lineNumber: lineCount, message: line });
}
}
return { lineCount, errorCount: errors.length, errors };
}
const sampleLog = `INFO: 서버 시작
INFO: DB 연결 성공
ERROR: 타임아웃 발생
INFO: 재연결 시도
ERROR: 인증 실패
INFO: 서버 종료`;
processLog(sampleLog).then(({ lineCount, errorCount }) => {
console.log(`총 ${lineCount}줄, 에러 ${errorCount}개`);
// 총 6줄, 에러 2개
});
실전 예제: 테스크 스케줄러
제너레이터로 협력적 멀티태스킹 구현합니다.
// 간단한 협력적 스케줄러
function scheduler() {
const tasks = [];
let running = false;
function* runner() {
while (tasks.length > 0) {
const task = tasks.shift();
const gen = task();
let result = gen.next();
while (!result.done) {
yield; // 다른 작업에 제어권 넘김
result = gen.next();
}
}
}
return {
add(task) {
tasks.push(task);
},
run() {
if (running) return;
running = true;
const run = runner();
const step = () => {
const result = run.next();
if (!result.done) {
setTimeout(step, 0); // 비동기로 스케줄
} else {
running = false;
console.log("모든 작업 완료");
}
};
step();
},
};
}
const sched = scheduler();
sched.add(function* taskA() {
console.log("A: 1단계");
yield;
console.log("A: 2단계");
yield;
console.log("A: 완료");
});
sched.add(function* taskB() {
console.log("B: 1단계");
yield;
console.log("B: 완료");
});
sched.run();
// A: 1단계 → B: 1단계 → A: 2단계 → B: 완료 → A: 완료 → 모든 작업 완료
고수 팁
팁 1: 제너레이터로 상태 머신 구현
function* trafficLight() {
while (true) {
yield "빨간불"; // 30초
yield "초록불"; // 25초
yield "노란불"; // 5초
}
}
const light = trafficLight();
const sequence = Array.from({ length: 9 }, () => light.next().value);
console.log(sequence);
// ["빨간불", "초록불", "노란불", "빨간불", "초록불", "노란불", "빨간불", "초록불", "노란불"]
팁 2: Symbol.iterator와 제너레이터 결합
class NumberRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
// 제너레이터를 Symbol.iterator로 등록
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const range = new NumberRange(1, 5);
console.log([...range]); // [1, 2, 3, 4, 5]
console.log(Math.max(...range)); // 5
for (const n of range) {
process.stdout.write(`${n} `);
}
// 1 2 3 4 5
팁 3: 제너레이터로 지연 평가 파이프라인
// 수백만 개 데이터를 중간 배열 없이 처리
function* lazyTransform(data) {
for (const item of data) {
if (item % 2 === 0) { // filter
yield item * item; // map
}
}
}
// 100만 개 숫자에서 처음 5개만 효율적으로 추출
function* naturalsTo(n) {
for (let i = 1; i <= n; i++) yield i;
}
const result = take(5, lazyTransform(naturalsTo(1_000_000)));
console.log(result); // [4, 16, 36, 64, 100]
// 처음 10개 숫자만 처리하고 중단 — 나머지 999990개는 처리 안 됨!