개요
비동기는 강력한 도구임. 다만 배열과 스트림 같은 이터레이터와 결합되면 누가 무엇을 언제 기다리는지 불명확해지기 쉬움 핵심 포인트 세 가지 기억
- 완료 보장 확보했는지
- 동시성 제어를 명시했는지
- 백프레셔로 생산 속도 ≤ 소비 속도 유지했는지
비동기가 의도대로 동작하지 않는 케이스
forEach + async 사용
- 콜백이 반환한 프로미스를 외부가 수집하지 않음 → 완료 보장 깨짐, 레이스와 누락 가능성 증가
items.forEach(async (x) => { await doAsync(x) // 외부에서 기다리지 않음 })map + async 이후 기다리지 않음
- map은 프로미스 배열을 반환함. 배열 자체를 await 불가, 반드시 모아서 기다려야 함
const promises = items.map((x) => doAsync(x)) // await promises // X await Promise.all(promises) // O무제한 병렬 처리
- Promise.all에 수천 개를 한 번에 던지면 API·DB·네트워크 폭주, 스로틀·쿼터 초과, 메모리 스파이크 유발
스트림 백프레셔 무시
- 생산 속도가 소비 속도보다 빠르면 버퍼 적체로 메모리 터짐. 소비 측에서 처리 속도 제어 필요
에러 집계 전략 부재
- Promise.all은 하나라도 실패하면 전체 reject. 일부 실패를 허용하면서 진행하려면 allSettled 필요
순서 보장 착각
- 병렬 처리에서는 완료 순서가 섞임. 순서 보장이 필요하면 순차 처리 또는 인덱스 기반 재정렬 필요
상황별 베스트 프랙티스
순차 처리 필요 시
- 순서 중요, 레이트리밋, 트랜잭션, 멱등 요구 같은 조건에서 안정성과 백프레셔 확보 목적
for (const item of items) { await doAsync(item) // 하나 끝나야 다음으로 이동 }최대 속도 필요 시
- 순서 무관, 리소스 여유가 있을 때 전체 처리 시간 최소화 목적
await Promise.all(items.map((item) => doAsync(item)))병렬 + 동시성 제한 필요 시
- 외부·내부 리소스 보호와 안정적 처리율 확보 목적
const K = 5 for (let i = 0; i < items.length; i += K) { await Promise.all(items.slice(i, i + K).map(doAsync)) }일부 실패 허용·집계 필요 시
- 성공과 실패를 분리해 보고 및 재시도 가능 상태로 유지
const results = await Promise.allSettled(items.map(doAsync)) const successes = results.filter(r => r.status === 'fulfilled').map(r => r.value) const failures = results.filter(r => r.status === 'rejected').map(r => r.reason)스트리밍·비동기 소스 처리 시
- 자연스러운 백프레셔와 메모리 효율 확보 목적
for await (const chunk of asyncIterable) { await handle(chunk) // 입력 속도에 맞춰 소비 }map에서의 비동기 처리 패턴
- 올바른 패턴은 프로미스 배열 생성 후 모아서 기다림
const promises = items.map((x) => doAsync(x)) await Promise.all(promises)- 잘못된 패턴 예시
- await items.map(…) 사용 불가, 배열은 await 대상 아님
- forEach + async 조합은 외부 대기 불가
타임아웃·취소·재시도 넣기
- 외부 I/O 안정성 강화 목적. 행잉 방지와 회복 탄력성 확보
const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 5_000) try { await fetch(url, { signal: controller.signal }) } finally { clearTimeout(timeout) }
맺음말
비동기와 이터레이터 조합에서의 실패 원인은 대개 기다림, 동시성, 백프레셔를 명시하지 않은 데서 옴 기억할 것
- 순차는 for…of + await
- 병렬은 Promise.all, 실패 집계는 Promise.allSettled
- 스트림은 for await…of
- forEach + async는 금지, map은 반드시 Promise.all로 기다림
- 동시성 제한 필수. 배치나 세마포어로 시스템 보호 이 원칙을 지키면 의도대로 정확하고 빠른 비동기 처리가 가능함
참고자료
- https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
- https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
- https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/for-await...of
- https://developer.mozilla.org/docs/Web/API/AbortController
- https://nodejs.org/api/stream.html#streams-compatibility-with-async-generators-and-async-iterators