본문 바로가기

제너레이터 함수

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

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

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

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

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

Generator는 caller와 frame 사이에서 제어와 값을 교환한다 — 상태 추적

next(), next(true), next(false)에 따라 실행 지점과 입력/출력/done이 바뀐다. 첫 next 인자와 시작 전 return/throw 경계를 구분한다.

교재 simpleGenerator의 next(), next(true), next(false) 요청과 1, 2, 완료 결과를 caller와 generator 두 lifeline 사이에서 교환한다.

GENERATOR / CONTROL AND VALUE

next 입력과 yield·return 결과를 실제 호출 순서로 읽는다

simpleGenerator() 호출은 iterator를 만들고 본문은 첫 next에서 시작합니다. 이후 next 인자가 직전 yield 표현식의 값이 됩니다. 최종 return 값은 done: true 결과에 담깁니다.

next 입력과 yield·return 결과를 실제 호출 순서로 읽는다교재 simpleGenerator의 next(), next(true), next(false) 요청과 1, 2, 완료 결과를 caller와 generator 두 lifeline 사이에서 교환한다.Callernext 입력Generator중단·재개next() → 본문 시작{ value: 1, done: false }next(true) → 첫 yield에 true{ value: 2, done: false }next(false) → 둘째 yield에 false{ value: 완료 문자열, done: true }
next 입력과 yield 결과를 시간 순서로 교환한다호출자와 제너레이터의 두 lifeline에서 next(), 값1·done false, next(true), 값2·done false, next(false), 최종 문자열 모든 작업 완료!·done true를 교환한다. 첫 yield 입력은 true, 둘째 입력은 false다. 반환 메시지는 점선이며 시간은 위에서 아래로 흐른다.호출자Caller제너레이터Generatornext()본문 시작value: 1done: falsenext(true)첫 yield에true 입력value: 2done: falsenext(false)둘째 yield에false 입력모든 작업완료!done: true

START

첫 next 입력은 사용하지 않는다

첫 요청 앞에는 값을 받을 yield가 없습니다. 교재의 첫 next()는 실행을 시작해 1을 반환합니다.

TYPE CHANNELS

입력·중간 값·최종 값은 별개

Generator<number, string, boolean>은 yield 숫자, return 문자열, next 입력 boolean을 구분합니다. 이후 완료된 next는 undefined와 done: true를 돌려줍니다.

THROW / RETURN

중단 상태에 따라 종료가 다르다

진입한 try/finally는 throw·return 처리 중 실행할 수 있습니다. 시작 전 return/throw는 본문에 진입하지 않습니다. finally가 yield하면 return도 즉시 done: true가 아닐 수 있습니다.

YIELD STAR

위임은 별도의 반복자와 교환

yield*는 다른 iterable의 값을 전달합니다. 교재의 배열·anotherGenerator 위임은 simpleGenerator의 위 시퀀스와 별도 예제입니다.

LESSON LIMIT

간단한 비동기 runner의 한계

원문의 Promise 실패 콜백은 generator.throw의 결과·예외를 외부 Promise에 연결하지 않습니다. 이 그림은 그 runner가 실패·정리를 완성했다고 보장하지 않습니다.

next·throw·return은 호출자의 제어 요청입니다. 지연 실행만으로 외부 자원 정리가 구현되지는 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
전체 관계·조건 설명호출자와 제너레이터의 두 lifeline에서 next(), 값 1·done false, next(true), 값 2·done false, next(false), 최종 문자열 모든 작업 완료!·done true를 교환한다. 첫 yield 입력은 true, 둘째 입력은 false다. 반환 메시지는 점선이며 시간은 위에서 아래로 흐른다.
원도표 설명Generator<number, string, boolean>은 yield 숫자, return 문자열, next 입력 boolean을 구분합니다. 이후 완료된 next는 undefined와 done: true를 돌려줍니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
첫 next()value: 1 / done: falsenext()가 yield 1까지 실행된다첫 next의 인자는 첫 yield의 입력으로 쓰이지 않는다
next(true)입력: true / 결과: value:2, done:falsetrue를 이전 yield 표현식에 넣고 yield 2까지 재개한다next의 입력과 yield의 출력은 방향이 다르다
next(false)value: 모든 작업 완료! / done: truefalse로 재개한 뒤 모든 작업 완료!를 반환한다return 결과는 일반 yield 항목으로 소비되지 않는다
완료 뒤 next()value: undefined / done: true끝난 iterator는 value undefined와 done:true를 반환한다이미 완료된 본문은 다시 실행되지 않는다

Generator는 caller와 frame 사이에서 제어와 값을 교환한다 — 상태 추적

next(), next(true), next(false)에 따라 실행 지점과 입력/출력/done이 바뀐다. 첫 next 인자와 시작 전 return/throw 경계를 구분한다.

