Front-end/JS

자바스크립트 프로미스와 비동기 처리 Promise & Asynchronous

ciocio 2021. 9. 21. 22:03

프로미스를 이해하기 위해서는 자바스크립트가 자료를 어떻게 비동기적으로 받아오는 지, 해당 원리를 이해할 필요가 있다.

 

2021.09.18 - [개발 공부/Browser] - Event Loop & Task queue 이벤트 루프와 태스크 큐

 

Event Loop & Task queue 이벤트 루프와 태스크 큐

📌 브라우저 환경 📍 자바스크립트 엔진 (싱글 스레드) 대부분의 자바스크립트 엔진은 크게 2가지 영역으로 구분할 수 있다. ✔ 콜 스택 call stack (실행 컨텍스트 스택) 소스코드 평가 과정에서

code-designer.tistory.com

 

 

📌  프로미스 등장 배경

 

2021.09.20 - [개발 공부/JS] - 자바스크립트 콜백 함수와 비동기 처리 Call back func & Asynchronous

 

자바스크립트 콜백 함수와 비동기 처리 Call back func & Asynchronous

콜백 함수를 이해하기 위해서는 자바스크립트가 자료를 어떻게 비동기적으로 받아오는 지, 해당 배경을 이해할 필요가 있다. 2021.09.18 - [개발 공부/Browser] - Event Loop & Task queue 이벤트 루프와 태스

code-designer.tistory.com

 

 

📌  프로미스 promise란?

 

호스트 객체가 아닌, ECMAScript 사양에 정의된 표준 빌트인 객체이다.

Promise 생성자 함수를 new 연산자와 함께 호출하면 Promise 객체를 생성한다.

Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 인수로 전달받는다. (resolve 함수 & reject 함수)

 

// 프로미스 생성
const promise = new Promise((resolve, reject) => {
  if(/*비동기 처리 성공*/){
    resolve('result');
  }
  else{
    reject('failure reason');
  }
});

 

const promiseGet = url => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.send();
    
    xhr.onload = () => {
      if(xhr.status === 200) {
        resolve(JSON.parse(xhr.response));
      }
      else {
        reject(new Error(xhr.status));
      }
    };
  });
};

promiseGet('https://code-designer.tistory.com/post/1')
  .then(res => console.log(res))
  .catch(err => console.log(err))
  .finally(() => console.log('Bye!'));

 

비동기 처리가 성공하면, 비동기 처리 결과를 resolve 함수에 인수로 전달하면서 호출하고

비동기 처리가 싱패하면, 비동기 처리 결과를 reject 함수에 인수로 전달하면서 호출한다.

 

 

📌  프로미스 상태 정보

 

프로미스 상태 정보 의미 상태 변경 조건
pending 비동기 처리가 아직 수행되지 않은 상태 프로미스가 생성된 직후 (default)
fulfilled 비동기 처리가 수행된 상태 (성공 ) resolve 함수 호출
rejected 비동기 처리가 수행된 상태 (실패 ) reject 함수 호출

 

-->  프로미스는 비동기 처리 상태처리 결과관리하는 객체다.

 

 

📌  프로미스 후속 처리 메서드

 

프로미스는 then, catch, finally 후속 메서드를 제공한다.

프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출된다.

이때 후속 처리 메서드의 콜백 함수에 프로미스의 처리 결과가 인수로 전달된다.

모든 후속 처리 메서드는 프로미스를 반환하며, 비동기로 동작한다.

 

◾  Promise.prototype.then 

 

// fulfilled
new Promise(resolve => resolve('fulfilled'))
  .then(v => console.log(v), e => console.error(e));  // fulfilled
  
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
  .then(v => console.log(v), e => console.log(e));    // Error: rejected

 

2개의 콜백 함수를 인수로 전달받는다.

첫 번째 콜백 함수는 프로미스가 fulfilled 상태가 되면 호출된다.

두 번째 콜백 함수는 프로미스가 rejected 상태가 되면 호출된다.

 

◾  Promise.prototype.catch 

 

1개의 콜백 함수를 인수로 전달받는다.

프로미스가 rejected 상태인 경우에만 호출된다.

 

◾  Promise.prototype.finally 

 

1개의 콜백 함수를 인수로 전달받는다.

프로미스의 상태와 관계 없어 무조건 1번 호출된다.

 

 

