icon안동민 개발노트

제네레이터 함수


 제네레이터 함수는 일시 중지와 재개가 가능한 특별한 종류의 함수입니다.

 이는 값의 시퀀스를 생성하고, 복잡한 비동기 흐름을 제어하는 데 사용될 수 있습니다.

제네레이터 함수의 개념과 차이점

 일반 함수와 달리, 제네레이터 함수는 호출될 때 즉시 모든 코드를 실행하지 않습니다.

 대신, 제네레이터 객체를 반환하며, 이 객체의 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());

제네레이터의 실제 활용 사례

  1. 무한 시퀀스 생성
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
// ...
  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와 주의사항

  1. 메모리 효율성 고려 : 제네레이터는 전체 시퀀스를 메모리에 유지하지 않으므로, 대량의 데이터를 다룰 때 유용합니다.
  2. 복잡한 상태 관리 주의 : 제네레이터의 상태 관리가 복잡해질 수 있으므로, 로직을 단순하게 유지하려 노력해야 합니다.
  3. 에러 처리 : 제네레이터 내부에서 발생한 에러를 적절히 처리해야 합니다.
function* errorProne(): Generator<number> {
    try {
        yield 1;
        throw new Error("Something went wrong");
    } catch (error) {
        console.error(error);
        yield -1;
    }
}
  1. 타입 안전성 유지 : 제네레이터 함수의 반환 타입을 명시적으로 선언하여 타입 안전성을 확보합니다.
  2. 무한 루프 주의 : 무한 시퀀스를 생성할 때는 소비하는 쪽에서 적절히 제어해야 합니다.
  3. 양방향 통신 활용 : next() 메서드를 통해 제네레이터에 값을 전달할 수 있음을 기억하고 활용합니다.
  4. 이터러블과의 통합 : for...of 루프나 전개 연산자와 함께 사용할 수 있음을 활용합니다.