[JavaScript] 비동기 이터레이터와 비동기 제너레이터

비동기 이터레이터와 for‑await‑of

비동기 이터레이터를 구현하려면, 객체에 Symbol.asyncIterator 메서드를 추가해야 합니다. 이 메서드는 일반 이터레이터와 유사하지만, 각 next() 호출에서 반환하는 값이 프라미스로 감싸져 있어야 합니다.

const asyncRange = {
  from: 1,
  to: 5,
  [Symbol.asyncIterator]() {
    let current = this.from;
    const last = this.to;
    return {
      async next() {
        // 1초 대기 (비동기 작업 시뮬레이션)
        await new Promise(resolve => setTimeout(resolve, 1000));
        if (current <= last) {
          return { done: false, value: current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

(async () => {
  console.log("비동기 이터레이터 시작:");
  for await (const num of asyncRange) {
    console.log(num); // 1, 2, 3, 4, 5 (각 값 사이에 1초씩 지연)
  }
  console.log("비동기 이터레이션 완료.");
})();
  • Symbol.asyncIterator를 구현하여, 이터레이터 객체를 반환합니다.
  • 각 next() 호출 시 1초를 기다린 후 숫자를 반환하고, 반복이 끝나면 { done: true }를 반환합니다.
  • for await ... of 반복문을 사용해, 비동기 이터러블에서 값을 순차적으로 받아 처리합니다.

비동기 제너레이터

비동기 제너레이터는 일반 제너레이터 함수 앞에 async 키워드를 붙여 정의합니다. 이를 사용하면 제너레이터 내부에서 await를 사용할 수 있으며, 자동으로 비동기 이터러블을 생성할 수 있습니다.

async function* asyncSequence(start, end) {
  for (let i = start; i <= end; i++) {
    // 비동기 작업(딜레이)
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i;
  }
}

(async () => {
  console.log("비동기 제너레이터 시작:");
  for await (const value of asyncSequence(1, 5)) {
    console.log(value); // 1, 2, 3, 4, 5 (각 값 사이에 1초 딜레이)
  }
  console.log("비동기 제너레이터 완료.");
})();
  • async function* 구문은 함수를 비동기 제너레이터 함수로 선언합니다. 비동기 제너레이터 함수는 일반 제너레이터 함수와 유사하게 실행 흐름을 제어하고 값을 산출하지만, 비동기적인 작업을 포함할 수 있다는 점이 핵심적인 차이점입니다.
  • 비동기 제너레이터 함수 내부에서는 await 키워드를 사용하여 프라미스 기반 비동기 작업의 완료를 대기할 수 있습니다.
  • yield 키워드는 비동기 제너레이터 함수 내에서 비동기적인 값을 산출하는 역할을 수행합니다.

비동기 제너레이터를 활용한 예시

// 백프레셔(backpressure)를 생성하는 기능을 명확하게 나타냅니다.
async function* generateBackpressure(id, interval) {
  while (true) {
    await new Promise(resolve => setTimeout(resolve, interval));
    yield { id, value: Math.random() * 100 };
  }
}

// 여러 백프레셔 스트림을 병합하는 기능을 명확히 설명합니다.
async function* mergeBackpressureStreams(...backpressures) {
  while(true) {
    const result = await Promise.all(backpressures.map(backpressure => backpressure.next()))
    yield result;
  }          
}

// 스트림의 데이터를 변환하는 기능을 명시합니다.
async function* transformStream(mergeBack, fn) {
  for await (const data of mergeBack) {
    yield fn(data)
  }
}

// 각 배치에서 최대값을 필터링하는 기능을 설명합니다.
async function* filterMaxValue(mergeBack) {
  for await (const data of mergeBack) {
    let max = -Infinity;
    let result = null
    data.forEach(e => {
      if(max < e.value) {
        max = e.value;
        result = e;
      }
    })
    yield result;
  }
}

async function startExample() {
  const backpressure1 = generateBackpressure(1, 1000);
  const backpressure2 = generateBackpressure(2, 2000);
  const backpressure3 = generateBackpressure(3, 3000);
  const mergedStream = mergeBackpressureStreams(backpressure1, backpressure2, backpressure3)
  // 구조분해를 통해서 스트림 데이터를 변환
  const transformedStream = transformStream(mergedStream, data => {
    return data.map(({value:{id, value}, ...rest}) => ({id, value: Math.ceil(value), ...rest}))
  })
  const filteredStream = filterMaxValue(transformedStream);
  for await (const data of filteredStream) {
    console.log(data)
  }
}

startExample();