안동민 개발노트

본문 시작

제네레이터 함수

function*와 yield로 실행을 중단·재개하는 반복자를 만들고 위임·무한 시퀀스·비동기 제너레이터에 적용합니다.

자바스크립트와 타입스크립트에서 제너레이터 함수(Generator Functions)는 일반 함수와 달리 함수의 실행을 일시 중지했다가 다시 시작할 수 있는 특별한 종류의 함수입니다.

이 기능은 특히 비동기 프로그래밍, 데이터 스트림 처리, 무한 시퀀스 생성 등에 매우 유용하게 사용될 수 있습니다.

제너레이터 함수는 function* 문법으로 선언하며, 함수 본문 내에서 yield 키워드를 사용하여 값을 반환합니다.

yield 키워드는 함수의 실행을 잠시 멈추고 값을 호출자에게 전달하며, 다음 번에 함수가 재개될 때 yield 지점부터 다시 실행됩니다.

Generator는 caller와 frame 사이에서 제어와 값을 교환한다

GENERATOR · CONTROL

Generator는 caller와 frame 사이에서 제어와 값을 교환한다

호출은 즉시 실행하지 않고 frame을 만들며, next·throw·return이 중단 지점으로 들어가 yield·done·finally 결과를 되돌린다.

Generator는 caller와 frame 사이에서 제어와 값을 교환한다 호출은 즉시 실행하지 않고 frame을 만들며, next·throw·return이 중단 지점으로 들어가 yield·done·finally 결과를 되돌린다. create iterator next() yield value next(input) yield* delegate done · finally Caller next · throw · return Generator frame 중단된 실행 Iterable yield* 위임
  1. generator 호출은 iterator만

    generator 호출은 iterator만 만든다.

  2. next가 첫 중단

    next가 첫 중단 지점까지 실행한다.

  3. yield가 값과 제어를

    yield가 값과 제어를 caller에 돌려준다.

  4. next(input)는 직전 yield

    next(input)는 직전 yield 표현식의 값이 된다.

  5. throw와 return도 frame으로

    throw와 return도 frame으로 들어가 finally를 실행한다.

  6. yield*는 다른 iterable에

    yield*는 다른 iterable에 제어를 위임한다.

generator의 지연성은 호출자가 소비 속도와 종료 시점을 소유한다는 뜻이다.

그림은 실행을 시작한 뒤 중단된 frame의 제어 교환을 요약합니다. 첫 next의 인자는 전달할 이전 yield가 없어 사용되지 않습니다. 시작 전 return/throw는 본문에 진입하지 않으므로 본문의 finally도 실행하지 않습니다.


제너레이터 함수의 기본 동작

제너레이터 함수를 호출하면 즉시 실행되는 것이 아니라, 제너레이터 객체(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));

이 러너는 Promise 결과를 다음 next 입력으로 전달하는 성공 경로의 설명용 예입니다. 거부 콜백에서 generator.throw(err)의 반환값을 처리하지 않고, 그 호출의 예외도 바깥 reject로 연결하지 않아 실패 시 외부 Promise가 정착하지 않을 수 있습니다. 따라서 그림의 종료·정리 계약을 완성한 범용 러너가 아닙니다. async/await의 명세 동작을 이 구현과 동일시하지 않습니다.

현대 타입스크립트에서는 대부분 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): 중단 지점에 반환 완료를 전달합니다. 이미 진입한 finally가 실행되며, 그 안에서 다시 yield하면 즉시 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는 대량의 데이터를 비동기적으로 스트리밍 방식으로 처리하거나, 실시간 이벤트 스트림을 다룰 때 매우 강력한 패턴입니다.

제너레이터를 실제 코드에 적용할 때는 일반 함수보다 "호출자가 값을 당겨 가는 구조"가 문제를 더 단순하게 만드는지 확인해야 합니다.

아래 보드는 일반 제너레이터, 위임, 비동기 제너레이터를 선택할 때 확인할 기준을 한 화면에 모읍니다.

값의 개수와 시간 모델로 지연 도구를 고른다

LAZY SEQUENCE · CHOICE

값의 개수와 시간 모델로 지연 도구를 고른다

단일·복수, eager·pull, 동기·비동기 질문으로 값·배열·generator·async generator 중 가장 단순한 도구를 선택한다.

값의 개수와 시간 모델로 지연 도구를 고른다 단일·복수, eager·pull, 동기·비동기 질문으로 값·배열·generator·async generator 중 가장 단순한 도구를 선택한다. 아니오 예 생산할 값 개수와 시간 모델 복수 값을 지연 생산하는가? pull 여부 확인 값 · Promise · 배열 단순 결과 generator · async generator 지연 pull sequence
  1. 단일 즉시 값은

    단일 즉시 값은 값으로 반환한다.

  2. 단일 비동기 결과는

    단일 비동기 결과는 Promise를 쓴다.

  3. 이미 모두 있는

    이미 모두 있는 복수 값은 배열을 쓴다.

  4. 동기 pull sequence는

    동기 pull sequence는 generator를 쓴다.

  5. 비동기 pull sequence는

    비동기 pull sequence는 async generator를 쓴다.

  6. 조기 종료 시

    조기 종료 시 cleanup 소유자를 정한다.

지연 추상화의 선택 기준은 문법이 아니라 생산 시점과 취소·정리 책임이다.

이때 중요한 판단 기준은 값을 언제 만들고, 누가 다음 값을 요청하며, 중간 실패를 어디서 정리할 것인가입니다.

일반 제너레이터, 비동기 제너레이터, async/await은 모두 흐름 제어 도구이지만, 소비 방식과 타입 경계가 다르므로 목적에 맞게 선택해야 합니다.