[JavaScript] Promise가 비동기 처리를 하는 방식, 프로미스의 메서드, 에러 처리 방법

이은진·2021년 3월 9일
1

JavaScript Study

목록 보기
21/24

이전 글에서 자바스크립트의 비동기 처리를 위해 콜백 패턴을 사용하는 경우 콜백 헬이 발생하고 에러 처리가 어려워지는 단점이 발생한다는 것을 살펴보았다. 이를 보완하기 위해 ES6에서 등장한 Promise에 대해 알아보자.

1. Promise가 비동기 처리를 하는 방식

const promise = new Promise((resolve, reject) => {
  if (true) {
    resolve('result')
  } else {
    reject('failure reason')
  }
})

Promise 생성자 함수를 new 연산자와 함께 호출하면 프로미스 객체를 생성한다. Promise 생성자 함수의 인수로 전달된 콜백 함수 내부에서 비동기 처리를 수행한다. 그 콜백 함수의 인수 중에서, resolve 함수는 비동기 처리가 성공했을 때 호출되고, reject 함수는 비동기 처리가 실패했을 때 호출된다. 이전 글에서 콜백 패턴으로 구현해 본 비동기 함수 get을 프로미스를 사용해 다시 구현해 보면 다음과 같다.

// GET 요청을 위한 비동기 함수
const promiseGet = url => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.send()
    
    xhr.onload = () => {
      if (xhr.status === 200) {
        // 성공적으로 응답을 전달받으면 resolve 함수를 호출한다
        resolve(JSON.parse(xhr.response))
      } else {
        // 에러 처리를 위해 reject 함수를 호출한다.
        reject(new Error(xhr.status))
      }
    }
  })
}

promiseGet('https://jsonplaceholder.typicode.com/posts/1')

똑같이 XMLHttpRequest 객체를 생성해서 이벤트 핸들러에 이벤트를 등록해서, 상태가 200일 경우 전달 받은 응답에 대하여 비동기 처리를 하는 함수다. 그러나 함수 내부에서 프로미스 객체를 생성해서 비동기 처리가 성공할 때와 실패할 때의 경우 모두 처리하고 그 값을 반환하기 때문에 콜백 패턴에서의 단점을 더 이상 찾아볼 수 없다.

그렇게 반환된 프로미스는 비동기 처리가 어떻게 진행되고 있는지를 나타내고 있는 비동기 처리 상태 정보 / PromiseStatus를 가진다. 프로미스가 생성된 직후에는 기본적으로 pending 상태다. 이후 비동기 처리가 수행되면 그 결과에 따라 프로미스의 상태가 변경된다. 비동기 처리가 성공할 경우, resolve 함수를 호출해 프로미스를 fulfilled 상태로 변경하고, 비동기 처리가 실패할 경우 reject 함수를 호출해 프로미스를 rejected 상태로 변경한다. fulfilled 또는 rejected 상태처럼 이미 비동기 처리가 수행된 상태는 settled 상태로, 이후에는 다른 상태로 변하지 않는다.

또 비동기 처리가 수행된 프로미스는 비동기 처리 결과 정보 / PromiseValue를 갖는다. 콘솔에 resolved된 값과 rejected된 값을 찍어 보면 처리 결과를 알 수 있다. 비동기 처리가 성공하는 경우, resolve 함수의 결과값을 갖고, 실패하는 경우 Error 객체를 값으로 갖는다. 즉 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체다.

2. 프로미스의 후속 처리 메서드

콜백 패턴으로 비동기 처리를 할 때, 후속 처리를 위해 콜백함수를 중첩하였다. 프로미스에서는 fulfilled 상태 또는 rejected 상태가 되면 프로미스의 처리 결과 또는 에러를 가지고 후속 처리를 한다. 프로미스는 후속 메서드 then, catch, finally를 제공한다. 프로미스의 비동기 처리 상태가 변화하면, 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출된다. 그리고 그 후속 처리 메서드의 콜백 함수로 프로미스의 처리 결과가 인수로 전달된다. 후속 처리 메서드 3가지를 차례로 알아보자.

2.1. Promise.prototype.then

기본적으로 then은 두 개의 콜백 함수를 인수로 전달받는다. 첫 번째는 fulfilled 상태에서 호출하고, 두 번째는 rejected 상태에서 호출하는 실패 처리 함수다. then 메서드는 언제나 프로미스를 반환한다. 만약 then 메서드의 콜백 함수가 프로미스를 반환하면 그 프로미스를 그대로 반환하고, 프로미스가 아닌 다른 값을 반환하면 그 값을 resolve 또는 reject하여 프로미스를 생성해 반환한다.

