holdanddeepdive/javascript-deep-dive

45장 프로미스

Opened this issue · 0 comments

45장. 프로미스

  • ES6에서 비동기 처리를 위해 도입된 Promise는 전통적인 콜백 패턴의 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.

비동기 처리를 위한 콜백 패턴의 단점

콜백 헬

  • 비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않아도 즉시 종료된다. 즉, 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료되므로 처리 결과를 외부로 반환하거나 상위 스코프 변수에 할당하면 기대한 대로 동작하지 않는다.
    • 비동기 함수가 호출되면 함수 코드를 평가하는 과정에서 함수의 실행 컨텍스트가 생성되고 실행 컨텍스트 스택(콜 스택)에 푸시된다.
    • 비동기 함수가 종료하면 함수 실행 컨텍스트가 콜스택에서 팝된다.
    • 이벤트 핸들러는 이벤트가 발생하면 일단 태스크 큐에 저장되어 대기하다가, 콜 스택이 비면 이벤트 루프에 의해 콜 스택으로 푸시되어 실행된다.
  • callback hell: 콜백 함수 호출이 중첩되어 복잡도가 높아지는 현상

에러 처리의 한계

  • 콜백 패턴은 에러 처리가 곤란하다.
try {
  setTimeout(() => { throw new Error('Error!')}, 1000)
} catch (e) {
  console.log(e) // 에러 캐치 불가
}
  • setTimeout이 호출되면 함수의 실행 컨텍스트가 생성되어 콜 스택에 푸시되어 실행되는데 비동기 함수이므로 즉시 종료되어 콜 스택에서 제거된다.
  • 이후 타이머가 만료되면 콜백함수는 태스크 큐로 푸시되고 콜 스택이 비었을 때 이벤트 루프에 의해 콜 스택으로 푸시되어 실행된다.
  • 콜백 함수가 실행될 때 setTimeout 함수는 이미 콜 스택에서 제거되었으므로 콜백 함수의 호출자가 setTimeout 함수가 아니다. 에러는 호출자 방향으로 전파되므로 catch에서 잡히지 않는다.

프로미스의 생성

  • Promise 생성자 함수를 new 연산자와 함께 호출하면 Promise 객체를 생성한다. Promise 생성자 함수는 resolvereject 함수를 인수로 전달받는 콜백 함수를 인수로 받는다.
const promise = new Promise((resolve, reject) => {
  if ('성공') {
    resolve('result');
  } else {
    reject('fail')
  }
})

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(JSON.parse(xhr.response));
      } else {
        reject(new Error(xhr.status));
      }
    };
  });
};
  • Promise는 비동기 처리의 진행 상태 정보를 갖는다.
    • pending: 비동기 처리가 아직 수행되지 않은 상태 <-> seettled
    • fulfilled: 비동기 처리가 수행된 상태(성공), resolve 함수가 호출되어야 함
    • rejected: 비동기 처리가 수행된 상태(실패), reject 함수가 호출되어야 함
  • Promise는 비동기 처리 상태와 처리 결과를 관리하는 객체다.

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

  • 프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출된다. 이때 후속 처리 메서드(then, catch, finally)의 콜백 함수에 프로미스의 처리 결과가 인수로 전달된다.

Promise.prototype.then

  • 첫 번째 콜백 함수는 fulfilled 상태가 되면 호출되며 비동기 처리 결과를 인수로 전달받는다. (성공 처리 콜백 함수)
  • 두 번째 콜백 함수는 rejected 상태가 되면 호출되며 프로미스의 에러를 인수로 전달받는다. (실패 처리 콜백 함수)
  • then 메서드는 언제나 프로미스를 반환한다.

Promise.prototype.catch

  • catch 메서드는 한 개의 콜백 함수를 인수로 전달받는다. 프로미스가 rejected 상태인 경우만 호출된다.

Promise.prototype.finally

  • 프로미스 성공 여부와 상관없이 무조건 한 번 호출된다.
promiseGet('url')
  .then(res => console.log(res))
  .catch(err => console.log(err))
  .finally(() => console.log('bye'))

프로미스의 에러 처리

  • then 메서드에 두 번째 콜백 함수를 전달하는 것보다 catch 메서드를 사용하는 것이 더 가독성이 좋고 명확하다.

