Promise


기본형태

let promise = new Promise(function (resolve, reject) {
executer(제작 코드 ,'가수')
});

new Promise에 전달되는 함수는 executer(실행자, 실행 함수)라고 한다. executer는 new Promise가
만들어 질 때 자동으로 실행. 결과를 최종적으로 만들어내는 제작 코드를 포함.

resolve 와 reject는 자바스크립트에서 자체 제공하는 콜백이기 때문에
resolve와 reject는 신경 쓰지 않고 executer안 코드만 작성하면 된다.

대신 executer에선 상황에 따라 인수로 넘겨준 콜백 중 하나를 반드시 호출해야 한다.

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

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

  • state: 처음엔 "pending"(보류) 이었다 resolve가 호출되면 "fulfilled", reject가 호출되면 "rejected"
  • result: 처음엔 undefined 이었다 resolve(value)가 호출되면 value로, reject(error)가 호출되면 error로 변한다.
let promise = new Promise((resolve, reject) => {
  // 프라미스가 만들어지면 executer는 자동으로 실행

  // 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 '완료'가 된다.
  setTimeout(() => {resolve('완료'), 1000};
});

위 예제에서 알 수 있는 사실은 두 가지다.

  1. executer는 new Promise에 의해 자동으로 그리고 즉각적으로 실행
  2. executer는 resolve와 reject 함수를 받으며, 둘 중 하나는 반드시 호출해야 한다.

executor '처리'가 시작 된 지 1초 후, resolve('완료')가 호출되고 결과가 만들어진다.

Promise {
state: "Pending",
result: undefined,
}

↓ resolve('완료')

Promise {
state: "fulfiled",
result: "완료",
}

성공적으로 처리되었을 때의 프라미스는 'fulfiled promise(야속이 이행된 프라미스)'라고 불린다.

에러와 함께 약속한 작업을 거부하는 경우,
let promise = new Promise(function(resolve, reject) {
1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보냅니다.
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

Promise {
state: "Pending",
result: undefined,
}

↓ reject(Error객체)

Promise {
state: "rejected",
result: Error객체,
}

resolved 혹은 rejected 상태의 프라미스는'처리된(settled)' 프라미스라고 부른다.
반대되는 프라미스는 '대기(pending)' 중인 프라미스.

then, catch, finally

then

가장 중요하고 기본이 되는 메서드

promise.then(
  function (result) {
    /_ 결과(result)를 다룬다._/;
  },
  function (error) {
    /_ 에러(error)를 다룬다._/;
  }
);

첫 번째 인수는 프라미스가 이행되었을 때 실행 되는 함수, 실행 결과를 받는다.
두 번째 인수는 프라미스가 거부되었을 때 실행 되는 함수, 에러를 받는다.

let promise = new Promise(function (resolve, reject) {
  setTimeout(() => resolve("완료!"), 1000);
});

resolve 함수는 .then의 첫 번째 함수(인수)를 실행합니다.

promise.then(
  result => alert(result), 1초 후 "완료!"를 출력
  error => alert(error) 실행되지 않음
);

첫 번째 함수가 실행

거부된 경우 두 번째 함수가 실행

let promise = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

reject 함수는 .then의 두 번째 함수를 실행합니다.

promise.then(
  result => alert(result), 실행되지 않음
  error => alert(error) 1초 후 "Error: 에러 발생!"을 출력
);

catch

에러만 발생한 상황을 다루고 싶다면 .then(null, errorHandlingFn)와 같이 첫 번째 인수에 null을
전달하면 된다. .catch(errorHandlingFn)을 써도 되는데, then에 null울 전달하는 것과 동일하게 작동한다.

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

.catch(f)는 promise.then(null, f)과 동일하게 작동합니다
promise.catch(alert); 1초 뒤 "Error

finally

프라미스가 처리되면(이행 또는 거부) 항상 실행된다는 점에서 .then(f, f)과 유사하다.

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

finally는 then(f, f)과 완전히 같지는 않다.

  1. 핸들러에 인수가 없다. finally에선 프라미스가 이행되었는지 거부되었는지 알 수 없다.
    //보편적 동작을 수행하기 때문에 성공, 실패 여부를 몰라도 된다.
  2. finally 핸들러는 자동으로 다음 핸들러에 결과와 에러를 전달한다.

프라미스 체이닝

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;
});
  1. 1초 후 최초 프라미스가 이행됩니다. – (*)
  2. 이후 첫번째 .then 핸들러가 호출됩니다. – (**)
  3. 2에서 반환한 값은 다음 .then 핸들러에 전달됩니다. – (***)
  4. 이런 과정이 계속 이어집니다.
    result가 핸들러 체인을 따라 전달되므로, alert 창엔 1, 2, 4가 순서대로 출력된다.

프라미스 체인이 가능한 이유는 promise.then을 호출하면 프라미스가 반환되기 때문이다.

핸들러가 값을 반환할 때, 이 값이 프라미스의 result가 된다.

  • 프라미스 하나에 .then을 여러 개 추가하는 것은 프라미스 체이닝이 아니다.
let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
  alert(result); 1
  return result \* 2;
});

promise.then(function(result) {
  alert(result); 1
re  turn result \* 2;
});

promise.then(function(result) {
  alert(result); 1
  return result \* 2;
});

위 예시의 핸들러는 result를 순차적으로 전달하지 않고 독립적으로 처리한다.

프라미스 반환하기

.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
});

프라미스 에러 핸들링

프라미스가 거부되면 제어 흐름이 제일 가까운 rejection 핸들러로 넘어가기 때문에 프라미스 체인을 사용하면 에러를 쉽게 처리할 수 있다.

