제네레이터 함수
제네레이터 함수는 일시 중지와 재개가 가능한 특별한 종류의 함수입니다.
이는 값의 시퀀스를 생성하고, 복잡한 비동기 흐름을 제어하는 데 사용될 수 있습니다.
제네레이터 함수의 개념과 차이점
일반 함수와 달리, 제네레이터 함수는 호출될 때 즉시 모든 코드를 실행하지 않습니다.
대신, 제네레이터 객체를 반환하며, 이 객체의 next() 메서드를 호출할 때마다 다음 yield 문까지 실행됩니다.
제네레이터 함수 정의와 사용
타입스크립트에서 제네레이터 함수는 다음과 같이 정의합니다.
function* numberGenerator(): Generator<number, void, unknown> {
yield 1;
yield 2;
yield 3;
}
const generator = numberGenerator();
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3
console.log(generator.next().done); // true
yield 키워드의 동작 원리
yield 키워드는 제네레이터 함수의 실행을 일시 중지하고 지정된 값을 반환합니다.
함수의 상태는 보존되며, 다음 next() 호출 시 중단된 지점부터 실행이 재개됩니다.
function* countDown(start: number): Generator<number> {
while (start > 0) {
yield start;
start--;
}
}
const counter = countDown(3);
console.log(counter.next().value); // 3
console.log(counter.next().value); // 2
console.log(counter.next().value); // 1
제네레이터 함수의 반환 타입
제네레이터 함수의 반환 타입은 Generator<T, TReturn, TNext>
입니다.
- T : yield되는 값의 타입
- TReturn : 제네레이터 함수의 반환 값 타입
- TNext : next() 메서드에 전달될 수 있는 값의 타입
function* complexGenerator(): Generator<number, string, boolean> {
const reset = yield 1;
if (reset) {
yield 0;
}
yield 2;
return "Done";
}
const gen = complexGenerator();
console.log(gen.next().value); // 1
console.log(gen.next(true).value); // 0
console.log(gen.next().value); // 2
console.log(gen.next().value); // "Done"
제네레이터와 비동기 프로그래밍
제네레이터를 사용한 비동기 프로그래밍은 async/await 이전에 많이 사용되었습니다.
현재는 async/await가 더 간단하고 직관적이지만 복잡한 비동기 흐름 제어에는 여전히 유용할 수 있습니다.
function* fetchData(): Generator<Promise<any>, void, unknown> {
const users = yield fetch('/api/users').then(r => r.json());
const posts = yield fetch('/api/posts').then(r => r.json());
console.log(users, posts);
}
function runGenerator(gen: Generator) {
const next = (data?: any) => {
const result = gen.next(data);
if (result.done) return;
result.value.then(next);
};
next();
}
runGenerator(fetchData());
제네레이터의 실제 활용 사례
- 무한 시퀀스 생성
function* infiniteSequence(): Generator<number> {
let i = 0;
while (true) {
yield i++;
}
}
const numbers = infiniteSequence();
console.log(numbers.next().value); // 0
console.log(numbers.next().value); // 1
// ...
- 데이터 스트리밍
function* streamData(url: string): Generator<Promise<any>, void, unknown> {
let page = 1;
while (true) {
const data = yield fetch(`${url}?page=${page}`).then(r => r.json());
if (data.length === 0) break;
page++;
}
}
const dataStream = streamData('/api/data');
dataStream.next().value.then(console.log);
제네레이터, 이터러블/이터레이터 프로토콜
제네레이터 함수는 이터러블 프로토콜을 구현합니다.
이를 활용하여 커스텀 이터러블 객체를 쉽게 만들 수 있습니다.
class FibonacciSequence implements Iterable<number> {
constructor(private limit: number) {}
*[Symbol.iterator](): Generator<number> {
let prev = 0, curr = 1;
for (let i = 0; i < this.limit; i++) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
}
const fib = new FibonacciSequence(5);
for (const num of fib) {
console.log(num); // 1, 1, 2, 3, 5
}
Best Practices와 주의사항
- 메모리 효율성 고려 : 제네레이터는 전체 시퀀스를 메모리에 유지하지 않으므로, 대량의 데이터를 다룰 때 유용합니다.
- 복잡한 상태 관리 주의 : 제네레이터의 상태 관리가 복잡해질 수 있으므로, 로직을 단순하게 유지하려 노력해야 합니다.
- 에러 처리 : 제네레이터 내부에서 발생한 에러를 적절히 처리해야 합니다.
function* errorProne(): Generator<number> {
try {
yield 1;
throw new Error("Something went wrong");
} catch (error) {
console.error(error);
yield -1;
}
}
- 타입 안전성 유지 : 제네레이터 함수의 반환 타입을 명시적으로 선언하여 타입 안전성을 확보합니다.
- 무한 루프 주의 : 무한 시퀀스를 생성할 때는 소비하는 쪽에서 적절히 제어해야 합니다.
- 양방향 통신 활용 : next() 메서드를 통해 제네레이터에 값을 전달할 수 있음을 기억하고 활용합니다.
- 이터러블과의 통합 : for...of 루프나 전개 연산자와 함께 사용할 수 있음을 활용합니다.