---
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<T>(url: string): AsyncGenerator<T> {
  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<User>('/api/users')) {
  if (await shouldStop(user)) break; // generator 의 finally 자동 실행
  await process(user);
}
```

### 동시성 제한 매핑
```ts
async function* mapConcurrent<T, U>(
  items: AsyncIterable<T>,
  fn: (t: T) => Promise<U>,
  concurrency: number
): AsyncGenerator<U> {
  const buffer: Promise<U>[] = [];
  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<string> {
  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<Event> {
  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<void>[] 반환 — 안 동작. for-await 필수.

## 🤖 LLM 활용 힌트
- 페이지네이션 / 라인 처리 / streaming HTTP 는 디폴트 async generator.
- AbortSignal 와 try/finally 한 쌍.

## 🔗 관련 문서
- [[Backpressure_Patterns]]
- [[Idempotent_Operations]]