프로미스 체이닝

  • then, catch, finally 후속 처리 메서드는 콜백 함수가 반환한 프로미스를 반환하므로 연속적으로 호출할 수 있는데 이를 프로미스 체이닝이라 한다.
  • 프로미스는 체이닝을 통해 비동기 처리 결과를 전달받아 후속 처리를 하므로 비동기 처리를 위한 콜백 패턴에서 발생하던 콜백 헬이 발생하지 않는다. 다만 프로미스도 콜백 패턴을 사용하므로 콜백 함수를 사용하지 않는 것은 아니다.
  • 콜백 패턴은 가독성이 좋지 않은데 이 문제는 async/await을 통해 해결할 수 있다.

프로미스의 정적 메서드

  • Promise는 객체이므로 메서드를 가질 수 있다.

Promise.resolve / Promise.reject

  • 이미 존재하는 값을 래핑하여 프로미스를 생성하기 위한 용도
    • 인수로 전달받은 값을 resolve/reject하는 프로미스를 생성
const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [1, 2, 3]

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

Promise.all

  • 여러 개의 비동기 처리를 모두 병렬 처리할 때 사용
  • Promise.all 메서드는 인수로 전달받은 배열의 모든 프로미스가 모두 fulfilled 상태가 되면 종료한다.
    • Promise.all 메서드가 종료하는 데 걸리는 시간은 가장 늦게 fulfilled 상태가 되는 프로미스의 처리 시간보다 조금 더 길다.
  • 모든 프로미스가 fulfilled 상태가 되면 resolve된 처리 결과를 모두 배열에 저장해 새로운 프로미스를 반환한다. 처리 수순서의 보장
  • 하나라도 rejected 상태가 되면 나머지 프로미스를 기다리지 않고 즉시 종료한다.

Promise.race

  • 가장 먼저 fulfilled 상태가 된 프로미스의 처리 결과를 resolve하는 새로운 프로미스를 반환하는 메서드
  • 메서드에 전달된 프로미스가 하나라도 rejected 상태가 되면 에러를 reject하는 새로운 프로미스를 즉시 반환한다.

Promise.allSettled

  • 전달받은 프로미스가 모두 settled(fulfilled or rejected) 상태가 되면 처리 결과를 배열로 반환한다.
    • IE를 제외한 대부분의 모던 브라우저에서 지원
    • fulfilled: status && value(처리 결과)
    • rejected: status && reason
Promise.allSettled([
  new Promise(resolve => setTimeout(() => resolve(1), 2000)),
  new Promise((_, reject) => setTimeout(() => reject(new Error('Error!')), 1000))
  ]).then(console.log)

/*
[
  { status: 'fulfilled', value: 1 },
  {
    status: 'rejected',
    reason: Error: Error!
        at Timeout._onTimeout (/runtime/javascript/3y68wmfcp/HelloWorld.js:3:54)
        at listOnTimeout (internal/timers.js:531:17)
        at processTimers (internal/timers.js:475:7)
  }
]
*/

마이크로태스크 큐

  • 프로미스의 후속 처리 메서드의 콜백 함수는 태스크 큐가 아닌 별도의 큐인 마이크로태스크 큐에 저장된다.
    • 마이크로태스크 큐에는 프로미스의 후속 처리 메서드의 콜백 함수가 일시 저장
    • 비동기 함수의 콜백 함수, 이벤트 핸들러는 태스크 큐에 일시 저장
  • 마이크로태스크 큐는 태스크 큐보다 우선순위가 높다.
    • 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에 있는 함수를 가져와 실행하고 마이크로태스트 큐가 비면 태스크 큐에서 대기하고 있는 함수를 가져와 실행한다.

fetch

  • HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API
  • XMLHttpRequest 객체보다 사용법이 간단하고 프로미스를 지원
    • HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환하므로 후속 처리 메서드 then을 통해 프로미스가 resolve한 Response 객체를 전달받을 수 있다.
  • fetch가 반환하는 프로미스는 기본적으로 HTTP 에러가 발생해도 에러를 reject하지 않고 ok 상태를 false로 설정한 Response 객체를 resolve한다.
    • 오프라인 네트워크 장애나 CORS 에러 등일 때만 reject
  • 참고로 axios는 모든 HTTP 에러를 reject하는 프로미스를 반환한다.