FE (Front End) (구)/javascript

중급 기본 정리 (with 앙마코딩) Promise 프로미스 async, await

B_Tae 2022. 3. 19. 23:16

이 글은 인프런에서 앙마코딩님의 무료 강좌를 학습한 내용입니다. (자바스크립트 중급 강좌)
문제 시 바로 삭제하겠습니다.

출처 [무료] 자바스크립트 중급 강좌 대시보드 - 인프런 | 강의 (inflearn.com)

Promise 프로미스

강의에서 들은 예시가 적절해 보여 같은 예를 들어본다.
우리가 어떤 상품을 주문했다. 그 상품이 완성됐는지 알아보는 원초적인 방법은 주기적으로 연락을 취하는 것이다.
생각만 해도 비효율적이다.
그래서 대부분의 사람들은 연락처를 남겨 업체들이 완성이 됐거나, 어떤 오류로 인해 완성하지 못했을 경우 구매자에게 연락을 준다.
이 과정을 프로미스라고 생각한다.


const pr = new Promise((resolve, reject) => {
    //code
});

프로미스에 기본 구문이다. 여기서 함수는 인수를 받는데 resolve는 성공했을 때, reject는 실패했을 때 받는다.
이렇게 어떤 일이 완료됐을 때 (성공이든 실패든) 받는 인수를 콜백함수라고 한다.


new Promis가 반환하는 객체 프로퍼티는 state와 result가 있다. 예시 구문을 통해 과정을 알아보면

const pr = new Promise((resolve, reject) => {
    setTimeout(() => { 
      resolve('ok') // 성공
    }, 3000)
});

기본 상태, 즉 함수가 선언됐을 때에는
state : pending(대기)
result : undefined
상태로 있는다.


그 후 3초 이후에 실행되는 함수를 받게되면 (성공했다)
state : fulfilled (이행됨)
result:'ok'
상태로 변하게 된다.


실패했을 경우는? 위 예제는 성공만(resolve)만 있기 때문에 코드를 변경해보면

