프론트 공부/웹과 API, 네트워크

promise 객체 공부 기록.

홍구리당당 2023. 11. 20. 21:01

0. 오늘의 배울 것.

promise 객체가 비동기 실행을 하게 해주는 객체임을 배웠다.

오늘은 좀 더 심화 내용을 배워보자.

왜 promise 객체가 필요한지, promise 객체를 직접 만들어볼 수는 없는지 등!!

1. promise 객체는 왜 있는 걸까?

fetch 함수에 그냥 바로 콜백 함수를 인자로 주면 안되는 걸까?

함수에 콜백을 직접 넣는 방식은 콜백 헬 callback hell이라는 문제를 일으킬 수 있다. 지옥의 피라미드라고도 불리는 콜백 헬은 코드 가독성이 겁나 떨어지는 현상을 말한다.

promise 객체를 활용하면 가독성이 좋아지고 여러 비동기 작업을 순차적으로 처리하면서 callback hell 문제를 해결할 수 있다

뿐만 아니라 promise 객체에는 비동기 작업에 관한 세밀한 개념들이 반영되어있다. pending, fulfilled, rejected 상태 등과 then, catch, finally 메소드들이 있어 더욱 정교한 설계가 가능한 것이다!!


(콜백 헬...)


2. 프로미스 객체 만들어보기

이때껏 한 건 fetch 나 then 등 메소드들이 반환하는 promise 객체를 활용했던 것이다. 이제는 객체를 만들어보자!!

아래 코드에서 new 키워드로 프로미스 객체를 생성하고, 그 안에 생성자로 함수를 주었다. 그걸 executor 함수라고 하는데, 이건 객체가 생성될 때 자동으로 실행되는 함수이다.

const p = new Promise((resolve, reject) => {});
  1. resolve 인자는 promise 객체를 fulfilled 상태로 만들어준다.
  2. reject 인자는 promise 객체를 rejected 상태로 만들어준다.

한 번 resolve, reject를 써보자.

// 2초 후에 p는 fulfilled 상태가 되면서 작업성공 결과로 success라는 문자열을 가진다.
const p = new Promise((resolve, reject) => {
  setTimeOut(() => {
    resolve("success");
  }, 2000);
});

p.then((result) => {
  console.log(result);
});

// 이번엔 2초 후에 rejected 객체가 생성된다. 작업 실패 원인에는 new Error('fail') 객체가 들어간다.
const p = new Promise((resolve, reject) => {
  setTimeOut(() => {
    reject(new Error("fail"));
  }, 2000);
});

p.catch((error) => {
  console.log(error);
});

이렇게 객체를 직접 만드는 건 실무에서 자주 쓰이진 않고 promisify 라는 경우에나 좀 사용된다.

promisify 란?

전통적인 형식의 비동기 실행 함수 코드를 promise 기반 코드로 변환하는 것.

  1. setTimeout 함수를 promise 기반으로 바꾸기

예를 들어 다음과 같은 함수가 있다고 하자.

// wait 함수는 milliseconds 만큼 시간이 지난 후에야 text 값을 리턴하는 함수이다.
function wait(text, milliseconds) {
  setTimeout(() => text, milliseconds);
}

위의 wait 함수를 promise chaining 코드에서 사용할 때 바로 then 사이에 함수를 호출하면 제대로 작동되지 않는다. 왜냐면 여기서 setTime함수가 리턴한 text 값을, 함수 wait은 리턴해주지 않기 떄문이다!!

// 2초 후에 response by hongjw 이라 출력되게 하고 싶은데
// 이렇게 하면 제대로 result가 출력되지 않음.

function wait(text, milliseconds) {
  setTimeout(() => text, milliseconds);
}
fetch("url")
  .then((response) => response.text())
  .then((result) => wait(`${result} by hongjw`, 2000))
  .then((result) => {
    console.log(result);
  });

따라서 비동기 함수를 promise chaining 안에서 사용하려면, 나중에 실행되는 부분을 promise chaining에서 사용할 수 없기 때문에 직접 객체를 생성해 return해야 한다.

// 이렇게 쓰자...
function wait(text, milliseconds) {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(text);
    }, 2000);
  });
  return p;
}
fetch("url")
  .then((response) => response.text())
  .then((result) => wait(`${result} by hongjw`, 2000))
  .then((result) => {
    console.log(result);
  });

위의 코드처럼 작성하면 executor 함수 안에서 setTimeout. 함수를 호출하고, 작업이 성공하면 promise 객체는 fulfilled 상태에 작업 성공 결과로 text 값을 가진다.

이렇게 전통적인 형식의 비동기 함수를 promise 객체로 감싸서 그 promise 객체를 리턴하는 형식으로 만드는 작업을 promisify, 프로미스화 한다고 한다!!

  1. node.js 에서 promisify 하기.

여기서는 자칫 callback hell문제가 생길 수 있다. 이걸 막기 위해 promisify를 하는 것이다.

그런데 promisify를 하면 안되는 함수도 있다!!

비동기 실행 함수 중 콜백을 한 번만 실행하는 setTimeOut, readFile 같은 건 되지만, 조건에 따라 콜백을 여러 번 실행하는 setInterval, addEventListener 은 안된다. 왜냐면 한 번 promise 객체가 pendding에서 fulfilled나 rejected 상태가 되면 그 뒤로는 상태나 그 내부 작업 결과물이 바뀌지 않기 때문이다!!

만약 addEventListener로 버튼을 누를 때마다 count 되는 코드를 짰다면, 객체 내부 count 변수 값이 바뀌어야 하는데 이걸 못 한다는 뜻.


3. 이미 상태가 결정된 promise 객체