교재 simpleGenerator의 next(), next(true), next(false) 요청과 1, 2, 완료 결과를 caller와 generator 두 lifeline 사이에서 교환한다.

GENERATOR / CONTROL AND VALUE

next 입력과 yield·return 결과를 실제 호출 순서로 읽는다

simpleGenerator() 호출은 iterator를 만들고 본문은 첫 next에서 시작합니다. 이후 next 인자가 직전 yield 표현식의 값이 됩니다. 최종 return 값은 done: true 결과에 담깁니다.

next 입력과 yield·return 결과를 실제 호출 순서로 읽는다교재 simpleGenerator의 next(), next(true), next(false) 요청과 1, 2, 완료 결과를 caller와 generator 두 lifeline 사이에서 교환한다.Callernext 입력Generator중단·재개next() → 본문 시작{ value: 1, done: false }next(true) → 첫 yield에 true{ value: 2, done: false }next(false) → 둘째 yield에 false{ value: 완료 문자열, done: true }
next 입력과 yield 결과를 시간 순서로 교환한다호출자와 제너레이터의 두 lifeline에서 next(), 값1·done false, next(true), 값2·done false, next(false), 최종 문자열 모든 작업 완료!·done true를 교환한다. 첫 yield 입력은 true, 둘째 입력은 false다. 반환 메시지는 점선이며 시간은 위에서 아래로 흐른다.호출자Caller제너레이터Generatornext()본문 시작value: 1done: falsenext(true)첫 yield에true 입력value: 2done: falsenext(false)둘째 yield에false 입력모든 작업완료!done: true

START

첫 next 입력은 사용하지 않는다

첫 요청 앞에는 값을 받을 yield가 없습니다. 교재의 첫 next()는 실행을 시작해 1을 반환합니다.

TYPE CHANNELS

입력·중간 값·최종 값은 별개

Generator<number, string, boolean>은 yield 숫자, return 문자열, next 입력 boolean을 구분합니다. 이후 완료된 next는 undefined와 done: true를 돌려줍니다.

THROW / RETURN

중단 상태에 따라 종료가 다르다

진입한 try/finally는 throw·return 처리 중 실행할 수 있습니다. 시작 전 return/throw는 본문에 진입하지 않습니다. finally가 yield하면 return도 즉시 done: true가 아닐 수 있습니다.

YIELD STAR

위임은 별도의 반복자와 교환

yield*는 다른 iterable의 값을 전달합니다. 교재의 배열·anotherGenerator 위임은 simpleGenerator의 위 시퀀스와 별도 예제입니다.

LESSON LIMIT

간단한 비동기 runner의 한계

원문의 Promise 실패 콜백은 generator.throw의 결과·예외를 외부 Promise에 연결하지 않습니다. 이 그림은 그 runner가 실패·정리를 완성했다고 보장하지 않습니다.

next·throw·return은 호출자의 제어 요청입니다. 지연 실행만으로 외부 자원 정리가 구현되지는 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
전체 관계·조건 설명호출자와 제너레이터의 두 lifeline에서 next(), 값 1·done false, next(true), 값 2·done false, next(false), 최종 문자열 모든 작업 완료!·done true를 교환한다. 첫 yield 입력은 true, 둘째 입력은 false다. 반환 메시지는 점선이며 시간은 위에서 아래로 흐른다.
원도표 설명Generator<number, string, boolean>은 yield 숫자, return 문자열, next 입력 boolean을 구분합니다. 이후 완료된 next는 undefined와 done: true를 돌려줍니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
첫 next()value: 1 / done: falsenext()가 yield 1까지 실행된다첫 next의 인자는 첫 yield의 입력으로 쓰이지 않는다
next(true)입력: true / 결과: value:2, done:falsetrue를 이전 yield 표현식에 넣고 yield 2까지 재개한다next의 입력과 yield의 출력은 방향이 다르다
next(false)value: 모든 작업 완료! / done: truefalse로 재개한 뒤 모든 작업 완료!를 반환한다return 결과는 일반 yield 항목으로 소비되지 않는다
완료 뒤 next()value: undefined / done: true끝난 iterator는 value undefined와 done:true를 반환한다이미 완료된 본문은 다시 실행되지 않는다

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


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

반복자 생성과 실제 작업 시작은 다른 시점입니다

simpleGenerator()를 호출해 객체만 만들면 본문의 시작 로그도 아직 나오지 않습니다. 호출자가 첫 next를 요청해야 첫 yield까지 실행합니다. 같은 객체를 두 번 순회하면 같은 중단 상태를 이어서 소비하며, 독립적인 처음부터의 순회가 필요하면 함수로 새 객체를 만듭니다. 반면 MyCollection은 Symbol.iterator를 호출할 때마다 새 제너레이터를 돌려주는 예제입니다.