const pr = new Promise((resolve, reject) => {
    setTimeout(() => { 
      reject(new Error('error') // 실패
    }, 3000)
});

이런 코드가 될 수 있고 3초 후에는
state : rejected(거부됨)
result: error
상태로 변하게 된다.


지금까지는 생성자 입장에서 함수라 생각한다면 소비자(사용자)에 함수를 알아보자.

const pr = new Promise((resolve, reject) => {
    setTimeout(() => { 
      resolve('ok') // 성공
    }, 3000)
});

pr.then(
  function(result){},
  function(err){}
);

사용할 때는 then을 붙이면 된다. 첫번째 함수는 이행 되었을 때 실행되는 함수이고 이 예제에서는 result값에 'ok'가 들어가게 된다.
두번째 함수에는 실패했을 때 실행되는 함수이고 이 예제에서는 해당 값은 없다.


then이외에 사용할 수 있는 것이 있다. 바로 catchfinally이다.

catch는 이행되지 않은 함수를 묶는데 사용한다.

//위 pr.then 코드를 이렇게도 사용할 수 있다.
pr.then(
  function(result){}
  ).catch(
  function(err){}
  );

둘다 똑같이 구현된다. catch를 사용하게 되면 가독성도 좋아지고 첫번 째 함수(result 값을 받는)를 실행할 때 나타나는 에러도 잡을 수 있어 catch를 사용하는 걸 권장한다.

finally는 이행이나 에러가 결과에 상관없이 처리만 되면 실행되는 함수이다.

pr.then(
  function(result){}
).catch(
  function(err){}
).finally(
  function(){}
);

이런 방식으로 사용된다. finally안에 있는 함수는 앞에 실행과 관련 없이 처리만 되면 실행된다.
(로딩화면을 없앨 때 유용하다)



일반 함수 vs 프로미스 차이 비교


총 3번에 주문을 순차적으로 해야 한다면

const f1 = (calback) => {
  setTimeour(function () {
    console.log('1번 주문 완료');
    callback();
  },1000);
};

/*같은 구조를 갖는 f2, f3 함수는 생략*/

f1(function () {
  f2(function() {
    f3(function () {
    });
  });
});

함수 내용을 알아야 이해하기 편하긴 하지만.... 너무 길어질까봐 이렇게 정리해본다.
f1 함수가 먼저 실행되야 하고, 이후에는 f2함수가 실행 그 이후에는 f3 함수가 실행되야 하기 때문에, 각 함수안에 다음 함수를 갖게된다. 이렇게 함수안에 함수선 언하여 콜백 함수가 계속 쌓여가는 것을 콜백 헬/ 콜벡 지옥이라 부른다.


이 코드를 promise로 바꾸면

const f1 = (message) => {
  return new Promise((res, rej) => {
    setTimeout(() => {
      res('1번 주문 완료');
    },1000);
  });
};

const f2 = (message) => {
  console.log(message);
  return new Promise((res, rej) => {
    setTimeout(() => {
      res('2번 주문 완료');
    },1000);
  });
};


/* f3 함수 생략 f2와 구조 동일 */

f1()
  .then((res) => f2(res)) // f1 함수는 promise 함수를 반환하고 res값을 f2 message에 넘기는 과정
  .then((res) => f3(res)) // 마찬가지로 f3 message에 넘기는 과정 
  .then((res) => console.log(res)); // 더 넘길 함수가 없다.
  .catch(console.log)
  .finally(() => {
    console.log("끝");
  });

여기서는 앞선 예제와 다르게 catch와 finally를 추가했다. 그 외에는 앞선 예제와 기능은 동일하다.


만약 이 예제에서 f2함수에 res가 아닌 rej 즉 에러가 났다고 가정한다면 결과값은 어떻게 바뀔까?
모두가 res값을 받을 때에는 순차적으로 1,2,3 주문을 마치고 마지막 '끝'까지 반환을 한다.
만일 f2가 rej값을 받는다면, 순차적으로 실행하다(f1부터 실행되다) 에러를 반환하는 함수를 만나게되면 에러를 반환하고 과정을 마치게 된다.(f3 함수가 실행되지 않는다) 당연히 finally로 실행되는 '끝'은 반환된다.


순차적이 아니라 동시에 받고 싶다 ! Promise.all

앞선 예제에서는 setTimeout을 통해 1초 뒤에 실행되도록 했다. f1,f2,f3가 모두 1초 이후에 실행된다면 전체가 실행되는데 3초가 걸리는 것이다.
우리는 이 작업을 순차적이 아닌 동시에 시작하도록 할 수 있다. 즉 총 3초가 아닌 1초에 해결이 가능한 셈이다. (delay 시간이 다를 경우 가장 긴 시간을 갖은 함수를 기준으로 끝난다)


const f1 = (message) => {
  return new Promise((res, rej) => {
    setTimeout(() => {
      res('1번 주문 완료');
    },1000);
  });
};

const f2 = (message) => {
  console.log(message);
  return new Promise((res, rej) => {
    setTimeout(() => {
      res('2번 주문 완료');
    },1000);
  });
};

/* f3 함수 생략 f2와 구조 동일 */

Promise.all([f1() , f2() , f3() ]).then((res) => {
    console.log(res);
});

위 예제와 같이 Promise.all을 사용하고 값은 배열을 받는다. 반환되는 값도 배열로 반환된다.
실행되는 순서를 보면 배열안에 있는 함수를 모두 동시에 실행이 완료된 후에 then()이 실행된다.
이 방법에는 주의할 점이 생긴다
앞선 예제에서 rej(에러)값을 받게 되면 순차적으로 만나기 전까지만 실행되고 에러를 만난 이후에는 정지하게된다.
따라서 그 앞까지 몇개의 값은 반환된다.

하지만 Promise.all을 사용하여 한 번에 받을 때 하나라도 rej(에러) 값을 받게 된다면, 모든 값을 받을 수 없게 된다.
(주의점이긴 하나, 하나라도 정보를 얻지 못하면 페이지를 안보여준다는 상황에서 사용 할 수 있다.)


동시에 시작하지만 먼저 받아보고 싶다 ! Promise.race

Promise.all과 사용 방법은 동일하다 all 대신 race를 작성하면 되는데,
결과 값이 달라진다. all은 동시에 시작해서 한 번에 배열로 받았다. 그러기 때문에 하나라도 에러 값을 받는다면 전체 값을 받지 못했다. race는 동시에 시작한다는 점은 동일하지만, 먼저 끝낸 함수를 먼저 받는다는 점에서 다르다.
만약 f1() 은 dalay가 1초, f2()는 에러값을 받지만 delay가 2초라면 f1()값은 먼저 끝났기 때문에 값을 받을 수 있고 f2()는 에러이기 때문에 값을 못받는다. 당연히 그 이후로 끝나는 함수는 중지되기 때문에 값을 받지 못한다.





async, await는 내일 다시 정리 할 예정이다.