[JavaScript] 프로미스 체이닝, 정적 메서드 정리

이은진·2021년 3월 12일
1

JavaScript Study

목록 보기
22/24

이전 글에서 비동기 처리를 위한 콜백 패턴이 콜백 헬 문제를 안고 있는 반면, 프로미스는 then, catch, finally 후속 처리 메서드를 통해 콜백 헬을 해결함을 알아보았다.

1. 프로미스 체이닝

1.1. 기존에 콜백 헬이 발생하는 예제

const url2 = 'https://jsonplaceholder.typicode.com'

callbackGet(`${url2}/posts/1`, ({userId}) => {
  console.log(userId)
  
  callbackGet(`${url2}/users/${userId}`, userInfo => {
    console.log(userInfo)
  })
})

콜백함수를 중첩하는 방식으로 비동기 처리를 하면 콜백 헬이 발생하여 가독성이 좋지 않다. 만약 위의 코드에서 다시 userInfo 값을 이용해서 후속처리를 하려면, 또다른 콜백함수를 내부에 작성해야 할 것이다.

1.2. 프로미스의 후속 처리 메서드로 해결하는 예제

const url2 = 'https://jsonplaceholder.typicode.com'

promiseGet(`${url2}/posts/1`)
  .then(({userId}) => promiseGet(`${url2}/users/${userId}`))
  .then(userInfo => console.log(userInfo))
  .catch(err => console.log(err))

프로미스 후속 처리 메서드인 then, catch, finally를 연속적으로 호출하는 것을 프로미스 체이닝이라고 한다. 해당 메서드는 언제나 프로미스를 반환하기 때문에 연속적으로 호출이 가능하고 resolve와 reject 처리가 직관적이다.

1.3. async / await 으로 가독성을 해결하는 예제

const asyncGet = async () => {
  const {userId} = await promiseGet(`${url2}/posts/1`)
  const userInfo = await promiseGet(`${url2}/users/${userId}`)
  console.log(userInfo)
}

asyncGet()

async / await도 프로미스를 기반으로 동작한다. async는 반드시 프로미스를 반환하고, await은 프로미스 앞에 쓰여, 해당 프로미스가 처리될 때까지 기다렸다가 반환한 프로미스 객체를 다음 await 뒤의 프로미스에게 넘겨준다. 동기 처리처럼 프로미스가 처리 결과를 반환하도록 구현한다.

2. 프로미스의 정적 메서드

2.1. Promise.resolve / Promise.reject

Promise.resolve / Promise.reject 는 이미 존재하는 값을 래핑하여 프로미스를 생성한다.

인수로 전달한 배열을 resolve하는 프로미스를 생성

const resolvedPromise = Promise.resolve([1,2,3])
resolvedPromise.then(console.log) // [1,2,3]

const resolvedPromise = new Promise(resolve => resolve([1,2,3]))
resolvedPromise.then(console.log) // [1,2,3]

→ 위 아래 코드가 동일하게 동작함.


인수로 전달한 에러 객체를 reject하는 프로미스를 생성

const rejectedPromise = Promise.reject(new Error('Error!'))
rejectedPromise.catch(console.log) // Error: Error!

const rejectedPromise = new Promise((_, reject) => reject(new Error('Errorrrrr!!!')))
rejectedPromise.catch(console.log) // Error: Errorrrrr!!!

→ 위 아래 코드가 동일하게 동작함

2.2. Promise.all

Promise.all은 여러 개의 비동기 처리를 한꺼번에 병렬처리할 때 사용한다.

여러 개의 비동기 처리를 순차적으로 처리할 때

const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000))
const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000))
const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000))

const res = []
requestData1()
  .then(data => {
    res.push(data)
    return requestData2()
})
  .then(data => {
    res.push(data)
    return requestData3()
})
  .then(data => {
    res.push(data)
    console.log(res) // [ 1, 2, 3 ] => 6초
})
  .catch((err) => console.log(err))

→ 첫 번째 비동기 처리에 3초, 두 번째에 2초, 세 번째에 1초 걸려서 총 6초 이상 소요
→ 서로 의존하지 않고 개별적으로 수행되는 것. 후속 처리가 아님. 순차적으로 처리할 필요 없음


여러 개의 비동기 처리를 병렬적으로 처리할 때

const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000))
const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000))
const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000))

Promise.all([requestData1(), requestData2(), requestData3()])
  .then(data => console.log(data)) // [ 1, 2, 3 ] => 3초
  .catch(err => console.log(err))

→ 가장 늦게 fulfilled 상태가 되는 첫 번째 프로미스의 처리 시간인 3초 가량 걸림
→ 모든 프로미스가 fulfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스 반환.
→ 인수로 전달받은 배열의 프로미스가 하나라도 rejected되면 즉시 에러 띄우고 종료


Promise.all 쓰는 예제

const promiseAll = url => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.send()
    
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.response))
      } else {
        reject(new Error(xhr.status))
      }
    }
  })
}

const githubIds = ['jeresig', 'ahejlsberg', 'ungmo2']

// 순차적으로 실행될 필요가 없는 로직.
Promise.all(githubIds.map(id => promiseAll(`https://api.github.com/users/${id}`)))
  .then(users => users.map(user => user.name))
  .then(name => console.log(name)) // [ 'John Resig', 'Anders Hejlsberg', 'Ungmo Lee' ]
  .catch(err => console.log(err))

→ 깃헙 아이디로 깃헙 사용자 이름을 따는 비동기 처리를 순차적으로 처리될 필요가 없음.

2.3. Promise.race

Promise.race도 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달받음. 가장 먼저 fulfilled된 프로미스의 처리 결과만 resolve함

const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000))
const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000))
const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000))

Promise.race([requestData1(), requestData2(), requestData3()])
  .then(res => console.log(res)) // 3
  .catch(err => console.log(err))

→ 다 fulilled 상태가 되기를 기다리는 게 아니라 하나만 fulfilled 되면 종료
→ 하나라도 reject되면 에러 띄우고 종료 (Promise.all이랑 같음)

2.4. Promise.allSettled

Promise.allSettled도 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 받음. 그리고 각각의 비동기 처리가 settled된 상태(fulfilled 또는 rejected)를 배열로 반환함

Promise.allSettled([
  new Promise(resolve => setTimeout(() => resolve(1), 2000)),
  new Promise((_, reject) => setTimeout(() => reject(new Error('Error!!!')), 1000))
]).then(data => console.log(data))

// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: Error: 'Error!!!' }
// ]

→ 각각의 프로미스의 처리 결과가 객체로 나타남.
→ fulfilled 상태인 경우 비동기 처리 상태를 나타내는 status 프로퍼티와 처리 결과를 나타내는 value 프로퍼티 가짐
→ rejected 상태인 경우 비동기 처리 상태를 나타내는 status 프로퍼티와 에러를 나타내는 reason 프로퍼티 가짐

profile
빵굽는 프론트엔드 개발자

0개의 댓글