비동기 이터레이터와 for-await-of
이전 11장 2절에서 일반 제너레이터로 이터러블 객체를 만들고 동기적으로 순회하는 방법을 배웠습니다.
하지만 웹/Node.js 환경에서는 파일 스트림, 웹소켓 메시지, DB 커서처럼 비동기 데이터 시퀀스를 순회해야 하는 경우가 더 많습니다.
이런 비동기 스트림을 동기 코드처럼 다룰 수 있도록
ES2018에서 비동기 이터레이터(Async Iterators)와
for await...of 구문이 도입됐습니다.
타입스크립트는 이 기능을 완전히 지원하며, 강력한 타입 검사로 스트림 처리 코드를 더 견고하게 만들어 줍니다.
비동기 이터러블과 비동기 이터레이터
정의- 비동기 이터러블 (Async Iterable):
Symbol.asyncIterator메서드를 구현한 객체입니다. 이 메서드는 비동기 이터레이터(Async Iterator)를 반환합니다. - 비동기 이터레이터 (Async Iterator):
next()메서드가 Promise를 반환하는 객체입니다. 이 Promise는{ value: T, done: boolean }형태의 객체로 resolve됩니다.
설명:
일반 이터러블(Symbol.iterator를 구현)이 next() 메서드에서 직접 { value, done } 객체를 반환하는 반면, 비동기 이터러블은 next() 메서드가 Promise를 반환합니다.
그리고 그 Promise가 { value, done } 객체로 resolve됩니다.
여기서 value는 다음 비동기 데이터이고, done은 스트림 종료 여부를 나타냅니다.
for await...of 구문
아래 다이어그램은 이 절의 핵심 흐름을 역할과 상태 전환 중심으로 정리한 것입니다.
for await...of 루프는 비동기 이터러블을 순회하기 위한 전용 구문입니다. 일반 for...of 루프가 동기 이터러블을 순회하듯이, for await...of는 비동기적으로 생성되는 값을 기다리며 순회합니다.
// 비동기 제너레이터 함수 (async function*)를 사용하여 비동기 이터러블 구현
async function* asyncNumberGenerator(): AsyncGenerator<number, void, undefined> {
let count = 0;
while (count < 5) {
console.log(`[Generator] Yielding ${count} after 500ms...`);
await new Promise(resolve => setTimeout(resolve, 500)); // 비동기 작업 대기
yield count++;
}
console.log("[Generator] Done yielding numbers.");
}
async function processAsyncStream(): Promise<void> {
console.log("Starting async stream processing...");
const generator = asyncNumberGenerator(); // 비동기 이터러블 (async generator object) 반환
for await (const num of generator) { // for await...of 루프 사용
console.log(`[Consumer] Received: ${num}`);
await new Promise(resolve => setTimeout(resolve, 200)); // 각 항목 처리에도 비동기 작업 시뮬레이션
}
console.log("Async stream processing finished.");
}
processAsyncStream();
console.log("Main program continues immediately...");asyncNumberGenerator()를 호출하면 즉시 AsyncGenerator 객체가 반환됩니다.
for await (const num of generator) 루프는 이 AsyncGenerator 객체의 [Symbol.asyncIterator]() 메서드를 호출하여 비동기 이터레이터를 얻습니다. (사실 async function*는 이미 그 자체로 비동기 이터레이터 겸 이터러블입니다.)
루프는 반복적으로 이터레이터의 next() 메서드를 호출합니다.
next()는 Promise를 반환하고, for await...of는 이 Promise가 해결될 때까지 기다립니다.
Promise가 { value, done } 객체로 resolve되면, value는 num 변수에 할당되고 루프 본문이 실행됩니다.
done이 true가 될 때까지 이 과정이 반복됩니다.
비동기 이터레이터의 활용 사례
비동기 이터레이터는 다음과 같은 시나리오에서 매우 유용합니다.
파일 시스템 스트림 읽기: 대용량 파일을 청크(chunk) 단위로 비동기적으로 읽을 때.
import * as fs from 'fs';
import * as readline from 'readline';
async function* readLinesFromFile(filePath: string): AsyncGenerator<string> {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
for await (const line of rl) {
yield line;
}
}
async function processLargeFile(filePath: string): Promise<void> {
console.log(`Processing file: ${filePath}`);
try {
let lineNumber = 1;
for await (const line of readLinesFromFile(filePath)) {
console.log(`Line ${lineNumber++}: ${line}`);
// await new Promise(resolve => setTimeout(resolve, 10)); // 각 라인 처리의 비동기 시뮬레이션
}
console.log("Finished processing file.");
} catch (error) {
console.error("Error processing file:", error);
}
}
// 예시 사용: 'my_large_file.txt' 라는 파일이 있다고 가정
// Node.js 환경에서 실행 가능
// require('fs').writeFileSync('my_large_file.txt', 'Line 1\nLine 2\nLine 3\n');
// processLargeFile('my_large_file.txt');웹소켓 메시지 처리: 실시간으로 수신되는 메시지를 순차적으로 처리할 때.
// 가상의 웹소켓 클라이언트 (실제 구현은 ws 라이브러리 등 사용)
class MockWebSocket {
private messages: string[] = ['Hello', 'World', 'Async', 'Iterator', 'Done!'];
private index = 0;
async *[Symbol.asyncIterator](): AsyncGenerator<string> {
while (this.index < this.messages.length) {
await new Promise(resolve => setTimeout(resolve, 300)); // 메시지 수신 대기 시뮬레이션
const message = this.messages[this.index++];
console.log(`[WebSocket] Sending: ${message}`);
yield message;
}
}
}
async function processWebSocketMessages(): Promise<void> {
console.log("Connecting to WebSocket...");
const ws = new MockWebSocket(); // 실제로는 new WebSocket(...)
for await (const msg of ws) {
console.log(`[App] Received: ${msg}`);
if (msg === 'Done!') {
break; // 특정 메시지 수신 시 종료
}
}
console.log("WebSocket message processing finished.");
}
processWebSocketMessages();데이터베이스 커서: 대량의 쿼리 결과를 커서(Cursor) 방식으로 비동기적으로 가져올 때.
// 가상의 데이터베이스 커서
interface DbRecord {
id: number;
name: string;
}
class MockDbCursor {
private data: DbRecord[] = [
{ id: 1, name: 'Item A' },
{ id: 2, name: 'Item B' },
{ id: 3, name: 'Item C' },
{ id: 4, name: 'Item D' },
];
private currentIndex = 0;
private pageSize = 2; // 페이지당 가져올 레코드 수
async *[Symbol.asyncIterator](): AsyncGenerator<DbRecord[]> {
while (this.currentIndex < this.data.length) {
const page = this.data.slice(this.currentIndex, this.currentIndex + this.pageSize);
this.currentIndex += this.pageSize;
await new Promise(resolve => setTimeout(resolve, 700)); // DB 쿼리 지연 시뮬레이션
console.log(`[DB Cursor] Yielding page with ${page.length} records.`);
yield page; // 레코드 묶음을 페이지 단위로 yield
}
}
}
async function processDatabaseRecords(): Promise<void> {
console.log("Fetching database records...");
const cursor = new MockDbCursor();
for await (const page of cursor) {
console.log(`[App] Processing new page of records (${page.length} items):`);
for (const record of page) {
console.log(` - Record: ${record.id}, ${record.name}`);
}
await new Promise(resolve => setTimeout(resolve, 300)); // 각 페이지 처리의 비동기 시뮬레이션
}
console.log("Finished processing all database records.");
}
processDatabaseRecords();타입스크립트와 비동기 이터레이터
타입스크립트는 비동기 이터레이터를 위한 타입을 내장하고 있으며, async function* 문법을 사용할 때 이를 자동으로 추론합니다.
AsyncIterable<T>:[Symbol.asyncIterator](): AsyncIterator<T>메서드를 구현한 객체의 타입입니다.AsyncIterator<T>:next(): Promise<IteratorResult<T>>메서드를 구현한 객체의 타입입니다.AsyncGenerator<YieldType, ReturnType, NextType>:async function*가 반환하는 제너레이터 객체의 타입입니다. 일반Generator와 유사하게yield값,return값,next()인자의 타입을 명시할 수 있습니다.
타입스크립트를 사용하면 for await...of 루프 내에서 변수의 타입이 정확하게 추론되므로, 개발자가 예상치 못한 타입 오류를 방지할 수 있습니다.
// 이전 예시의 async function* asyncNumberGenerator(): AsyncGenerator<number, void, undefined>
// 여기서 <number>는 yield 되는 값의 타입, <void>는 최종 return 값의 타입,
// <undefined>는 next()에 전달할 수 있는 값의 타입 (없으므로 undefined)
async function consumeStringStream(): Promise<void> {
async function* stringStream(): AsyncGenerator<string> {
yield "Hello";
yield "TypeScript";
yield "Async";
yield "Iterators";
}
for await (const item of stringStream()) {
console.log(item.length); // item은 string 타입으로 추론되므로 .length 접근 가능
// item.toFixed(); // Error: 'string' 형식에 'toFixed' 속성이 없습니다. (타입 안전성 보장)
}
}
consumeStringStream();비동기 스트림 처리 요약
비동기 이터레이터와 for await...of는 비동기 데이터 스트림을 동기 코드처럼 간결하고 직관적으로 처리할 수 있게 해주는 강력한 기능입니다.
Promise와 async/await이 단일 비동기 작업에 최적화된 반면, 비동기 이터레이터는 연속적인 비동기 값의 흐름을 다루는 데 강점을 가집니다.
타입스크립트의 타입 시스템과 결합하면 이러한 비동기 스트림 처리 코드를 더욱 안전하고 유지보수하기 쉽게 만들 수 있습니다.
이것으로 11장 비동기 프로그래밍을 마칩니다. 비동기 프로그래밍은 현대 웹 및 서버 개발의 핵심이며, Promise, async/await, 제너레이터, 그리고 비동기 이터레이터는 이러한 복잡성을 효과적으로 관리하기 위한 필수적인 도구들입니다.