우리는 promise 객체가 pending 상태일 때만 만들었는데, 처음부터 아예 fulfilled 상태나 rejected 상태인 객체를 만들 수도 있다.

// 이전에 배운 방식
const p = new Promise((resolve, reject) => {
  //코드 내용...
});

// fulfilled 상태 객체 만들기
const p1 = Promise.resolve("success");

// rejected 상태 객체 만들기
const p2 = Promise.reject(new Error("fail"));

이렇게 미리 상태가 정해진 객체들은, 보통 함수 안에서 리턴하는 값이 여러 개인데 모든 리턴 값을 promise 객체로 통일할 경우에 종종 쓴다.

예를 들면 아래 코드와 같다.

function doSomething(a, b) {
  //...
  if (problem) {
    // 이렇게 에러 객체를 바로 throw할 수도 있지만.
    // throw new Error("failed due to ..");
    //
    // 이렇게 promise 객체를 리턴하는 식도 가능!
    return Promise.reject(new Error("failed due to .."));
  } else {
    return fetch("url");
  }
}

하나 짚고 넘어가야 할 점!!

then 메소드는 promise 객체가 pending 상태일 때만 가능하고, 객체가 fulfilled, rejected 상태가 되는 순간 결과를 콜백의 파라미터로 넘어간다고 오해할 수 있다.

그런데 사실 위에서 봤듯이 이미 fulfilled, rejected 상태가 된 promise 객체에서도 then 을 붙이면 작업 결과를 받을 수 있다!!!!


4. 여러 promise 객체를 다루는 방법

앞에서는 promise 객체 한 개씩만 다뤘지만, 사실 여러 개 를 동시에 다룰 수도 있다.

  1. all 메소드 쓰기.

all 메소드는 인자로 배열을 받는다. 그 배열 안의 요소들은 각각 promise 객체이다. all 메소드는 이 배열 내부 객체들이 모두 pending에서 fulfilled 상태가 될 때까지 기다리고, fulfilled 상태가 되면 all 메소드는 인자로 받은 객체들의 작업 성공 결과들을 배열에 넣는다.

그리고 이 all 메소드가 리턴하는 객체도 fulfilled 상태가 되고 작업 성공 결과로는 위에서 만든 배열을 갖는다!!

그래서 all 메소드는 여러 객체의 작업 성공 결과를 모두 한 번에 취합할 때 사용한다.

만약 객체들 중 하나라도 rejected 상태가 되면 all 메소드가 리턴할 promise 객체도 rejected 상태가 된다. 그래서 하나라도 에러가 날 경우를 대비해 try catch 문을 쓴다.

// 1번 직원 정보
const p1 = fetch("https://learn.codeit.kr/api/members/1").then((res) =>
  res.json()
);
// 2번 직원 정보
const p2 = fetch("https://learn.codeit.kr/api/members/2").then((res) =>
  res.json()
);
// 3번 직원 정보
const p3 = fetch("https://learn.codeit.kr/api/members/3").then((res) =>
  res.json()
);

Promise.all([p1, p2, p3])
  .then((results) => {
    console.log(results); // Array : [1번 직원 정보, 2번 직원 정보, 3번 직원 정보]
  })
  .catch((error) => {
    console.log(error);
  });

참고로 axios에서도 all 메소드는 있다. axios 쓰는 법도 알아둬야 하니 기억하자.

 getApi() {
      axios
          .all([
              axios({
                  url: "api1",
                  method: "get",
                  validateStatus: status => {
                      return true;
                  }
              }),
              axios({
                  url: "api2",
                  method: "get",
                  validateStatus: status => {
                      return true;
                  }
              })
          ])
          .then(
              axios.spread((res1, res2) => {
                  if (res1.status === 200 && res2.status === 200) {
                  //성공
                  } else {
//실패
                  }
              })
          )
          //그외의 에러
          .catch(error => console.log(error));
  },
  1. race 메소드 쓰기.

race 메소드도 promise 객체 여러 개가 묶인 배열을 아규먼트로 받는다. 그리고 그 객체들 중 가장 먼저 fulfilled 상태가 되거나 rejected 상태가 된 promise 객체와 동일한 상태, 결과를 가진다!!

아래 코드에서 가장 먼저 상태가 결정되는 p1 객체에 따라 race 메소드가 반환하는 객체의 상태, 값이 달라진다.

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("Success"), 1000);
});
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("fail")), 2000);
});
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("fail2")), 4000);
});

Promise.race([p1, p2, p3])
  .then((result) => {
    console.log(result); // hello 출력
  })
  .catch((value) => {
    console.log(value);
  });
  1. allSettled 메소드도 있다.

allSettled가 객체들이 담긴 배열을 받을 때, 객체들 상태가 하나도 빠짐없이 pendding 상태에서 rejected든 fulfilled든 일단 다 결정이 나면 allSettled의 객체가 그제야 fulfilled 상태가 되고, 작업 성공 결과로는 파라미터로 받은 객체들의 각 status, value(작업 성공 결과), reasone(작업 실패 결과) 를 담은 객체들의 모음 배열을 가진다.

rejected, fulfilled 상태를 묶어서 settled 상태라고 하기 때문에, 뭐로 결정 나든간에 allSettled 라고 불리는 것!

  1. any 메소드도 있다.

객체들 중 가장 먼저 fulfilled 상태가 되는 객체의 상태와 결과를 반환하는데, 만약 아무것도 fulfilled 되지 않고 싹 다 rejected가 된다면 AggregateError 에러를 작업 실패 정보로 갖고 rejected 상태인 객체를 반환한다. 즉, 파라미터로 받은 배열 내 객체 중 하나라도 fulfilled 상태가 되면 된다는 것!