JS | Promise

김보훈·2021년 9월 1일
0

JavaScript

목록 보기
9/16
post-thumbnail
post-custom-banner

Promise

비동기를 간편하게 처리할 수 있도록 도와주는 자바스크립트에 내장되어 있는 object 이다.

콜백 함수를 사용하면 가독성이 나쁘고 비동기 처리 중 발생한 에러의 처리가 곤라한 일이 발생한다.

es6에서 콜백의 문제점을 보완하고자 도입한 것이 promise 이다. 프로미스는 비동기 이면서 비동기 처리 시점을 명확하게 표현할 수 있다.

문법

let promise = new Promise(function (resove, reject) {
  // executor (제작 코드 , '가수')
})
  • executor(실행자, 실행 함수) : new Promise 에 전달되는 함수,
    new Promise 가 만들어질 때 자동으로 실행되고 결과를 최종적으로 만들어내는 제작 코드를 포함한다.

executor 는 자동으로 실행되는데 원하는 일을 처리되면 executor는 처리 성공 여부에 따라 resolve , reject 를 호출한다.

  • resolve(value) : 일이 성공적으로 끝난 경우 그 결과를 나타내는 value 와 함께 호출
  • reject(error) : 에러 발생 시 에러 객체를 나타내는 error와 함께 호출

주의점 - executors 는 자동으로 처리되기 때문에 자동으로 처리하면 안되는 일의 코드를 넣어버리면 불필요한 통신이 일어날 수 있다.


new Promise 생성자가 반환하는 promise 객체는 다음과 같은 내부 프로퍼티를 갖는다.

  • state — 처음엔 "pending"(보류)이었다 resolve가 호출되면"fulfilled",
    reject가 호출되면 "rejected"로 변한다.

  • result — 처음엔 undefined이었다 resolve(value)가 호출되면 value로, reject(error)가 호출되면 error로 변한다.

then , catch , finally

생성된 Promise 객체를 사용하기 위해서는 .then , .catch , .finally 메소드를 사용하여 연결시켜야한다. 소비자,소비함수 라고도 한다.

then

promise.then(
  function(result) { /* 결과(result)를 다룹니다 */ },
  function(error) { /* 에러(error)를 다룹니다 */ }
);
  • .then의 첫 번째 인수 : 프라미스가 이행되었을 때 실행되는 함수이고, 여기서 실행 결과를 받는다.

  • .then의 두 번째 인수 : 프라미스가 거부되었을 때 실행되는 함수이고, 여기서 에러를 받는다.

catch

에러가 발생한 경우만 다루고 싶다면 .then(null, errorHandlingFunction) 같이 null을 첫 번째 인수로 전달하면 됩니다. .catch(errorHandlingFunction)를 써도 된다.

finally

프라미스가 처리되면 항상 어떤 함수를 실행하고 싶을때 finally를 사용한다.
쓸모가 없어진 로딩 인디케이터(loading indicator)를 멈추는 경우같이, 결과가 어떻든 마무리가 필요하면 finally가 유용하다.

사용법

new Promise((resolve, reject) => {
  /* 시간이 걸리는 어떤 일을 수행하고, 그 후 resolve, reject를 호출함 */
})
  // 성공·실패 여부와 상관없이 프라미스가 처리되면 실행됨
  .finally(() => 로딩 인디케이터 중지)
  .then(result => result와 err 보여줌 => error 보여줌)

.finally(f) !== .then(f, f)
둘의 차이점은 다음과 같다.
1. finally 핸들러에는 인수가 없다. 프라미스가 이행되었는지 거부되었는지 알 필요가 없기 때문이다.
2. finally 핸들러는 자동으로 다음 핸들러에 결과와 에러를 전달한다. 따라서 finally는 결과를 처리하기위해 만든 것이 아니다. 결과는 finally를 통과해서 전달된다.
3. 함수를 중복해서 쓸 필요가 없기 때문에 .then(f, f) 보다 문법 측면에서 편리하다.

Promise chaining

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

promise chaining 이 가능한 이유는 .then 을 호출하면 promise 가 반환되고, 반환된 promise에도 역시 .then 을 호출 할 수 있다.

핸들러가 값을 반환할 때엔 이 값이 프라미스의 result가 된다. 따라서 다음 .then은 이 값을 이용해 호출된다.

프라미스 반환하기

.then(handler) 에 사용된 핸들러가 프라미스를 생성하거나 반환하는 경우도 있다.

이 경우 이어지는 핸들러는 프라미스(👉)가 처리될 때까지 기다리다가 처리가 완료되면 그 결과를 받는다.

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {

  alert(result); // 1

  👉 return new Promise((resolve, reject) => { // (*)
     setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) { // (**)

  alert(result); // 2

  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) {

  alert(result); // 4

});

프라미스 에러 핸들링

가장 쉬운 에러 처리 방법은 체인 끝에 .catch를 붙이는 것이다.

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise((resolve, reject) => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  }))
  .catch(error => alert(error.message));

