[JavaScript] Promise.all은 언제 사용하는 걸까? (비동기 문제 해결하기)

[JavaScript] Promise.all은 언제 사용하는 걸까? (비동기 문제 해결하기)

·

3 min read

Promise란 ES6에서 비동기 처리를 위한 패턴이다. 전통적인 콜백 패턴이 가진 단점인 콜백 헬을 방지하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점을 지닌다.

Promise는 여러 정적 메서드를 제공한다. 그 중 Promise.all에 대해 살펴보자.

🟢 Promise.all이란?

Promise.all 메서드는 여러 개의 비동기 처리를 모두 병렬로 처리할 때 사용한다.

const req1 = () =>
    new Promise((resolve, reject) => setTimeout(() => resolve(new Error('1')), 3000)

const req2 = () =>
    new Promise((resolve, reject) => setTimeout(() => resolve(new Error('2')), 2000)

const req3 = () =>
    new Promise((resolve, reject) => setTimeout(() => resolve(new Error('3')), 1000)

const res = []
req1()
    .then(data => {
        res.push(data)
        return req2()
    })
    .then(data => {
        res.push(data)
        return req3()
    })
    .then(data => {
        res.push(data)
        console.log(res)
    })
    .catch(console.error)

세 개의 비동기 처리를 순차적으로 처리하면 어떻게 될까? 앞선 비동기 처리가 완료되면 다음 비동기 처리를 수행하기 때문에 총 6초 이상이 소요된다.

그러나 위 코드의 경우 비동기 처리가 서로 의존하지 않고 개별적으로 수행된다. 따라서 세 개의 비동기를 순차적으로 처리할 필요가 없다.

Promise.all 메서드는 이러한 비동기 처리를 병렬적으로 처리할 때 사용한다.

const req1 = () =>
    new Promise((resolve, reject) => setTimeout(() => resolve(new Error('1')), 3000)

const req2 = () =>
    new Promise((resolve, reject) => setTimeout(() => resolve(new Error('2')), 2000)

const req3 = () =>
    new Promise((resolve, reject) => setTimeout(() => resolve(new Error('3')), 1000)

Promise.all([req1, req2, req3])
    .then(console.log) // [1, 2, 3]
    .catch(console.log)

위 코드에서 각 프로미스는 다음과 같이 동작한다.

  • req1 프로미스는 3초 후에 1을 resolve 한다.

  • req2 프로미스는 2초 후에 2을 resolve 한다.

  • req3 프로미스는 1초 후에 3을 resolve 한다.

모든 프로미스가 모두 fulfilled 상태가 되면 종료한다. 따라서 Promise.all이 종료하는 데 걸리는 시간은 가장 늦게 fulfilled 상태가 되는 3초보다 조금 더 걸린다.

모든 프로미스가 fulfilled 상태가 되면 resolve된 결과를 모두 배열에 저장하고 새로운 프로미스를 반환한다. 이때 Promise.all은 처리 순서가 보장되므로, 첫 번째 프로미스가 가장 나중에 fulfilled 되어도 배열 순서와 동일하게 새로운 프로미스를 반환한다.

Promise.all 메서드는 배열의 프로미스가 하나라도 rejected 상태가 되면 나머지 프로미스가 fulfilled 상태가 되는 것을 기다리지 않고 바로 종료한다.

const req1 = () =>
    new Promise((resolve, reject) => setTimeout(() => reject(new Error('1')), 3000)

const req2 = () =>
    new Promise((resolve, reject) => setTimeout(() => reject(new Error('2')), 2000)

const req3 = () =>
    new Promise((resolve, reject) => setTimeout(() => reject(new Error('3')), 1000)

Promise.all([req1, req2, req3])
    .then(console.log)
    .catch(console.log) // 3

위 코드에서 req3 프로미스가 가장 먼저 reject 상태가 되므로 해당 에러가 catch 메서드로 전달된다. 따라서 3이 로그에 찍히고 나머지 프로미스는 종료된다.

🔵 Promise.all 사용하기

실제로 Promise.all을 사용하여 문제를 해결한 예시를 보자.

const response = await fetch(`https://pokeapi.co/api/v2/pokemon/?limit=${LIMIT}&offset=${(page - 1) * LIMIT}`)
const data = await response.json()
const pokemonsData: PokemonType[] = await data.results.map(async ({ url }: { url: string }) => {
  const id = getIdFromUrl(url)
  const name = await getKoreanName(id)
  return {
    name,
    id,
  }
})
setPokemons(pokemonsData)

위 코드는 포켓몬 데이터의 name과 id 값을 배열에 담아 Pokemons 상태에 저장하는 과정 중 일부이다.

이렇게 데이터가 패치되었지만 id와 name 값이 Unknown 상태이기 때문에 아무런 정보도 뜨지 않는다.

이런 결과가 나타난 이유는 다음과 같다. data.results.map에서 반환된 배열은 비동기 함수의 프로미스 객체를 포함한 배열이다. 하지만 await를 사용하면 배열 자체가 비동기 작업의 결과가 아닌 프로미스를 반환한다.

이 문제를 해결하기 위해서 Promise.all 메서드를 사용할 수 있다. Promise.all을 통해 모든 비동기 작업이 완료한 후 결과를 한 번에 반환할 수 있다.

const response = await fetch(`https://pokeapi.co/api/v2/pokemon/?limit=${LIMIT}&offset=${(page - 1) * LIMIT}`)
const data = await response.json()
const promises: PokemonType[] = data.results.map(async ({ url }: { url: string }) => {
  const id = getIdFromUrl(url)
  const name = await getKoreanName(id)
  return {
    name,
    id,
  }
})
const pokemonsData = await Promise.all(promises)
setPokemons(pokemonsData)

data.results.map 배열을 Promise.all 메서드의 인수로 넣어 병렬적으로 작업하여 아래와 같이 올바르게 결과를 받을 수 있게 되었다.


https://poiemaweb.com/es6-promise#72-promiseall