TIL 86 | Promise Basics & Chaining

meow·2020년 12월 9일
1

JavaScript

목록 보기
34/46

코어 자바스크립트 - 프라미스와 async, await

promise 문법

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

executor (실행자, 실행 함수) : new Promise에 전달되는 함수. 결과를 최종적으로 만들어내는 제작코드를 포함한다.

executor에서는 결과를 즉시 얻든, 늦게 얻든 처리 성공 여부에 따라 인수로 넘겨준 콜백 중 하나를 반드시 호출해야 한다.

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

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

  • state : 처음엔 'pending'이었다가 resolve가 호출되면 'fulfilled', reject가 호출되면 'rejected'로 변한다.
  • result : 처음엔 undefined이었다, resolve(value)가 호출되면 value로, reject(error)가 호출되면 error로 변한다.

then, catch, finally

프라미스 객체는 executor와 결과나 에러를 받을 소비함수를 이어주는 역할을 한다. 소비함수는 .then, .catch, .finally 메서드를 사용해 등록(구독)된다.

then

promise.then(
  function(result) { /* 결과(result)를 다룹니다 */ },
  function(error) { /* 에러(error)를 다룹니다 */ }
);

.then의 첫 번째 인수는 프라미스가 이행되었을 때 실행되는 함수이고, 여기서 실행 결과를 받는다.

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

catch

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

.catch(f) === .then(null, f)

finally

프라미스가 처리되면 항상 어떤 함수를 실행하고 싶을때 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) 보다 문법 측면에서 편리하다.

프라미스 체이닝

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.then을 호출하면 프라미스가 반환되기 때문이다. 프라미스가 반환되기 때문에 .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

});

fetch와 체이닝 함께 응용하기

프론트 단에서 네트워크 요청시 자주 사용한 프라미스!

function loadJson(url) {
  return fetch(url)
    .then(response => response.json());
}

function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`)
    .then(response => response.json());
}

function showAvatar(githubUser) {
  return new Promise(function(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);
  });
}

// 함수를 이용하여 다시 동일 작업 수행
loadJson('/article/promise-chaining/user.json')
  .then(user => loadGithubUser(user.name))
  .then(showAvatar)
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));
  // ...

프라미스 에러 핸들링

fetch('https://no-such-server.blabla') // 거부
  .then(response => response.json())
  .catch(err => alert(err)) // TypeError: failed to fetch (출력되는 내용은 다를 수 있음)

가장 쉬운 에러 처리 방법은 체인 끝에 .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));

암시적 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문으로 만든 에러가 아니라도 모든 종류의 에러를 처리할 수 있다. 핸들러 위쪽에서 비정상적으로 발생한 에러 또한 잡는다.

다시 던지기

.catch 안에서 throw를 사용하면 제어 흐름이 가장 가까운 곳에 있는 에러 핸들러로 넘어간다. 여기서 에러가 성공적으로 처리되면 가장 가까운 곳에 있는 .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 핸들러가 트리거 된다. 이 핸들러 안에서 원하는 작업을 할 수 있다.

Promise.all(promise)

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

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

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); // 프라미스 전체가 처리되면 1, 2, 3이 반환됩니다. 각 프라미스는 배열을 구성하는 요소가 됩니다.

위 코드에서 result는 배열 [1, 2, 3] 이다. 위 코드에서 알 수 있듯이, 첫 번째 프라미스가 가장 늦게 이행되지만 배열의 첫번째 요소에 그 결과가 저장된다.

  • 전달되는 프라미스 중 하나라도 거부되면, 반환하는 프라미스는 에러와 함께 바로 거부된다. 에러가 발생하면 다른 프라미스는 모두 무시된다.
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: 에러 발생!
profile
🌙`、、`ヽ`ヽ`、、ヽヽ、`、ヽ`ヽ`ヽヽ` ヽ`、`ヽ`、ヽ``、ヽ`ヽ`、ヽヽ`ヽ、ヽ `ヽ、ヽヽ`ヽ`、``ヽ`ヽ、ヽ、ヽ`ヽ`ヽ 、ヽ`ヽ`ヽ、ヽ、ヽ`ヽ`ヽ 、ヽ、ヽ、ヽ``、ヽ`、ヽヽ 🚶‍♀ ヽ``ヽ``、ヽ`、

0개의 댓글