개요

여러 비동기 작업을 한 번에 묶어 처리하거나 결과를 모아야 하는 경우가 잦음 키-값 기반으로 데이터를 구조화해 저장하고 순회해야 하는 요구도 흔함 이 글은 Promise.all의 동작과 주의점, Map의 핵심 사용법을 개발자 관점에서 요약 정리함

Promise.all 개념과 정의

여러 Promise를 단일 Promise로 집계하는 유틸리티 모든 입력이 이행되면 결과를 같은 순서의 배열로 반환 하나라도 거부되면 즉시 거부로 끝나는 fail fast 특성 보유 입력은 Promise와 값 혼합 가능하며 값은 내부적으로 Promise.resolve로 이행 처리됨

Promise.all 동작 원리와 특징

집계 대상 생성 시점에 작업이 시작됨 Promise.all 자체가 작업을 시작시키는 것은 아니고, 보통 배열에 담는 과정에서 각 Promise의 executor가 실행됨 모든 입력이 이행되면 동일한 인덱스 순서로 결과 배열 반환 하나라도 거부되면 첫 번째 거부 이유로 즉시 종료 종료 이후에도 이미 시작된 다른 작업의 부수효과는 계속 진행될 수 있음 빈 배열을 넘기면 즉시 이행하며 결과는 빈 배열 이터러블이면 배열이 아니어도 처리 가능

간단 문법

Promise.all(iterable)
  • iterable에 Promise 또는 값 나열
  • 반환은 단일 Promise 하나

사용 예시 최소 스니펫

const a = Promise.resolve(3)
const b = new Promise((resolve) => setTimeout(() => resolve('foo'), 1000))

Promise.all([a, b])
  .then(([x, y]) => {
    console.log(x, y) // 3 'foo'
  })
  .catch(err => {
    console.error('error', err)
  })

거부가 포함된 경우의 동작

const ok = Promise.resolve(1)
const fail = new Promise((_, reject) => reject(new Error('boom')))

Promise.all([ok, fail])
  .then(() => {})
  .catch(err => {
    console.error(err.message) // boom
  })

주의와 한계

fail fast 특성으로 하나가 거부되면 즉시 거부하여 나머지 결과는 수집되지 않음 부분 성공과 실패를 모두 보고 싶다면 Promise.allSettled 선택 이미 실행된 비동기 작업의 취소는 기본적으로 지원하지 않음 AbortController 같은 별도 취소 메커니즘 연동 필요 동시성 제어 기능 없음 필요하면 큐나 제한 도구를 별도로 구성하여 배치 크기 제한 네트워크나 스토리지처럼 외부 리소스를 다루는 경우, 실패 시 이미 진행된 요청의 부수효과를 고려한 롤백 전략 필요

TypeScript에서의 반환 타입 추론

배열 리터럴을 전달하면 튜플 기반으로 각 요소 타입을 추론

const p1: Promise<number> = Promise.resolve(5)
const p2: Promise<string> = Promise.resolve('hi')

Promise.all([p1, p2]).then(([n, s]) => {
  // n: number, s: string
})

infer가 흐려지는 경우는 가변 배열이나 넓은 타입으로 수집할 때 발생 가능 필요 시 as const 또는 명시적 제네릭 타입 힌트로 튜플화

Promise.all과 Promise.allSettled 비교

  • Promise.all
    • 하나라도 거부되면 즉시 거부
    • 모든 결과가 필요하고 어느 하나의 실패 시 전체 흐름을 중단해야 할 때 적합
  • Promise.allSettled
    • 모든 입력의 상태와 값을 끝까지 수집
    • 일부 실패를 허용하고 결과를 종합 평가할 때 적합
Promise.allSettled([a, b]).then(results => {
  for (const r of results) {
    if (r.status === 'fulfilled') console.log(r.value)
    else console.warn(r.reason)
  }
})

현실적인 사용 맥락과 팁

여러 독립 API 요청을 동시에 보내고 전부 필요할 때 사용 각 요청은 미리 생성 후 한 번에 Promise.all로 합치기 개별 실패가 전체를 망치면 안 되는 경우에는 allSettled로 전환 후 실패만 필터링해 재시도 빈 입력은 즉시 이행되므로 조건 분기 간소화 가능 입력 순서가 결과 순서를 보장하므로 호출 순서와 매핑 로직을 단순화

Map 개념과 정의

키-값 쌍을 저장하는 표준 컬렉션 모든 타입을 키로 사용 가능하며 삽입 순서 유지 키 중복 불가이며 동일 키 설정 시 마지막 값이 유지

Map의 핵심 특징

모든 자료형 키 지원, 객체와 함수 포함 삽입 순서 기반 순회 보장 size 프로퍼티로 요소 개수 조회 직렬화 내장 지원 없음, 직렬화 시 변환 로직 필요 객체 키는 참조 동일성 기준 비교 사용

기본 사용법 최소 스니펫

생성과 추가

const m = new Map()
m.set('name', 'Alice')
m.set('age', 25)

조회와 존재 확인

m.get('name') // 'Alice'
m.has('age')  // true

삭제와 크기

m.delete('age')
m.size // 1

순회

for (const [k, v] of m) {
  console.log(k, v)
}

객체와 Map 비교 포인트

  • 키 타입
    • 객체는 문자열과 심벌 중심
    • Map은 모든 데이터 타입을 키로 허용
  • 순서와 순회
    • 객체는 속성 열거 순서가 상황에 따라 달라질 수 있음
    • Map은 삽입 순서 보장으로 일관 순회 가능
  • 크기 계산
    • 객체는 Object.keys().length 필요
    • Map은 size 즉시 제공
  • 성능과 용도
    • 다량의 삽입·삭제·순회에서 Map이 일관된 특성을 제공
    • 단순 구조, 직렬화 중심 데이터는 객체가 편의성 높음

실제 사용 예시와 팁

간단 캐시 패턴

const cache = new Map()
async function getUser(userId) {
  if (cache.has(userId)) return cache.get(userId)
  const data = await doFetchUser(userId) // 외부 호출 추상화
  cache.set(userId, data)
  return data
}

주의사항

  • 객체를 키로 쓸 때는 같은 모양의 다른 객체는 다른 키로 취급됨
  • 캐시로 사용할 때는 만료 정책, 약참조 사용 가능성, 메모리 누수 방지 전략 필요
  • 직렬화가 필요하면 배열이나 객체로 변환해 저장 형식에 맞추는 전처리 필요

마무리

여러 비동기 작업을 동시에 묶고 모두의 성공이 필요하면 Promise.all 선택 부분 실패를 용인하고 결과를 종합하려면 Promise.allSettled 고려 키-값 컬렉션이 필요하고 키 타입의 제약 없이 일관 순회가 중요하면 Map 선택 객체는 단순 데이터 구조와 직렬화 중심 시나리오에서 여전히 유효 용도와 제약을 기준으로 도구를 고르는 것이 유지보수와 성능 모두에 이득임

참고자료