본문으로 건너뛰기

안동민 개발노트

본문 시작

제네레이터 함수

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

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

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

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

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

TypeScript 제너레이터 실행 모델

제너레이터 함수는 호출 즉시 실행되지 않고 next() 호출 사이에서 실행과 중단을 반복합니다.

  1. 생성
    function* 호출

    함수 본문을 실행하지 않은 채 제너레이터 객체를 반환합니다.

  2. 진행
    next()

    다음 yield까지 코드를 실행하고 value와 done을 돌려줍니다.

  3. 중단
    yield

    값을 내보내고 현재 실행 위치와 지역 상태를 보존합니다.

  4. 재개
    next(input)

    전달한 입력을 이전 yield 표현식의 결과로 받아 실행을 이어갑니다.

  5. 종료
    return

    최종 반환값과 done: true를 돌려주며 반복을 끝냅니다.


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

제너레이터 실행 흐름

제너레이터는 한 번에 끝나는 함수가 아니라 next 호출마다 yield 지점까지 실행하고 상태를 보존하는 이터레이터입니다.

  1. 1
    이터레이터 생성

    제너레이터 함수를 호출하면 즉시 실행되지 않고 iterator 객체가 만들어집니다.

  2. 2
    next 호출

    next가 호출될 때 함수 본문이 다음 yield 지점까지 진행됩니다.

  3. 3
    상태 보존

    yield 이후 지역 변수와 실행 위치가 보존되어 다음 호출에서 이어집니다.

  4. 4
    완료 처리

    return에 도달하면 done이 true가 되고 이후 반복이 종료됩니다.

제너레이터 함수를 호출하면 즉시 실행되는 것이 아니라, 제너레이터 객체(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): 제너레이터의 실행을 강제로 종료하고, 주어진 valuevalue 속성으로, 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는 대량의 데이터를 비동기적으로 스트리밍 방식으로 처리하거나, 실시간 이벤트 스트림을 다룰 때 매우 강력한 패턴입니다.

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

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

제너레이터 적용 선택 기준

값의 도착 방식과 소비 속도를 먼저 구분하면 일반 함수, 동기 제너레이터, 비동기 제너레이터 중 적절한 도구를 고를 수 있습니다.

  1. 즉시
    일반 함수

    결과가 작고 한 번에 계산해도 부담이 없으면 값 또는 배열을 바로 반환합니다.

  2. 필요할 때
    function*

    동기 값이 많거나 무한해 소비자의 next() 속도에 맞춰 하나씩 만듭니다.

  3. 늦게 도착
    async function*

    네트워크·파일처럼 비동기 값이 순서대로 도착하고 for await로 소비할 때 사용합니다.

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

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

제너레이터는 멈출 수 있는 생산자와 당겨오는 소비자를 분리한다

function*와 async function*는 값을 한꺼번에 만들지 않고 next 또는 for await가 요청할 때 다음 지점까지 실행한다.

  1. yield 지점에서 멈춤

    생산자 yield 지점에서 멈춤 지역 변수와 실행 위치가 보존된다.

  2. next 또는 for...of

    소비자 next 또는 for...of 다음 값을 언제 받을지 결정한다.

  3. Yield, Return, Next 분리

    타입 Yield, Return, Next 분리 전달 값과 최종 결과를 섞지 않는다.

구분의미판단
일반 제너레이터동기 순차 값무한 ID, 페이지 커서, custom iterable
비동기 제너레이터늦게 도착하는 값네트워크, 파일, 이벤트 스트림
async/await최종 결과 하나중간 값을 노출하지 않아도 되는 작업

제너레이터 함수는 함수 실행 흐름 제어, 이터러블 프로토콜 구현, 비동기 작업 관리에 유연성을 제공하는 강력한 기능입니다.

async/await이 비동기 프로그래밍의 주류가 되었지만, 제너레이터는 무한 시퀀스, 커스텀 이터레이터, 특정 비동기 라이브러리(예: Redux-Saga)의 기반으로 여전히 중요한 역할을 합니다.

타입스크립트는 제너레이터에 대한 강력한 타입 지원을 제공해 더 안전하고 예측 가능한 코드를 작성하도록 돕습니다.

제너레이터의 제어면은 아래처럼 yield, next, return, throw를 분리하면 더 쉽게 읽을 수 있습니다.

제너레이터 제어면 정리

yield 값, next 입력, return 종료, throw 오류 주입을 분리해서 보면 제너레이터 타입 파라미터의 의미도 선명해집니다.

  1. yield
    값을 내보내고 멈춤

    함수 실행을 잠시 중지하고 호출자에게 현재 값을 전달합니다.

  2. next(input)
    다시 시작

    다음 호출에서 이전 yield 표현식의 결과로 입력값이 들어옵니다.

  3. return
    정상 종료

    최종 반환값과 done 상태로 스트림이 끝났음을 알립니다.

  4. throw
    오류 주입

    제너레이터 내부 try/catch/finally로 정리 흐름을 테스트할 수 있습니다.

아래 다이어그램은 Generator<Yield, Return, Next> 타입 매개변수를 호출 방향별로 분리해 보여줍니다.

Generator 타입 매개변수

`yield`, `return`, `next(value)`는 모두 흐름 제어지만 값이 오가는 방향과 타입 의미가 다릅니다.

  1. 1
    밖으로 내보내는 값

    Yield 반복 소비자가 `next()`로 받을 중간 결과의 타입입니다. IteratorResult<Y>

  2. 2
    끝날 때 확정값

    Return 제너레이터가 완료될 때 `done: true`와 함께 전달되는 값입니다.

  3. 3
    다시 넣는 입력

    Next 소비자가 `next(value)`로 제너레이터 내부에 되돌려 보내는 타입입니다.

  4. 4
    return과 throw

    Control 흐름을 조기 종료하거나 실패를 주입할 때 정리 로직을 함께 설계합니다. Generator<Y,R,N>

아래 다이어그램은 제너레이터 함수의 기본 동작과 yield가 책임 경계와 실행 흐름을 어떻게 바꾸는지 정리합니다.

Generator의 세 타입 축

Generator<TYield, TReturn, TNext>는 밖으로 내보내는 값, 종료 값, 재개할 때 안으로 받는 값을 서로 분리합니다.

  1. 밖으로
    TYield

    yield value가 done: false인 IteratorResult의 value로 소비자에게 전달됩니다.

  2. 종료
    TReturn

    return value가 done: true인 마지막 IteratorResult의 value가 됩니다.

  3. 안으로
    TNext

    next(input)의 input이 중단된 yield 표현식의 결과로 제너레이터 안에 들어갑니다.

제너레이터 함수를 실제 코드에 적용할 때는 yield로 내보내는 값, next()로 다시 들어오는 입력, return 종료 값, throw 정리 경로를 분리해서 봅니다.

이 네 방향이 타입으로 드러나면 흐름 제어와 테스트 신호가 한눈에 정리됩니다.