비동기(asynchronous)- 3 (Promise)

태로샐러드·2021년 11월 8일

javascript 비동기

목록 보기
3/4
post-thumbnail

Callback이라는 강력한 수단을 통해 비동기적인 함수의 순서를 제어할 수 있었다.
하지만 Callback hell 이라는 치명적인 단점이 발생하기도 했다.

이를 위해 고안된 것이 바로 Promise이다.

🍫 Promise 란?

  • javascript 안에 내장되어 있는 Class 이다.
  • Callback 대신에 비동기적인 함수 제어를 위해 유용하게 쓰인다.
  • 특정 기능을 수행시켰을 때, '이게 성공하면 이렇게 해줘' '이게 실패하면 이렇게 해줘' 등의 기능을 심어놓을 수 있는 성공 실패 처리(판독)기라고 생각하면 이해하기 쉽다.
  • 아직은 이게 무슨 말인지 이해가 안갈것이다. 다만 그전에, Promise는 그저 Callback을 조금 더 세련되게 표현할 수 있는 코드 디자인 패턴 중 하나라고 생각하고 차근차근 접근해보자.

🍫 Promise 객체 생성

Class이기 때문에 new라는 키워드를 통해 객체 인스턴스를 생성할 수 있다.
기본 생김새는 다음과 같다.

const promise = new Promise((resolve, reject) => {
  // executor 함수 본문
})

Promise 생성자에는 excecutor(실행자)라는 콜백 함수를 전달해줘야한다.
이는 new Promise 가 만들어질때에 자동으로 실행된다.
executor 함수에는 resolve와 reject라는 인자가 담겨있는데, 이는 javascript에서 자체 제공하는 콜백함수다. 이 둘은 성공과 실패를 판독하는 역할을 한다.

  • resolve : 기능을 정상적으로 수행해서 마지막에 그 결과를 나타내는 value와 호출
    (성공판독)
  • reject : 기능을 수행하다가 문제가 생기면 에러 객체를 나타내는 error와 호출
    (실패판독)

중간 요약하자면, new Promise로 선언하면 인자를 하나 받는데 그 인자는 함수다.
그 함수는 executor라고 부르고 이는 Promise가 생성 되면 자동으로 실행된다.
우리는 executor 내에 우리가 원하는 기능을 코드로 작성해놓을 것이다.
그러면 그 코드의 처리의 성공, 실패 여부에 따라 각각 resolve, reject를 호출한다.

🍫 Promise의 3가지 상태

그런데 이 Promise는 3가지 state(상태)를 가진다.

  • pending : 단어 뜻 그대로 보류 중(처리 중)이라는 상태다.(초기 default 상태)
  • fulfilled : resolve, 즉 Promise가 성공 판독 시 fullfilled(충족함)이라는 상태로 변경된다.
  • rejected : reject, 즉 Promise가 실패 판독 시 rejected(거부함)이라는 상태로 변경된다.

+) fullfilled 혹은 rejected 상태의 Promise를 묶어서 settled(처리된) Promise라고 부르기도 한다.

당연한 얘기겠지만, 이 Promise는 pending의 상태로 시작하지만, 결국은 executor함수 내에서 resolve(성공) 혹은 reject(실패) 둘 중 하나를 호출해서 상태를 변경해야한다.
(계속 pending으로 남으면 의미가 없잖아유?)
다만, resolve(성공) 혹은 reject(실패) 처리돼서 변경된 상태는 더 이상 변하지 않는다.

🍫 then, catch, finally

그럼 Promise 객체가 대충 성공, 실패를 판독한다는 건 알겠는데 이걸 어떻게 콜백처럼 활용하느냐? 그게 이제 지금부터 볼 3가지의 대표적인 Promise.prototype.method를 사용함으로써 가능해진다.
위에서 생성한 Promise 객체가 Producer(생산자)의 측면이었다면, 이 메소드들은 그 생산자가 만들어놓은 Promise를 활용하는 Consumer(소비자)의 측면이라고 생각하면 된다.

then

  • Promise 가 정상적으로 처리되어 resolve를 호출하면, resolve에서 전달된 결과를 받아서 다음 원하는 기능(함수)을 실행시키는 메소드.

사용방법은 아래와 같이 단순히 만들어진 Promise뒤에 붙여서 그 뒤에 원하는 기능(함수) 등을 추가하면 된다.)

