개요

비동기는 강력한 도구임. 다만 배열과 스트림 같은 이터레이터와 결합되면 누가 무엇을 언제 기다리는지 불명확해지기 쉬움 핵심 포인트 세 가지 기억

  • 완료 보장 확보했는지
  • 동시성 제어를 명시했는지
  • 백프레셔로 생산 속도 ≤ 소비 속도 유지했는지

비동기가 의도대로 동작하지 않는 케이스

  • 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로 기다림
  • 동시성 제한 필수. 배치나 세마포어로 시스템 보호 이 원칙을 지키면 의도대로 정확하고 빠른 비동기 처리가 가능함

참고자료