icon안동민 개발노트

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


 비동기 이터레이터는 비동기적으로 데이터를 순회할 수 있게 해주는 객체입니다.

 이는 일반 이터레이터와 유사하지만, Promise 기반으로 동작하여 비동기 데이터 스트림을 효과적으로 처리할 수 있습니다.

비동기 이터레이터 vs 일반 이터레이터

 주요 차이점

  1. next() 메서드가 Promise를 반환합니다.
  2. Symbol.asyncIterator 메서드를 사용합니다.
  3. for-await-of 루프로 순회합니다.

비동기 이터러블 객체 정의 및 구현

 타입스크립트에서 비동기 이터러블 객체 구현

class AsyncRandomNumbers implements AsyncIterable<number> {
    private count: number;
 
    constructor(count: number) {
        this.count = count;
    }
 
    public async *[Symbol.asyncIterator](): AsyncIterator<number> {
        for (let i = 0; i < this.count; i++) {
            await new Promise(resolve => setTimeout(resolve, 1000));
            yield Math.random();
        }
    }
}
 
// 사용 예
const asyncNumbers = new AsyncRandomNumbers(3);
for await (const num of asyncNumbers) {
    console.log(num);
}

for-await-of 문

 for-await-of 문은 비동기 이터러블 객체를 순회하는 데 사용됩니다.

async function processAsyncIterable(asyncIterable: AsyncIterable<number>) {
    for await (const value of asyncIterable) {
        console.log(value);
    }
}
 
processAsyncIterable(new AsyncRandomNumbers(5));

 이 루프는 각 반복에서 Promise가 이행될 때까지 기다린 후 다음 값으로 진행합니다.

Symbol.asyncIterator 메서드

 Symbol.asyncIterator는 객체의 비동기 이터레이터를 반환하는 메서드를 정의합니다.

class AsyncCounter {
    private count: number;
 
    constructor(count: number) {
        this.count = count;
    }
 
    [Symbol.asyncIterator](): AsyncIterator<number> {
        let current = 0;
        const max = this.count;
 
        return {
            async next() {
                if (current < max) {
                    await new Promise(resolve => setTimeout(resolve, 1000));
                    return { value: current++, done: false };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
}
 
// 사용 예
(async () => {
    const counter = new AsyncCounter(3);
    for await (const value of counter) {
        console.log(value);  // 0, 1, 2를 1초 간격으로 출력
    }
})();

비동기 제네레이터 함수

 비동기 제네레이터 함수는 async function* 문법을 사용하여 정의합니다.

async function* asyncGenerator() {
    yield await Promise.resolve(1);
    yield await Promise.resolve(2);
    yield await Promise.resolve(3);
}
 
async function useAsyncGenerator() {
    for await (const value of asyncGenerator()) {
        console.log(value);  // 1, 2, 3 출력
    }
}
 
useAsyncGenerator();

실제 사용 사례

  1. 페이지네이션
async function* fetchPaginatedData(url: string, pageSize: number) {
    let page = 1;
    while (true) {
        const response = await fetch(`${url}?page=${page}&pageSize=${pageSize}`);
        const data = await response.json();
        if (data.length === 0) break;
        yield* data;
        page++;
    }
}
 
async function processAllData() {
    const dataIterator = fetchPaginatedData('/api/data', 10);
    for await (const item of dataIterator) {
        console.log(item);
    }
}
  1. 실시간 데이터 스트리밍
async function* streamData(url: string) {
    const response = await fetch(url);
    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
 
    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        yield decoder.decode(value);
    }
}
 
async function processStream() {
    for await (const chunk of streamData('/api/stream')) {
        console.log(chunk);
    }
}

비동기 이터레이터 간 상호 운용성

 Promise 배열을 비동기 이터레이터로 변환

async function* promisesToAsyncIterator<T>(promises: Promise<T>[]) {
    for (const promise of promises) {
        yield await promise;
    }
}
 
// 사용 예
const promises = [
    Promise.resolve(1),
    Promise.resolve(2),
    Promise.resolve(3)
];
 
(async () => {
    for await (const value of promisesToAsyncIterator(promises)) {
        console.log(value);
    }
})();

타입 안정성 확보

 타입스크립트의 타입 시스템을 활용하여 비동기 이터레이터의 타입 안정성을 높일 수 있습니다.

interface AsyncIterableData<T> {
    [Symbol.asyncIterator](): AsyncIterator<T>;
}
 
class TypedAsyncIterable<T> implements AsyncIterableData<T> {
    private data: T[];
 
    constructor(data: T[]) {
        this.data = data;
    }
 
    async *[Symbol.asyncIterator](): AsyncIterator<T> {
        for (const item of this.data) {
            yield item;
        }
    }
}
 
async function processTypedAsyncIterable<T>(iterable: AsyncIterableData<T>) {
    for await (const item of iterable) {
        console.log(item);
    }
}
 
const numbers = new TypedAsyncIterable([1, 2, 3]);
processTypedAsyncIterable(numbers);

Best Practices와 주의사항

  1. 메모리 관리 : 대량의 데이터를 처리할 때 메모리 사용량에 주의하세요.
  2. 에러 처리 : try-catch 블록을 사용하여 비동기 이터레이션 중 발생할 수 있는 오류를 처리하세요.
  3. 취소 메커니즘 : 필요한 경우 이터레이션을 중단할 수 있는 방법을 제공하세요.
  4. 성능 고려 : 불필요한 비동기 작업을 피하고, 가능한 경우 배치 처리를 사용하세요.
  5. 타입 안정성 : 제네릭과 인터페이스를 활용하여 타입 안정성을 확보하세요.
  6. 테스트 용이성 : 비동기 이터레이터를 사용하는 코드에 대한 테스트 전략을 수립하세요.
  7. 문서화 : 복잡한 비동기 이터레이션 로직에 대해서는 주석을 통해 설명을 제공하세요.
  8. 호환성 고려 : 브라우저나 Node.js 버전에 따른 지원 여부를 확인하세요.
  9. 백프레셔 관리 : 데이터 생성 속도와 소비 속도의 균형을 유지하세요.
  10. 동시성 제어 : 필요한 경우 동시에 처리되는 비동기 작업의 수를 제한하세요.