fulfilled 상태에서 호출하는 경우

new Promise(resolve => resolve('fulfilled'))
	.then(v => console.log(v), e => console.log(error)) // fulfilled

→ fulfilled 상태에서 비동기 처리 결과를 인수로 받아 호출한다.

rejected 상태에서 호출하는 경우

new Promise((_, reject) => reject('rejected'))
	.then(v => console.log(v), e => console.log(e)) // Error: rejected

→ rejected 상태에서 에러를 인수로 받아 호출한다.

2.2. Promise.prototype.catch

catch는 한 개의 콜백 함수를 인수로 전달받는다. 이는 프로미스가 rejected 상태에서 호출할 함수다. 앞서 살펴본 then 메서드의 두 번째 인수에 rejected 상태에서 호출하는 함수가 들어간다고 하였는데, catch 메서드가 그와 동일한 역할을 한다. 즉 아래 두 코드의 결과는 (아래의 경우에) 같다.

new Promise((_, reject) => reject(new Error('rejected')))
	.catch(e => console.log(e)) // Error: rejected
new Promise((_, reject) => reject(new Error('rejected')))
	.then(undefined, e => console.log(e)) // Error: rejected

2.3. Promise.prototype.finally

finally 메서드 또한 한 개의 콜백 함수를 인수로 전달받는다. 프로미스가 resolve, rejected 되는지 여부에 상관 없이 무조건 공통적으로 호출할 함수가 있다면 finally 메서드를 사용한다.

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

promiseFinally('https://jsonplaceholder.typicode.com/posts/1')
  .then(res => console.log(res))
  .catch(err => console.log(err))
  .finally(() => console.log('Bye!'))

// Error: 'Failed to execute 'open' on 'XMLHttpRequest': 2 arguments required, but only 0 present.'
// 'Bye!'

promiseFinally 함수의 인자로 api를 넣어보았더니, 에러가 발생하여 catch 메서드에서 걸려 콘솔에 에러가 찍혔다. 그리고 finally 메서드에서도 콜백 함수가 호출되어 콘솔에 Bye!가 찍혔다.

3. 프로미스의 에러 처리

그렇다면 then의 두 번째 인수로 전달된 에러 처리를 위한 콜백 함수와 catch 메서드의 콜백 함수는 실제 기능에 아무런 차이가 없을까? 이 부분은 에러가 어느 부분에서 발생했는지에 따라 결과가 다르다.

3.1. 비동기 처리 중에 에러가 발생한 경우

const wrongUrl = 'https://jsonplaceholder.typicode.com/xxx/1'

promiseGet(wrongUrl).then(
  res => console.log(res),
  err => console.log(err)
) // Error: 404

promiseGet(wrongUrl)
  .then(res => console.log(res))
  .catch(err => console.log(err)) // Error: 404

두 코드에서 모두 똑같이 Error: 404가 콘솔에 찍힌다. 잘못된 링크 주소에 요청했을 때의 에러이므로 비동기 처리 중에 에러가 발생한 것이고, 이 경우 then 메서드의 두 번째 콜백 함수로 에러를 처리했을 때와 catch로 에러를 처리했을 때의 결과가 같다는 것을 알 수 있다. catch 메서드를 호출하면 내부적으로 then(undefined, onRejected)을 호출하기 때문에, 내부적으로 다음 코드와 같이 처리된다.

promiseGet(wrongUrl)
  .then(res => console.log(res))
  .then(undefined, err => console.log(err)) // Error: 404

3.2. then의 콜백 함수에서 에러가 발생한 경우

promiseGet('https://jsonplaceholder.typicode.com/todos/1').then(
  res => console.xxx(res),
  err => console.log(err)
) // 첫번째 콜백 함수에서 발생한 에러 캐치 x

promiseGet('https://jsonplaceholder.typicode.com/todos/1')
  .then(res => console.xxx(res))
  .catch(err => console.log(err)) // TypeError: console.xxx is not a function

then 메서드의 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못한다. 그와 반대로 catch 메서드를 모든 then 메서드를 호출한 이후에 호출하면, 비동기 처리에서 rejected 상태를 처리할 수 있을 뿐만 아니라, then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다. 또한 then 메서드에 두 번째 콜백 함수를 전달하는 것보다 catch를 사용하는 것이 코드의 가독성을 높여주기 때문에, 가급적 에러 처리는 catch 메서드를 이용하는 것이 권장된다.

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

0개의 댓글