비동기 이터레이터와 for-await-of
비동기 이터레이터는 비동기적으로 데이터를 순회할 수 있게 해주는 객체입니다.
이는 일반 이터레이터와 유사하지만, Promise 기반으로 동작하여 비동기 데이터 스트림을 효과적으로 처리할 수 있습니다.
비동기 이터레이터 vs 일반 이터레이터
주요 차이점
next()
메서드가 Promise를 반환합니다.Symbol.asyncIterator
메서드를 사용합니다.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();
실제 사용 사례
- 페이지네이션
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);
}
}
- 실시간 데이터 스트리밍
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와 주의사항
- 메모리 관리 : 대량의 데이터를 처리할 때 메모리 사용량에 주의하세요.
- 에러 처리 :
try-catch
블록을 사용하여 비동기 이터레이션 중 발생할 수 있는 오류를 처리하세요. - 취소 메커니즘 : 필요한 경우 이터레이션을 중단할 수 있는 방법을 제공하세요.
- 성능 고려 : 불필요한 비동기 작업을 피하고, 가능한 경우 배치 처리를 사용하세요.
- 타입 안정성 : 제네릭과 인터페이스를 활용하여 타입 안정성을 확보하세요.
- 테스트 용이성 : 비동기 이터레이터를 사용하는 코드에 대한 테스트 전략을 수립하세요.
- 문서화 : 복잡한 비동기 이터레이션 로직에 대해서는 주석을 통해 설명을 제공하세요.
- 호환성 고려 : 브라우저나 Node.js 버전에 따른 지원 여부를 확인하세요.
- 백프레셔 관리 : 데이터 생성 속도와 소비 속도의 균형을 유지하세요.
- 동시성 제어 : 필요한 경우 동시에 처리되는 비동기 작업의 수를 제한하세요.