const promise = new Promise((resolve, reject) => {
  // ...something to do
  if (성공) resolve(value);
  else reject(new Error());
});

promsie.then((value) => {
  consolel.log(value);
})

Promise executor 의 resolve에서 값을 위와 같이 원하는 값을 전달할 수 있고,
then은 그 값을 그대로 전달받아서 새로운 함수를 작성할 수 있다.
다만, 꼭 resolve 혹은 reject에 값을 전달해야하는 것은 아니다.

Catch

  • Promise 가 실패하여 reject를 호출하면, reject 전달된 에러를 받아서 에러를 처리할 기능(함수)를 실행시키는 메소드.

then과 사용하는 법은 같다.

const promise = new Promise((resolve, reject) => {
  // ...something to do
  if (성공) resolve(value);
  else reject(new Error('error'));
});

promsie
  .then((value) => {
    consolel.log(value);
  })
  .catch((err) => {
    console.log(err);
  })

다만 여기서, 우리가 만들어놓은 promise 객체 바로 뒤에 catch를 쓰는 것이 아니라 저렇게 then 뒤에다가 붙이는 것이 이해가 안 갈 것이다.
이는 Promise의 메소드들(then, catch, finally)은 Promise를 리턴하기 때문에 저런식으로 계속 이어붙일 수 있는 것이다.(체이닝이 가능하다)

then은 분명 Promise를 리턴하여 체이닝이 가능하다고 했는데, 아래와 같이 첫번째 then의 콜백에서 value2라는 promise가 아닌 값을 리턴하면 어떻게 되나?

const promise = new Promise((resolve, reject) => {
  // ...something to do
  if (성공) resolve(value);
  else reject(new Error('error'));
});

promsie
  .then((value) => {
    consolel.log(value);
    return value2
  })
  .then((value2) => {
    console.log(value2);
  })

이 경우에는 자동으로 promise가 fullfilled 된 것으로 간주하고, 그 다음 then에 리턴한 값을 인자로 전달시켜준다.

finally

  • Promise의 성공(resolve), 실패(reject) 결과와 상관 없이 무조건 마지막에 호출되는 메소드다.
  • 사용법은 then, catch와 동일하다.
const promise = new Promise((resolve, reject) => {
  // ...something to do
  if (성공) resolve(value);
  else reject(new Error('error'));
});

promsie
  .then((value) => {
    consolel.log(value);
  })
  .catch((err) => {
    console.log(err);
  })
  .finally(() => {
    console.log('finally');
  })

🍫 Promise.all

마지막으로 Promise에서 유용하게 쓰이는 메소드 하나만 살펴보자.

  • Promise가 담겨있는 Array와 같이 순회 가능한(iterable) 것을 인자로 받고, 그 전달받은 모든 Promise들을 병렬로 처리하여 그 결과를 resolve하는 새로운 Promise를 반환한다.

설명이 어려운데 예시를 보면 생각보다 간단하다.
여러 개의 Promise가 있다고 가정하자. 그리고 처리시간도 제각각 다르다.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Hi'), 3000)
});
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Hello'), 1000)
});
const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('안녕'), 2000)
});

이렇게 각각 다른 3개의 Promise가 모두 resolve된 후에 어떤 기능을 실행시키려고 한다고 가정했을 때, 이것을 일일이 체이닝하여 이어붙여 쓰는 것은 비효율적일 것이다.
이럴 때, Promise.all을 쓸 수 있다.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Hi'), 3000)
});
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Hello'), 1000)
});
const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('안녕'), 2000)
});

Promise.all( [promise1, promise2, promise3] )
  .then((value) => console.log(value))

// 3초 뒤 출력 :  ['Hi', 'Hello', '안녕']

위와 같이 3가지의 Promise를 담은 배열을 Promise.all의 인자로 주면 그 것은 Promise를 리턴하고, 각각의 Promise의 resolve값이 담긴 배열을 리턴한다.
다만, 그 배열에 담겨있는 Promise들 중 하나라도 reject된다면 Promise.all이 리턴하는 Promise는 에러와 함께 바로 거부된다.

🍫 마무리

Promise가 뭔가 아직도 낯설고 여전히 찝찝한 구석이 있긴 하지만, 이것도 단순히 Callback을 보완하기 위한 코딩 디자인 패턴이라고 생각하고 익숙해지면 좋을 것 같다.

profile
기획, 개발공부, 그 외 잡다한 여정 기록 (SEMI로)

0개의 댓글