정상적인 경우라면 .catch는 절대 트리거 되지 않는다. 그런데 네트워크 문제, 잘못된 형식의 JSON 등으로 인해 위쪽 프라미스 중 하나라도 거부되면 .catch에서 에러를 잡게 된다.

암시적 try...catch

프라미스 executor와 프라미스 핸들러 코드 주위엔 '보이지 않는 try...catch'가 있다. 예외가 발생하면 암시적 try...catch에서 예외를 잡고 이를 reject처럼 다룬다.

new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
}).catch(alert); // Error: 에러 발생!

위는 아래와 동일하게 동작한다.

new Promise((resolve, reject) => {
  reject(new Error("에러 발생!"));
}).catch(alert); // Error: 에러 발생!

핸들러 안에서도 throw를 사용해 에러를 던지면, 이 자체가 거부된 프라미스를 의미하게 된다. 따라서 제어 흐름이 가장 가까운 에러 핸들러로 넘어간다.

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  throw new Error("에러 발생!"); // 프라미스가 거부됨
}).catch(alert); // Error: 에러 발생!

throw문으로 만든 에러가 아니라도 모든 종류의 에러를 처리할 수 있다. 핸들러 위쪽에서 비정상적으로 발생한 에러 또한 잡는다.

다시 던지기

체인 마지막의 .catchtry..catch와 유사한 역할을 합니다. .then 핸들러를 원하는 만큼 사용하다 마지막에 .catch 하나만 붙이면 .then 핸들러에서 발생한 모든 에러를 처리할 수 있습니다.

// 실행 순서: catch -> then
new Promise((resolve, reject) => {

  throw new Error("에러 발생!");

}).catch(function(error) {

  alert("에러가 잘 처리되었습니다. 정상적으로 실행이 이어집니다.");

}).then(() => alert("다음 핸들러가 실행됩니다."));

처리되지 못한 거부

에러를 처리하지 못하는 상황, 즉 체인 끝에 .catch를 추가하지 못하는 경우엔 어떻게 될까? 에러가 발생하면 프라미스는 거부상태가 된다.

new Promise(function() {
  noSuchFunction(); // 에러 (존재하지 않는 함수)
})
  .then(() => {
    // 성공상태의 프라미스를 처리하는 핸들러. 한 개 혹은 여러 개가 있을 수 있음
  }); // 끝에 .catch가 없음!

자바스크립트 엔진은 프라미스 거부를 추적하다가 위와 같은 상황에는 전역 에러를 생성한다. 브라우저 환경에서는 이런 에러를 unhandledrejection 이벤트로 잡을 수 있다.

window.addEventListener('unhandledrejection', function(event) {
  // 이벤트엔 두 개의 특별 프로퍼티가 있습니다.
  alert(event.promise); // [object Promise] - 에러를 생성하는 프라미스
  alert(event.reason); // Error: 에러 발생! - 처리하지 못한 에러 객체
});

new Promise(function() {
  throw new Error("에러 발생!");
}); // 에러 처리 핸들러, catch가 없음

브라우저 환경에서 에러가 발생했는데 .catch가 없으면 unhandledrejection 핸들러가 트리거 된다.

unhandledrejection 핸들러는 에러 정보가 담긴 event 객체를 받기 때문에 이 핸들러 안에서 원하는 작업을 할 수 있다.

Promise.all

모든 프라미스가 이행될 때까지 기다렸다가 그 결과값을 담은 배열을 반환한다. 주어진 프라미스 중 하나라도 실패하면 Promise.all은 거부되고 나머지 프라미스의 결과는 무시된다.

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// expected output: Array [3, 42, "foo"]

Promise.all은 요소 전체가 프라미스인 배열을 받고 새로운 프라미스를 반환한다.

배열 안 프라미스가 모두 처리되면 새로운 프라미스가 이행되는데, 배열 안 프라미스의 결괏값을 담은 배열이 새로운 프라미스의 result가 된다.

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 프라미스 전체가 처리되면 3초뒤에 1, 2, 3이 반환됩니다. 각 프라미스는 배열을 구성하는 요소가 됩니다.

위 코드에서 result는 배열 [1, 2, 3] 이다.

Promise.all의 첫 번째 프라미스는 가장 늦게 이행되지만 처리 결과는 배열의 첫 번째 요소에 저장된다.

🚨유의점

Promise.all에 전달되는 프라미스 중 하나라도 거부되면, Promise.all이 반환하는 프라미스는 에러와 함께 바로 거부된다.

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: 에러 발생!

참고자료
모던 JavaScript 튜토리얼 - promise
mdn - promise

post-custom-banner

0개의 댓글