일반 제너레이터의 next는 다음 yield까지 동기 실행합니다. yield 앞에서 오래 계산하면 그 next 호출도 오래 걸리며, 제너레이터라는 이름만으로 CPU 작업이 비동기가 되지는 않습니다. asyncFlow는 일반 제너레이터가 내보낸 Promise를 러너가 기다리는 구성이고, async function* 자체가 Promise 형태의 반복 결과를 주는 비동기 제너레이터와 구분합니다.

제너레이터 함수를 호출하면 즉시 실행되는 것이 아니라, 제너레이터 객체(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 1에서 바깥으로 나가는 값 1과 그 표현식이 재개될 때 안으로 들어오는 true는 서로 다른 값입니다. 따라서 next(true)의 반환값이 true가 되는 것이 아닙니다. true를 받은 본문이 다음 yield 2까지 실행되어 { value: 2, done: false }를 돌려줍니다. return의 문자열은 최종 결과이며 for...of가 소비하는 다음 항목에는 포함되지 않습니다.


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 = nextValue === undefined ? generator.next() : 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의 명세 동작을 이 구현과 동일시하지 않습니다.

실패 뒤에도 진행할 수 있는 별도 실행기

아래 코드는 원래 실행기를 보존한 채 다른 이름으로 비교하는 실습입니다. 실행 중인 제너레이터가 문자열 Promise만 yield하고 마지막에 문자열을 반환한다는 범위에 한정합니다. next와 throw가 돌려주는 결과를 같은 처리기로 보내고, 처리하지 못한 예외는 바깥 Promise의 거부로 연결합니다. 작업 취소나 병렬 실행까지 구현한 라이브러리는 아닙니다.

function runSafely(
  iterator: Generator<Promise<string>, string, string>
): Promise<string> {
  return new Promise((resolve, reject) => {
    function move(action: () => IteratorResult<Promise<string>, string>): void {
      try {
        advance(action());
      } catch (error) {
        reject(error);
      }
    }

    function advance(result: IteratorResult<Promise<string>, string>): void {
      if (result.done) {
        resolve(result.value);
        return;
      }
      Promise.resolve(result.value).then(
        value => move(() => iterator.next(value)),
        error => move(() => iterator.throw(error))
      );
    }

    move(() => iterator.next());
  });
}

function* recoverFlow(): Generator<Promise<string>, string, string> {
  try {
    yield Promise.reject(new Error("첫 요청 실패"));
  } catch (error) {
    console.log(error instanceof Error ? error.message : String(error));
    const fallback = yield Promise.resolve("대체 값");
    return `완료: ${fallback}`;
  }
  return "기본 완료";
}

runSafely(recoverFlow()).then(result => console.log(result));

처음 거부는 중단된 yield 지점으로 전달됩니다. 본문의 catch가 받아 첫 요청 실패를 출력하고 다시 Promise를 yield합니다. 이때 throw의 반환값도 done: false인 새 반복 결과이므로 처리해야 합니다. 대체 Promise의 성공 값이 다음 next의 입력으로 들어가 최종 완료: 대체 값이 출력됩니다. catch가 없거나 catch가 다시 던지면 move가 그 예외를 받아 바깥 Promise를 거부합니다.

현대 타입스크립트에서는 대부분 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는 대량의 데이터를 비동기적으로 스트리밍 방식으로 처리하거나, 실시간 이벤트 스트림을 다룰 때 매우 강력한 패턴입니다.

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

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

즉시 값, Promise, 배열, generator, async generator와 정리 책임을 같은 6카드로 비교한다.

LAZY SEQUENCE / CHOICE

값의 개수·생산 시점·요청 방식을 비교한다

단일 결과와 여러 값, 이미 계산된 값과 요청할 때 생산하는 값을 구분합니다. generator와 async generator는 동기·비동기 pull 반복을 간단히 구현하는 도구입니다.

SINGLE SYNC

즉시 값

단일 결과가 이미 있거나 동기 계산이면 값을 반환합니다. 반복 프로토콜은 필요하지 않을 수 있습니다.

SINGLE ASYNC

Promise

한 번 정착하는 비동기 결과를 표현합니다. 여러 항목을 차례로 요청하는 프로토콜과는 다릅니다.

READY SEQUENCE

배열·기존 iterable

이미 모인 값을 순회할 때 배열을 사용할 수 있습니다. 모든 iterable이 eager인 것은 아니며 generator도 iterable입니다.

SYNC PULL

일반 generator

next 요청 때 실행해 다음 yield까지 생산합니다. 교재 ID 생성기는 필요한 만큼만 값을 만들며 MyCollection은 기존 배열을 순회합니다.

ASYNC PULL

async generator

next 결과를 Promise로 받습니다. 교재 사용자 스트림처럼 항목마다 비동기 작업을 기다리고 for await로 소비합니다.

OWNERSHIP

중간 종료·실패의 정리

return과 finally가 어떤 자원을 정리하는지 구현합니다. 소비 속도가 pull이어도 내부 prefetch·buffer·외부 생산 속도는 별도 설계입니다.

도구 이름보다 값의 의미와 생산·소비·정리 책임을 먼저 결정합니다.

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

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

제어권을 바꾸어 확인하기

기본 예제와 별개의 새 simpleGenerator() 객체 g를 만들고 g.next(), g.next(false), g.next(true), g.next()를 순서대로 호출하면 네 결과와 두 입력 로그는 무엇인가요? 이어 recoverFlow의 catch 안에서 대체 Promise를 yield하는 줄부터 반환문까지를 throw error;로 바꾸었을 때 runSafely(...).then(...).catch(...) 중 어느 핸들러가 실행되는지 설명해 보세요.

재개 입력과 실패 연결 확인

결과는 순서대로 값 1·미완료, 값 2·미완료, 문자열 모든 작업 완료!·완료, undefined·완료입니다. 첫 yield 표현식에 false, 둘째 yield 표현식에 true가 전달되므로 본문의 두 입력 로그도 false·true 순서입니다. 마지막 완료 결과는 추가 항목이 아닙니다.

변경한 catch는 최초의 Error를 다시 던집니다. 그 오류가 iterator.throw 호출 밖으로 나오고 move가 바깥 Promise를 거부하므로 catch 핸들러가 실행됩니다. 원래 단순 실행기처럼 거부 콜백 안에서 throw 호출만 하고 결과나 예외를 놓치지 않는 것이 핵심입니다.

연습: return 요청과 정리의 완료는 같은가요?

이 예제는 finally 안에서도 yield할 수 있음을 관찰하기 위한 코드입니다. 실제 자원 정리를 완료하는 구현이나 작업 취소 라이브러리는 아닙니다. 질문마다 새 객체를 사용하고, 다른 호출자는 없으며 console.log는 예외를 던지지 않는다고 가정합니다.

return 뒤에도 중단될 수 있는 제너레이터
function* cleanupTrace(): Generator<number, string, void> {
  try {
    yield 1;
    yield 2;
  } finally {
    console.log("정리 시작");
    yield 99;
    console.log("정리 끝");
  }
  return "자연 완료";
}
  1. 새 g에 g.next(), g.return("중단"), g.next(), g.next()를 순서대로 호출합니다. 네 value/done 결과와 정리 로그가 출력되는 호출을 각각 적으세요.
  2. 아직 next를 호출하지 않은 새 h에 h.return("시작 전 종료")를 먼저 호출합니다. 본문의 finally 로그가 나올까요? 이후 h.next의 결과도 적으세요.
  3. 새 k를 for (const value of k) { console.log("항목", value); break; }로 소비합니다. 99도 루프 본문의 항목으로 출력될까요? 루프가 끝났다는 사실만으로 정리 끝 로그까지 실행됐다고 판단할 수 있을까요? 그 뒤 k.next를 한 번 더 호출하는 경우와 구분하세요.
반환 요청·중단 상태·정리 로그 풀이
  1. 결과는 { value: 1, done: false }, { value: 99, done: false }, { value: "중단", done: true }, { value: undefined, done: true }입니다. return 요청은 이미 진입한 try의 finally로 이동하므로 두 번째 호출에서 정리 시작을 찍고 99를 yield합니다. 세 번째 next가 finally를 재개해 정리 끝을 출력한 뒤 보류한 반환 요청을 완료합니다. 그래서 자연 완료 문자열이나 두 번째 본문 항목 2로 바뀌지 않습니다.
  2. 첫 결과는 { value: "시작 전 종료", done: true }이고 finally 로그는 없습니다. 본문과 try에 진입하기 전에 종료됐기 때문입니다. 이후 next는 { value: undefined, done: true }입니다. 객체를 만들었다는 사실이 본문의 자원을 획득하거나 정리를 시작했다는 뜻은 아닙니다.
  3. 루프 본문은 항목 1만 출력합니다. break에 따른 반복자 닫기는 return 요청을 보내지만, finally의 yield99를 다음 루프 항목으로 소비하거나 남은 정리 코드를 자동으로 반복해서 실행하지 않습니다. 이때 정리 시작은 나오고 정리 끝은 아직 나오지 않습니다. k.next를 추가로 호출해야 이 예제의 정리 끝까지 재개됩니다. 실제 정리가 반드시 완료돼야 한다면 finally 안에서 중단하지 않는 구현이나, 남은 중단을 처리하는 명확한 호출자 계약을 설계해야 합니다.

이 동작은 Generator.return의 try/finally 설명과 ECMAScript의 제너레이터 제어 규칙에서 확인할 수 있습니다. return을 호출했다는 기록, done:true 결과, 실제 정리 코드 완료를 따로 확인하세요.