--- id: js-async-iterator-patterns title: JS Async Iterator — pull 기반 스트림 처리 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [javascript, typescript, async-iterator, streams, vibe-coding] tech_stack: { language: "TypeScript / Node.js / Browser", applicable_to: ["Backend", "Web"] } applied_in: [] aliases: [for await, async generator, AsyncIterable, Symbol.asyncIterator] --- # Async Iterator — pull 기반 스트림 > Async generator (`async function*`) + `for await` 는 **pull 기반 자연스러운 backpressure** 제공. consumer 가 `await` 끝낼 때까지 producer 가 다음 yield 안 함. RxJS 같은 push 모델보다 단순. ## 📖 핵심 개념 - `async function* gen()` → 호출하면 `AsyncGenerator`. - `for await (const x of gen())` 로 소비. - yield 시점에 consumer 가 await 중이면 producer suspend. - 자연스러운 cleanup: try/finally 가 break/throw 시에도 실행. ## 💻 코드 패턴 ### 페이지 fetch 스트리밍 ```ts async function* paginate(url: string): AsyncGenerator { let cursor: string | null = null; do { const res = await fetch(`${url}?cursor=${cursor ?? ''}`).then(r => r.json()); for (const item of res.items) yield item; cursor = res.nextCursor; } while (cursor); } // 소비자가 일찍 멈출 수 있음 for await (const user of paginate('/api/users')) { if (await shouldStop(user)) break; // generator 의 finally 자동 실행 await process(user); } ``` ### 동시성 제한 매핑 ```ts async function* mapConcurrent( items: AsyncIterable, fn: (t: T) => Promise, concurrency: number ): AsyncGenerator { const buffer: Promise[] = []; for await (const item of items) { buffer.push(fn(item)); if (buffer.length >= concurrency) { yield await buffer.shift()!; } } while (buffer.length) yield await buffer.shift()!; } ``` ### 파일 라인 단위 처리 (Node) ```ts import { createReadStream } from 'node:fs'; import { createInterface } from 'node:readline/promises'; async function* lines(path: string): AsyncGenerator { const rl = createInterface({ input: createReadStream(path), crlfDelay: Infinity }); try { for await (const line of rl) yield line; } finally { rl.close(); } } ``` ### Cleanup with try/finally ```ts async function* watch(): AsyncGenerator { const ctrl = new AbortController(); try { for await (const ev of subscribe(ctrl.signal)) yield ev; } finally { ctrl.abort(); // break / throw 시 자동 } } ``` ## 🤔 의사결정 기준 | 데이터 | 권장 | |---|---| | 한 번에 다 메모리에 못 담음 | async iterator | | 페이지네이션 / 무한 스크롤 source | async iterator | | 한 번 producer 시작 후 다수 consumer | async iterator X — EventEmitter / SharedFlow | | HTTP streaming response | ReadableStream → async iterator (`for await (const chunk of res.body)`) | | RxJS 가 더 자연스러운 (combine, debounce, retry) | RxJS | ## ❌ 안티패턴 - **소비 중 break 후 cleanup 안 함**: try/finally 누락 → connection / file leak. - **Promise.all 로 모두 모은 후 yield**: pull 의 의미 없음. 메모리 폭발. - **await 없는 yield**: producer 가 즉시 다음 yield → backpressure 의미 잃음. - **async iterator 안에서 setState 직접**: React 컴포넌트면 unmount 후 위험. AbortSignal 결합. - **iterator 이중 소비**: 대부분 single-use. tee / branching 라이브러리 필요. - **for-of 로 async iterator 소비**: 그냥 Promise[] 반환 — 안 동작. for-await 필수. ## 🤖 LLM 활용 힌트 - 페이지네이션 / 라인 처리 / streaming HTTP 는 디폴트 async generator. - AbortSignal 와 try/finally 한 쌍. ## 🔗 관련 문서 - [[Backpressure_Patterns]] - [[Idempotent_Operations]]