제네레이터 함수
자바스크립트와 타입스크립트에서 제너레이터 함수(Generator Functions) 는 일반 함수와 달리 함수의 실행을 일시 중지했다가 다시 시작할 수 있는 특별한 종류의 함수입니다. 이 기능은 특히 비동기 프로그래밍, 데이터 스트림 처리, 무한 시퀀스 생성 등에 매우 유용하게 사용될 수 있습니다.
제너레이터 함수는 function*
문법으로 선언하며, 함수 본문 내에서 yield
키워드를 사용하여 값을 반환합니다. yield
키워드는 함수의 실행을 잠시 멈추고 값을 호출자에게 전달하며, 다음 번에 함수가 재개될 때 yield
지점부터 다시 실행됩니다.
제너레이터 함수의 기본 동작
제너레이터 함수를 호출하면 즉시 실행되는 것이 아니라, 제너레이터 객체(Generator Object) 를 반환합니다. 이 제너레이터 객체는 이터러블(Iterable)이자 이터레이터(Iterator)입니다. next()
메서드를 호출하여 제너레이터 함수의 실행을 제어하고, yield
키워드가 반환하는 값을 받을 수 있습니다.
기본 구조 예시
function* simpleGenerator(): Generator<number, string, boolean> {
console.log("제너레이터 실행 시작");
let inputFromNext: boolean = yield 1; // 첫 번째 yield
console.log("첫 번째 yield 후 재개됨. 입력값:", inputFromNext);
inputFromNext = yield 2; // 두 번째 yield
console.log("두 번째 yield 후 재개됨. 입력값:", inputFromNext);
return "모든 작업 완료!"; // 최종 반환 값
}
// 제너레이터 함수 호출 -> 제너레이터 객체 반환
const generator = simpleGenerator();
console.log("1. next() 호출 (첫 번째 yield 전)");
let result1 = generator.next(); // 제너레이터 실행 시작 -> yield 1 -> 일시 중지
console.log("result1:", result1); // { value: 1, done: false }
console.log("\n2. next(true) 호출 (첫 번째 yield 후 재개)");
let result2 = generator.next(true); // inputFromNext에 true가 할당됨 -> yield 2 -> 일시 중지
console.log("result2:", result2); // { value: 2, done: false }
console.log("\n3. next(false) 호출 (두 번째 yield 후 재개)");
let result3 = generator.next(false); // inputFromNext에 false가 할당됨 -> return "모든 작업 완료!"
console.log("result3:", result3); // { value: "모든 작업 완료!", done: true }
console.log("\n4. 다음 next() 호출 (이미 완료됨)");
let result4 = generator.next(); // 이미 done: true 이므로 value: undefined
console.log("result4:", result4); // { value: undefined, done: true }
Generator<YieldType, ReturnType, NextType>
타입 파라미터
타입스크립트에서 제너레이터 함수의 반환 타입인 Generator
는 세 가지 제네릭 타입 파라미터를 가집니다.
YieldType
(number
):yield
키워드가 반환하는 값의 타입입니다.ReturnType
(string
): 제너레이터 함수가 최종적으로return
하는 값의 타입입니다.NextType
(boolean
):next()
메서드의 인자로 전달될 값의 타입입니다. 이는yield
표현식의 결과 타입이 됩니다.
yield*
yield*
표현식은 다른 이터러블(다른 제너레이터, 배열, 문자열 등)로 제어권을 위임할 때 사용됩니다. 이는 여러 제너레이터를 조합하여 더 복잡한 시퀀스를 만들 때 유용합니다.
function* yieldStarExample(): Generator<number> {
yield 1;
yield* [2, 3, 4]; // 배열의 요소들을 하나씩 yield
yield* anotherGenerator(); // 다른 제너레이터로 위임
yield 7;
}
function* anotherGenerator(): Generator<number> {
yield 5;
yield 6;
}
const delegatedGenerator = yieldStarExample();
for (const value of delegatedGenerator) {
console.log(value); // 1, 2, 3, 4, 5, 6, 7 순서로 출력
}
제너레이터 함수의 활용
제너레이터 함수는 다양한 상황에서 활용될 수 있습니다.
무한 시퀀스 생성
제너레이터는 done: false
상태를 계속 유지하며 무한한 시퀀스를 생성할 수 있습니다.
function* idGenerator(): Generator<number> {
let id = 0;
while (true) {
yield id++;
}
}
const ids = idGenerator();
console.log(ids.next().value); // 0
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
// 필요한 만큼 무한히 ID를 생성할 수 있습니다.
비동기 작업 관리
과거 async/await
이 도입되기 전에는 제너레이터를 사용하여 비동기 코드를 동기 코드처럼 보이게 하는 패턴(co-routine 또는 thunk/saga)이 널리 사용되었습니다. 예를 들어, redux-saga
같은 라이브러리가 제너레이터를 기반으로 비동기 로직을 처리합니다.
// Promise를 반환하는 더미 비동기 함수
function asyncOperation(value: number): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`비동기 작업 완료: ${value}`);
resolve(`결과_${value}`);
}, 1000);
});
}
// 제너레이터를 이용한 비동기 흐름 제어 (async/await과 유사한 역할)
function* asyncFlow(): Generator<Promise<string>, string, string> {
console.log("Async 흐름 시작");
const result1 = yield asyncOperation(1);
console.log("첫 번째 비동기 작업 결과:", result1);
const result2 = yield asyncOperation(2);
console.log("두 번째 비동기 작업 결과:", result2);
return "모든 비동기 작업 완료!";
}
// 제너레이터를 실행하는 러너 함수 (간단한 구현)
function runGenerator(generatorFunc: Generator<Promise<string>, string, string>): Promise<string> {
const generator = generatorFunc;
return new Promise((resolve, reject) => {
function step(nextValue?: string) {
let result;
try {
result = generator.next(nextValue);
} catch (e) {
return reject(e);
}
if (result.done) {
return resolve(result.value);
}
// yield 된 Promise가 완료될 때까지 기다리고, 그 결과로 다음 next()를 호출
Promise.resolve(result.value).then(
(res) => {
step(res);
},
(err) => {
generator.throw(err); // Promise 실패 시 제너레이터에 오류 던지기
}
);
}
step(); // 첫 번째 실행
});
}
// 비동기 흐름 실행
runGenerator(asyncFlow())
.then((finalResult) => console.log("최종 결과:", finalResult))
.catch((error) => console.error("오류 발생:", error));
이 예시는 async/await
이 어떻게 Promise와 제너레이터를 기반으로 작동하는지 이해하는 데 도움이 됩니다. async/await
은 사실상 이러한 러너 함수(co-routine runner)를 언어 레벨에서 제공하는 문법적 설탕(Syntactic Sugar)입니다. 현대 타입스크립트에서는 대부분 async/await
을 선호하지만, 특정 상황(예: 복잡한 상태 관리 라이브러리)에서는 여전히 제너레이터가 유용하게 사용될 수 있습니다.
이터러블 구현
제너레이터는 사용자 정의 이터러블 객체를 쉽게 구현하는 데 사용될 수 있습니다.
class MyCollection<T> {
private items: T[];
constructor(items: T[]) {
this.items = items;
}
// Symbol.iterator 메서드를 제너레이터 함수로 구현
*[Symbol.iterator](): Generator<T> {
for (const item of this.items) {
yield item;
}
}
}
const myNums = new MyCollection([10, 20, 30]);
for (const num of myNums) {
console.log(num); // 10, 20, 30 순서로 출력
}
제너레이터 메서드
제너레이터 객체는 next()
외에 두 가지 추가 메서드를 가집니다.
return(value?: any)
: 제너레이터의 실행을 강제로 종료하고, 주어진value
를value
속성으로,done: true
를 포함하는 객체를 반환합니다.throw(exception: any)
: 제너레이터 내부로 오류를 던집니다. 이는 제너레이터 함수 내의try...catch
블록에서 잡을 수 있습니다.
function* errorHandlingGenerator(): Generator<number> {
try {
yield 1;
yield 2;
console.log("이 부분은 실행되지 않음");
yield 3;
} catch (e: any) {
console.error("제너레이터 내부에서 오류 포착:", e.message);
} finally {
console.log("제너레이터 정리 작업 (finally)");
}
return 4; // return 호출 시 이 값은 무시될 수 있음
}
const errGen = errorHandlingGenerator();
console.log(errGen.next()); // { value: 1, done: false }
console.log(errGen.throw(new Error("외부에서 오류 주입!"))); // { value: 4, done: true } (finally 후 return 값)
console.log(errGen.next()); // { value: undefined, done: true } (이미 종료됨)
async 제너레이터
타입스크립트 2.3부터는 async function*
문법을 사용하여 비동기 제너레이터를 만들 수 있습니다. 이는 async/await
과 제너레이터의 조합으로, 비동기 데이터 스트림을 처리하는 데 매우 유용합니다. async
제너레이터는 AsyncIterable
을 반환하며, for await...of
루프를 사용하여 비동기적으로 yield
된 값을 순회할 수 있습니다.
async function fetchUserStream(): Promise<AsyncGenerator<string>> {
async function* userStreamGenerator(): AsyncGenerator<string> {
const users = ["Alice", "Bob", "Charlie"];
for (let i = 0; i < users.length; i++) {
console.log(`[Async Gen] Fetching user ${i + 1}...`);
await new Promise(resolve => setTimeout(resolve, 500)); // 비동기 작업 시뮬레이션
yield users[i];
}
console.log("[Async Gen] User stream finished.");
}
return userStreamGenerator();
}
async function processUserStream(): Promise<void> {
console.log("Processing user stream...");
const users = await fetchUserStream();
for await (const user of users) { // for await...of 사용
console.log(`Processed user: ${user}`);
// 여기서 각 사용자에 대한 비동기 처리도 가능
await new Promise(resolve => setTimeout(resolve, 200));
}
console.log("Finished processing all users.");
}
processUserStream();
async function*
와 for await...of
는 대량의 데이터를 비동기적으로 스트리밍 방식으로 처리하거나, 실시간 이벤트 스트림을 다룰 때 매우 강력한 패턴입니다.
제너레이터 함수는 함수의 실행 흐름을 제어하고, 이터러블 프로토콜을 구현하며, 비동기 작업을 관리하는 데 유연성을 제공하는 강력한 기능입니다. async/await
이 비동기 프로그래밍의 주류가 되었지만, 제너레이터는 여전히 무한 시퀀스, 커스텀 이터레이터, 그리고 특정 비동기 라이브러리(예: Redux-Saga)의 기반으로 중요한 역할을 합니다. 타입스크립트는 이러한 제너레이터에 대한 강력한 타입 지원을 제공하여 더욱 안전하고 예측 가능한 코드를 작성할 수 있게 합니다.