📍  프로미스 체이닝 Promise Chaining

 

then, catch, finally 후속 처리 메서드는 언제나 프로미스를 반환한다.

(만약 후속 처리 메서드의 콜백 함수가 프로미스가 아닌 값을 반환하더라도

그 값을 암묵적으로 resolve 또는 reject하여 프로미스를 생성해 반환한다.)

그렇기 때문에 연속적으로 호출할 수 있다. 이를 프로미스 체이닝이라고 한다.

 

콜백 패턴처럼 콜백 지옥은 발생하지않지만, 이 또한 콜백 함수를 연속적으로 사용하고 있는 건 맞다. (콜백 지양 plz)

이를 해결하기 위해 나온 문법이 async/await이다. 다음 시간에 ^!^

 

 

📌  프로미스의 에러 처리 (feat. catch 문의 존재 의미)

 

앞에서 살펴본 바에 의하면, 굳이 catch를 이용해 에러를 처리할 필요가 있나 싶다. 

then 메서드도 에러를 잡을 수 있기 때문이다. 그러나 then 메서드는 치명적인 단점이 존재한다.

 

// then 메서드의 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러는 캐치하지 못한다.
promiseGet('https://code-designer.tistory.com/post/1')
  .then(res => console.xxx(res), err => console.log(err));

 

반면 catch 메서드는 비동기 처리에서 발생한 에러뿐만 아니라 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다.

그래서 비동기 처리의 결과에 대한 모든 처리가 끝난 후에 catch 메서드로 에러를 처리하는 것이다. catch 권장 !ㅁ!

 

 

📌  프로미스 정적 메서드

 

◾  Promise.resolve

 

const resolvePromise = Promise.resolve([1, 2, 3]);
resolvePromise.then(console.log);  // [1, 2, 3]

 

◾  Promise.reject

 

const rejectPromise = Promise.reject(new Error('Error!'));
rejectPromise.catch(console.log);  // Error: Error!

 

◾  Promise.all

 

const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000));
const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000));
const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000));

Promise.all([requestData1(), requestData2(), requestData3()])
  .then(console.log)  // [1, 2, 3] -> 약 3초 소요
  .catch(console.error);
  
// Promise.all 메서드는 프로미스를 요소로 갖는 이터러블을 인수로 받는다.
// 전달받은 모든 프로미스가 모두 fulfilled상태가 되면 
// 모든 처리 결과를 이터러블에 저장해 새로운 프로미스를 반환한다.

// 이터러블 안에서 호출된 순서, 즉 처리 순서를 보장한다.
// 그러나 프로미스 하나라도 rejected된다면 나머지 프로미스의 처리를 고려하지 않고, 바로 error를 반환한다.

 

◾  Promise.race

 

Promise.race([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)),
  new Promise(resolve => setTimeout(() => resolve(2), 2000)),
  new Promise(resolve => setTimeout(() => resolve(3), 1000))
])
  .then(console.log)  // 3
  .catch(console.error);
  
// Promise.race 메서드는 Promise.all 메서드와 동일하게 프로미스를 요소로 갖는 이터러블을 인수로 받는다.

// 그러나 Promise.all 메서드와는 달리 
// 가장먼저 fulfilled 상태가 된 프로미스의 처리결과를 resolve하는 새로운 프로미스를 반환한다.

// 프로미스가 rejected 상태가 되면 Promise.all 메서드와 동일하게 처리된다.

 

◾  Promise.allSettled

 

Promise.allSettled([
  new Promise(resolve => setTimeout(() => resolve(1), 2000)),
  new Promise((_, reject) => setTimeout(() => reject(new Error('Error!')), 1000))
])
  .then(console.log);
  
/*
[
  {status: "fulfilled", value: 1},
  {status: "rejected", reason: Error: Error! at <anonymous>:3:54}
]
*/

// Promise.allSettled 메서드는 프로미스를 요소로 갖는 이터러블을 인수로 전달받는다.
// 전달받은 프로미스가 모두 settled 상태가 되면 모든 처리 결과를 이터러블에 담아 반환한다.
// *fulfilled이든 rejected이든 상관 없음

// *fulfilled 상태인 경우 status 프로퍼티와 value 프로퍼티를 갖는다.
// *rejected 상태인 경우 status 프로퍼티와 reason 프로퍼티를 갖는다.
반응형