fetch(url) // 거부
  .then((response) => response.json())
  .catch((err) => alert(err)); // TypeError: failed to fetch

.catch는 첫번째 핸들러일 필요는 없고 하나 혹은 여러 개의 .then 뒤에 올 수 있다.

암시적 try..catch

프라미스 executor와 프라미스 핸들러 주위엔 보이지 않는(암시적) try...catch가 있다. 예외가 발생하면 rejcet처럼 다룬다.

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

위 예시는 아래 예시와 똑같이 동작한다.

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

executer함수뿐만 아니라 핸들러 안에서 암시적 try...catch는 스스로 에러를 잡고, 에러를 거부 상태의 프라미스로 변경시킨다.

다시 던지기

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

// 실행 순서: catch -> then
new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
})
  .catch(function (error) {
    alert("에러가 잘 처리되었습니다. 정상적으로 실행이 이어집니다.");
  })
  .then(() => alert("다음 핸들러가 실행됩니다."));

.catch 블록이 정상적으로 종료되었기 때문에 다음 성공 핸들러 .then이 호출된다.

또 다른 예시로 (*)로 표시한 핸들러에서 에러를 잡는데, 여기서는 에러를 처리하지 못하기 때문에 에러를 다시 던진다.

// 실행 순서: catch -> catch
new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
})
  .catch(function (error) {
    // (*)

    if (error instanceof URIError) {
      // 에러 처리
    } else {
      alert("처리할 수 없는 에러");

      throw error; // 에러 다시 던지기
    }
  })
  .then(function () {
    /* 여기는 실행되지 않습니다. */
  })
  .catch((error) => {
    // (**)

    alert(`알 수 없는 에러가 발생함: ${error}`);
    // 반환값이 없음 => 실행이 계속됨
  });

처리되지 못한 거부

아래 예시처럼 에러가 발생했는데 .catch문을 추가하지 않아 에러를 처리하지 못하면 무슨 일이 생길까?

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

에러가 발생하면 프라미스는 거부상태가 되고, 실행 흐름은 가장 가까운 rejection 핸들러로 넘어가게 된다. 그런데 위 예시에는 에러를 처리해줄 핸들러가 없기 때문에 에러가 갇혀버린다.


setTimeout 에서의 에러

아래 예시에서 .catch가 트리거될까?

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

.catch는 트리거 되지 않는다.

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

'암시적 try..catch'가 함수 코드를 감싸고 있으므로 모든 동기적 에러는 '암시적 try..catch'에서 처리된다. 하지만 여기에서 에러는 executor(실행자, 실행 함수)가 실행되는 동안이 아니라 나중에 발생한다.

프라미스 API

Pomise.all

복수의 URL에 동시에 요청을 보내고, 모두 완료된 후에 처리할 때 Promise.all을 사용할 수 있다.

let promise = Promise.all([...promises]);

Promise.all은 요소 전체가 프라미스인 배열(이터러블 객체이지만, 대개는 배열)을 받고 새로운 프라미스를 반환한다.

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

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000),
  new Promise(resolve => setTimeout(() => resolve(2), 2000),
  new Promise(resolve => setTimeout(() => resolve(3), 1000),         
]).then(alert); // [1, 2, 3]

배열 result의 요소 순서는 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: 에러 발생!

이행된 다른 프라미스의 결과는 무시된다.

프라미스에는 '취소'라는 개념이 없어서 Promise.all도 프라미스를 취소하지 않는다. AbortController를 사용하면 프라미스 취소가 가능하긴 하지만, 프라미스API는 아니다.

💡 이터러블 객체가 아닌 일반값도 Promise.all(iterable)에 넘길 수 있다.
Promise.all(...)은 대개 프라미스가 요소인 이터러블 객체를 받지만, 요소가 프라미스가 아닌 객체일 경우엔 요소 그대로 결과 배열에 전달된다.

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3
]).then(alert); // 1, 2, 3

Promise.allSettled

Promise.allSettled는 모든 프라미스가 처리될 때까지 기다린다.

  • 응답이 성공할 경우: {status: "fulflled", value: result}
  • 에러가 발생한 경우: {status: "rejected", reason: error}

여러 요청 중 하나가 실패해도 다른 요청의 경과는 여전히 필요한 경우 사용할 수 있다.

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { //(*)
    results.forEach((result, num) => {
      if(result.status === "fulfilled") {
      	alert(`${url[num]}: ${result.value.status}`);
      }
      if(result.status === "rejected") {
        alert(`${url[num]}: ${result.reason}`);
      }
    });
  });

(*)로 표시한 줄의 results

[
  {status: 'fulfilled', value: ...응답...},
  {status: 'fulfilled', value: ...응답...},
  {status: 'rejected', reason: ...에러 객체...}
]

Promise.allSettled를 사용하면 각 프라미스의 상태와 값 또는 에러를 받을 수 있다.

Promise.race

Promise.racePromise.all과 비슷하다. 다만 가장 먼저 처리되는 프라미스의 결과 혹은 에러를 반환한다.

let promise = Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000),
  new Promise((resolve, reject) => setTimeout(() => rejected(new Error("에러 발생!")), 2000),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000),
]).then(alert) // 1

첫 번째 프라미스가 가장 빨리 처리상태가 되기 때문에 첫 번째 프라미스의 결과가 result값이 된다. 이렇게 Promise.race를 사용하면 '경주(race)의 승자'가 나타난 순간 다른 프라미스의 결과 또는 에러는 무시된다.

프라미스화

[출처]
https://ko.javascript.info/promise-basics
https://ko.javascript.info/promise-chaining
https://ko.javascript.info/promise-error-handling
https://ko.javascript.info/promise-api

0개의 댓글