자바스크립트 딥다이브 - 프로미스

ChoiYongHyeun·2024년 1월 8일
0

ES6 이전 자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백함수를 사용했다.

하지만 전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러를 처리하는데 한계가 있었다.

비동기 처리를 위한 콜백 패턴의 단점

콜백 헬

일반적으로 XMLHttpRequest 생성자를 이용하여 JSON 파일을 파싱해오는 코드는 다음과 같다.

<!DOCTYPE html>
  <script>
    const $pre = document.querySelector('pre');
    const URL = 'https://jsonplaceholder.typicode.com/posts';
    const get = (url) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.send();

      xhr.onload = () => {
        if (xhr.status === 200) {
          $pre.textContent = xhr.response;
        } else {
          console.error(`Error! ${xhr.status}${xhr.statusText}`);
        }
      };
    };

    get(URL);
  </script>

url 을 매개변수로 받아 파싱해온 값을 파싱 결과에 따라 pre 태그 내부에 텍스트로 넣는 함수이다.

이 때 만약 get 함수에서 상태코드가 200 이라면 파싱한 값을 로그 하도록 xhr.onload 에서 변경해보자

  <script>
    const $pre = document.querySelector('pre');
    const URL = 'https://jsonplaceholder.typicode.com/posts';
    const get = (url) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.send();

      xhr.onload = () => {
        if (xhr.status === 200) {
          return xhr.response; // status 가 200이면 response 값을 반환하도록
        } else {
          console.error(`Error! ${xhr.status}${xhr.statusText}`);
        }
      };
    };

    const result = get(URL);
    console.log(result); // undefined
  </script>

그런데 보면 열받게도 result 값은 undefined 이다. 분명히 xhr.status === 200 이면 xhr.response 값이 반환 되어야 할 것인데 말이다.

디버깅을 위해 완료되면 로그를 찍도록 해보자

  <script>
    const $pre = document.querySelector('pre');
    const URL = 'https://jsonplaceholder.typicode.com/posts';

    const getCurrentSecond = () => {
      const time = new Date();
      return `${time.getSeconds()}.${time.getMilliseconds()}`;
    };
    const get = (url) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.send();
      console.log(`send() 메소드를 보냈슈 ${getCurrentSecond()}`);
      xhr.onload = () => {
        if (xhr.status === 200) {
          console.log(`잘 파싱해왔어유~!! ${getCurrentSecond()}`);
          return xhr.response;
        } else {
          console.error(`Error! ${xhr.status}${xhr.statusText}`);
        }
      };
    };

    const result = get(URL);
    console.log(result, getCurrentSecond());
  </script>

자바스크립트 엔진이 코드를 실행한 순서를 보면 (발생한 이벤트를 순서로 보면)

xhr.send() -> console.log(result) -> xhr.onload() 이다.

onload() 는 보낸 request 에 대한 response 가 도착했을 때 발생하는 이벤트 핸들러이다.

그 말은 즉 get 함수가 종료된 후 xhr.onload() 가 호출되었다는 것이다.

왜 그럴까 ?

그 이유는 바로 XHRHttpRequest.onload()비동기처리 로 작업되기 때문이다.

get() 함수가 실행되면 자바스크립트 엔진은 xhr.send() 까지 콜스택에서 처리 한다.

xhr.onload() 이벤트 핸들러는 자바스크립트 엔진인 콜스택이 아닌 , 이벤트 루프로 넘어가 비동기적으로 response 를 기다리고 있는 상태이다.

자바스크립트 엔진은 콜스택에서 get 함수 내부에서 xhr.send()를 실행하고 이후의 get 함수 내부의 컨텍스트를 실행하는데 이 때 get 함수에는 return 값이 존재하지 않는다.

현재 코드에서 오로지 반환값은 xhr.onload() 가 실행 되었을 때에만 존재한다.

이후 get() 함수가 종료되고 나면 result 에는 반환값이 존재하지 않은 상태에서 로그 되고, 이후 xhr.onload() 가 작동하면서 파싱해온 결과값이 나타난다.

이렇게

비동기함수 내부에서 존재하는 코드의 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하는 것은 기대한 대로 동작하지 않는다.

더 직관적인 예시로 setTimeout 을 이용해보자

const timeMachine = (time, value) => {
  setTimeout(() => {
    value = 999;
    console.log(value);
  }, time);
};

let num = 1; // 1. num = 1 
timeMachine(1000, num); // 3. num = 999
console.log(num); // 2. num = 1

setTimeout 으로 보니 더욱 명확하다. 자바스크립트 엔진은 비동기 처리 작업의 경우는 건너 뛰고 콜스택에서 코드들을 진행시키다 보니

비동기 처리 함수는 상위 스코프의 변수의 값을 변경하거나 할 때 확인하는데 있어서 어려움을 겪는다.

그렇기 때문에 ES6 이전에는 비동기 처리 함수 내에서 반환값을 받은 후, 반환값을 확인하거나 반환값을 가지고 로직을 구성하기 위해서는 또 비동기 처리를 이용하고 또 비동기 처리를 이용해야 했다.

얼마나 악취가 나는지 확인해보자

  <script>
    const get = (url, callback) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.send();

      xhr.onload = () => {
        if (xhr.status === 200) {
          callback(JSON.parse(xhr.response));
        } else {
          console.error(`Error ! ${xhr.status} ${xhr.statusText}`);
        }
      };
    };

    const URL = 'https://jsonplaceholder.typicode.com';
    get(`${URL}/posts/1`, ({ userId }) => { // 처음 parsing 해온 값에서 userId 를  뽑아냄
      get(`${URL}/users/${userId}`, (userInfo) => {
    // 뽑아낸 userId 를 가지고 URL 를 변경해서 다시 parsing 해옴
        console.log(userInfo);
      });
    });
  </script>

인수로 들어가는 url 부분이 뒤죽 박죽 섞여 있어 이해가 잘 안가지만 로직은 다음처럼

    get('/step1' , a =>{
      get('/step2' , b => {
        get('/step3' , c => {
          get('/step4' , d => {
            console.log(d);
          })
        })
      })
    })

비동기 함수에서 비동기 함수를 또 호출하고, 비동기 함수에서 또 비동기 함수를 호출하고 ..

이런 식의 문제가 발생하게 된다.

정리

비동기 함수의 결과값을 가지고 로직을 작성하기 위해서는 비동기 처리에 비동기처리를 씌우고 .. 이런식으로 했어야 했다.
그런 이유는 비동기 함수의 결과값은 자바스크립트 엔진과 다른 부분인 이벤트 루프에서 처리되기 때문에 자바스크립트 엔진에서 비동기 처리의 결과값을 반환 받거나 확인하는 것이 어렵기 때문이다.

에러 처리의 한계

가장 심각한 것은 에러 처리가 곤란하다는 것이다.

try catch 문을 이용해서 에러가 발생한다면 캐치 할 수 있도록 해보자

try {
  throw new Error('고의적으로 발생시킨 에러');
} catch (e) {
  console.error('캐치했지롱 캐치 한 에러 명 :', e);
} // 캐치했지롱 캐치 한 에러 명 : Error: 고의적으로 발생시킨 에러 ... 

일반적으로 자바스크립트 엔진은 에러가 발생하면 에러를 캐치 할 수 있다.

하지만 비동기 함수는 그렇지 못하다.

try {
  setTimeout(() => {
    throw new Error('너는 에러를 캐치하지 못했어 이자식아');
  }, 1000); // Error: 너는 에러를 캐치하지 못했어 이자식아
} catch (e) {
  console.error('캐치한 에러', e);
}

그 이유는 try catch 문에 대해서 공부한 후에 더 깊숙히 알아보겠지만

현재까지 알아본 것은 try catch 문의 특성 때문이다.

try {
	// 이 구문에서 실행중인 실행 컨텍스트들이 콜스택에 쌓이다가
  	throw new Error() // 에러가 발생하면
} catch(){
  // 에러가 발생한 지점부터는 이 부분의 블록들이 시작됨
}

let num = 0;
try {
  while (num < 10) {
    num += 1;
    console.log(num);
    if (num === 5) {
      throw new Error('num 이 5가 되면 에러가 발생해요 ~!');
    }
  }
} catch (error) {
  console.log(error);
  console.log('catch 문 발동~!');
  while (num < 10) {
    num += 1;
    console.log(num);
  }
}

다음과 같이 try 문에서 num 값을 1씩 증가시키다가 num 의 값이 5가 되면 에러를 발생시킨다.

이 때 콜스택에서는 try 문에 존재하는 while 문이 콜스택에 들어가 while 문이 중단 될 때 까지 진행되다가 error 가 발생하면

이후 진행 사항을 catch 블록에 존재하는 컨텍스트가 콜스택에 담겨 진행된다.

그럼 비동기 함수의 try catch 문을 살펴보자

try {
  setTimeout(() => {
    throw new Error('너는 에러를 캐치하지 못했어 이자식아');
  }, 1000); // Error: 너는 에러를 캐치하지 못했어 이자식아
} catch (e) {
  console.error('캐치한 에러', e);
}

여기서 try 문의 블록에 존재하는 setTimeout(...) 이 콜스택에 담겼을 때

자바스크립트 엔진은 setTimeout(...) 을 호출하고 콜스택에서 제거된다.

이 때 setTImeout 내부에 존재하는 콜백함수는 이벤트 루프에 담겨있다가 1초가 지나면 태스크 큐로 간 후 콜스택에 담겨서 호출된다.

이 때의 콜백함수는 try 문의 setTimeout 함수에서 호출된 것이 아니기 때문에 catch 는 에러를 캐치하지 못한다.

catch {} 는 위의 try {} 컨텍스트에서 실행된 에러들을 캐치한다.
이벤트 루프에서 비동기적으로 처리된 콜백 함수는 try {} 컨텍스트에 존재하는 함수로 인해 호출된 콜백 함수가 아니기 때문에 catch {} 는 에러를 캐치하지 못한다는 뜻이다.

즉 에러는 호출자 방향으로 전파된다. catch {} 는 주구장창 위의 try {} 에서 호출된 함수들에 대한 에러를 캐치하려고 기다리고 있는데 실제 setTimeout(...) 내부에 존재하는 콜백함수는 이벤트 루프 방향으로 에러를 보내기 때문에

반복하여 말하지만 내부의 콜백함수의 호출자는 setTimeout 이 아니라 이벤트 루프와 태스크 큐이다.

캐치하지 못한다는 뜻이다.

정리

비동기 함수의 반환값을 자바스크립트 엔진에서 처리하기 힘들거나

에러를 캐치하기 어려웠던 이유는

자바스크립트 엔진은 호출 순서에 따라 콜 스택에 담아 처리하는데

비동기 함수는 콜스택에서 호출되면 콜스택이 아닌 이벤트 루프, 태스크 큐 로 넘어가서 기다린 후 콜스택에 담기기 때문에

비동기 함수의 결과값이나 에러에 대응하고자 하는 이후 컨텍스트들이 콜스택에 담겼을 때는 비동기 함수의 결과값이나 에러에 대응하지 못한다.


프로미스

킹 갓 ES6 이후에는 Promise 라는 생성자 함수와 Promise 객체가 존재한다.

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

Promise 생성자 함수는 resolvereject 함수를 인수로 전달 받는다.

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

반환값을 주지 않는 콜백 함수들을 인수로 넣으며

resolve 는 비동기 처리가 성공하였을 때의 콜백함수 , reject 는 비동기 처리가 실패했을 때의 콜백함수이다.

  <script>
    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(xhr.status, xhr.statusText);
          }
        };
      });
    };
    const URL = 'https://jsonplaceholder.typicode.com/posts';
    const promiseObj = promiseGet(URL);

    console.log(promiseObj);
  </script>

promiseGet 함수를 실행하면 Promise 객체가 반환되는데 반환된 객체를 로그해보면

내부 슬롯에 [[PromiseState]][[PromiseResult]] 가 존재한다.

비동기 처리가 성공한 현재 [[PromiseState]]fulfilled 이며

[[PromiseResult]] 에는 비동기 처리의 결과값이 존재한다.

그럼 이번에는 비동기 처리가 실패했을 때를 살펴보자

	...
   const URL = 'WrongURL';
    const promiseObj = promiseGet(URL);

    console.log(promiseObj);
  </script>

이번 [[PromiseState]]rejected 이고 [[PromiseResult]]reject(..) 부분에 존재하는 반환값이 담겨있다.

Promise 객체는 비동기 처리가 성공했을 때는 resolve 콜백함수의 인수에 담긴 값을 [[PromiseResult]] 에 담고 [[PromiseState]]fulfiled 로 변경한다.

비동기 처리가 실패했을 때는 reject 콜백함수의 인수에 담긴 값을 [[PromiseResult]] 에 담고 [[PromiseState]]rejected 로 변경한다.

상태 의미 상태 변경 조건
Pending 보류 중 Promise가 생성되었고, 아직 이행(resolve) 또는 거부(reject)되지 않은 상태
Fulfilled 이행됨 (성공) Promise가 성공적으로 완료되어 resolve 함수 호출
Rejected 거부됨 (실패) Promise가 실패하여 reject 함수 호출

처음 Promise 객체가 생성되었을 떄 상태는 Pending 이였다가, 비동기 처리 결과에 따라 상태가 변경된다.

이처럼 Promise 객체의 상태는 Pending 이였다가 비동기 처리 결과에 따라 fulfiled , rejected 로 변경되는데, 이처럼 상태가 변경된 Promise 의 상태를 settled 되었다고 한다.

settled 된 상태에서는 Pending 상태로 돌아가지 못한다.

정리

Promise 객체는 비동기처리의 처리 상태와 처리 결과를 관리하는 객체이다.


프로미스의 후속 처리 메소드

Promise 객체로 처리 상태와 처리 결과를 관리한다면

처리 상태와 처리 결과에 따른 후속 처리도 해야 한다.

이를 처리하기 위한 후속 메소드인 then , catch , finally 가 존재한다.

Promise.prototype.then

then 은 두 개의 콜백 함수를 인수로 전달 받는다.

첫 번째 콜백 함수는 Promise[[PromiseState]]fulfiled 상태가 되면 호출되며, 인수로는 비동기 처리 결과를 인수로 전달 받는다.

두 번쨰 콜백 함수는 Promise[[PromiseState]]rejected 상태가 되면 호출되며, 인수로는 비동기 처리의 에러를 인수로 전달 받는다.

    const URL = 'https://jsonplaceholder.typicode.com/posts';
    const promiseObj = promiseGet(URL);

    promiseObj.then(
      (v) => console.log(v), // state 가 fulifed 일 때 실행되는 콜백 함수
      (e) => console.error(e), // state 가 rejected 일 때 실행되는 콜백 함수
    );

이 때 then 의 인수를 살펴보면 선택적으로 넣을 수 있는데 두 번째 콜백 함수를 넣지 않으면 undefined 가 들어가지며 두 번째 콜백 함수는 실행되지 않는다.

    const URL = 'https://jsonplaceholder.typicode.com/posts';
    const promiseObj = promiseGet(URL);

    promiseObj.then((v) => console.log(v)); // 두 번째 콜백 함수를 넣지 않아도 잘 실행 됨
    promiseObj.then(undefined, (e) => console.error(e));
// 첫 번째 콜백 함수를 undefined 로 넣어 에러만 캐치 할 수 있도록 해도 잘 실행 됨

다만 두 개의 콜백 함수를 모두 넣는 것은 가독성 양상에서 매우 떨어지기 때문에

비동기 함수 처리가 성공 했을 때의 첫번째 콜백 함수만 넣어주고, 에러가 발생했을 때는 catch 를 이용해주기로 하자

Promise.prototype.catch

catch 메소드는 한 개의 콜백 함수를 인수로 전달 받으며 catch 메소드의 콜백함수는 프로미스가 rejected 일 때만 호출된다.

    const URL = 'https://jsonplaceholder.typicode.com/wrongurl';
    const promiseObj = promiseGet(URL);
    console.log(promiseObj);

    promiseObj.catch((e) => console.error(e));

Promise.then , Promise.catchPromise 객체 상태가 settled 되었을 때 실행된다.

그럼으로 Promise.then , Promise.catch 또한 비동기 함수이다.

Promise.prototype.finally

finally 는 프로미스의 상태와 상관없이 무조건 한 번 호출된다.

이는 프로미스의 상태와 상관 없이 공통적으로 수행해야 할 처리 내용이 있을 때 유용하다.

    const URL = 'https://jsonplaceholder.typicode.com/wrongurl';
    const promiseObj = promiseGet(URL); // finally는 state 가 어떻든 상관없다.

    promiseObj.finally(console.log('룰루뿅!')); // 룰루뿅! 

finally 는 어떤 인수도 받지 않는다.

Promisestate 와 무관하게 실행되기 때문에 당연한 듯 싶다.

중요한 것은 then , catch , finally 는 모두 promise 객체 자기 자신을 반환 하기 때문에 메소드 체이닝이 가능하다.

위에서 했던 예시를 가지고 메소드 체이닝을 이용해서 then , catch , finally 를 사용해보자

  <script>
    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(xhr.status);
          }
        };
      });
    };
    const URL = 'https://jsonplaceholder.typicode.com/posts';
    promiseGet(URL)
      .then((e) => console.log(e)) // 2. [실행 결과값 ...]
      .catch((v) => console.error(v)) // 실행되지 않음 
      .finally(console.log('비동기 함수 처리 끝 ! ')); // 1. 비동기 함수 처리 끝 !
  </script>

메소드 체이닝을 이용해서 야무지게 사용 할 수 있다.

다만 Promise 객체또한 비동기적으로 처리되기 때문에 실행 순서를 보면 비동기 처리 결과와 상관 없는 finally 가 가장 먼저 실행되는 모습을 볼 수 있다.

정리

Promise 는 비동기 처리 결과를 관리하는 객체이며, 메소드 체이닝이 가능한 메소드를 통해 비동기 처리결과를 좀 더 유연하게 처리 할 수 있다.

다만 Promise 의 메소드들인 then , catch 등 또한 비동기적으로 처리 되는 메소드이다.

Promise 를 이용하면 비동기 처리 결과 반환값을 비동기적이지 않은 느낌으로 처리 할 수 있는 마법의 객체처럼 생각했었는데 then , catch , finally 의 실행 순서를 보고 다시 이해 할 수 있었다.


프로미스의 에러처리

위에서 then 은 에러가 발생 했을 시의 콜백 함수 또한 두 번째 인수로 선택적 으로 받는다고 하였다.

    promiseGet(URL).then(
      (e) => console.log(e),
      (v) => console.error(v),
    );

이런식으로 말이다.

catch 는 에러가 발생 했을 시의 콜백 함수만을 인수로 받는다고 하였다.

이를 then 으로 구현하면

    promiseGet(URL).then(
      undefined,
      (v) => console.error(v),
    );

와 같다.

이 때 then 안에 비동기 처리가 성공 했을 때의 콜백함수 뿐이 아니라 실패했을 때의 콜백함수를 넣는 것 보다 then ,catch 를 메소드 체이닝을 이용하는 것이 두 가지 이유로 좋다.

// 비추천
    promiseGet(URL).then(
      (e) => console.log(e),
      (v) => console.error(v),
    );

// 추천
promiseGet(URL)
      .then((v) => console.error(v))
      .catch((e) => console.error(e));

가독성 향상

딱 봐도 메소드 체이닝 이용한게 가독성이 더 좋다.

then 에서 발생 할 수 있는 에러 처리

첫 번째 방법은 첫 번째 콜백 함수에서 또 비동기 처리를 할 떄 발생 할 수 있는 에러가 존재 할 경우의 에러를 두 번쨰 콜백 함수가 캐치 할 수 없으나

메소드 체이닝을 이용하면 then 단계에서 발생하는 에러또한 캡쳐 할 수 있다.

    const URL = 'https://jsonplaceholder.typicode.com/posts';
    promiseGet(URL)
      .then((v) => {
        throw new Error('error!');
      })
      .catch((e) => console.error(`${e}가 발생했슴둥`)); // Error: error!가 발생했슴둥

Promise 체이닝

Promise 체이닝은 then , catch 메소드의 반환값이 Promise 객체이기 때문에 메소드 이후 사용 이후에도 Promise.prototype 의 메소드를 사용 할 수 있는 기법을 의미한다.

이 때 반환되는 Promise 객체는 어떻게 생겼을까 ?

  <script>
    const firstPromise = new Promise((resolve) => resolve(1)); 
    // [[PromiseResult]] 가 1 인 Promise 객체 생성

    setTimeout(() => {
      // state 가 변경된 후의 promise 상태를 로그
      console.log(firstPromise);
    }, 1000);

	...    
  </script>

다음처럼 firstPromise 객체를 생성해주었다.

이 때 firstPromisethen 메소드를 이용해 secondePromise 객체를 생성해보자

  <script>
	...
    const secondPromise = firstPromise.then((value) => {
      return 'return value';
    });

    setTimeout(() => {
      // state 가 변경된 후의 promise 상태를 로그
      console.log(secondPromise);
    }, 1000);
    
  </script>

then 메소드가 반환하는 Promise 객체는 then 내의 콜백함수가 반환한 값이 [[PromiseResult]] 에 담긴 새로운 Promise 객체를 반환한다.

그럼 반환문이 없다면 어떻게 될까 ?

  <script>
	...
    const noReturnPromise = firstPromise.then((value) => 'return value');
    const undefinedReturnPromise = firstPromise.then((value) =>
      console.log(value),
    );

    setTimeout(() => {
      // state 가 변경된 후의 promise 상태를 로그
      console.log(noReturnPromise);
    }, 1000);

    setTimeout(() => {
      // state 가 변경된 후의 promise 상태를 로그
      console.log(undefinedReturnPromise);
    }, 1000);
  </script>

반환문이 명시되지 않고 값으로 평가될 수 있는 표현식만 존재한다면, 해당 값을 [[PromiseResult]] 에 담고, undefined 가 반환되는 값이라면 [[PromiseResult]]undefined 를 담는 모습을 볼 수 있다.

이처럼 Promise.prototype.then 메소드는 새로운 Promise 객체를 반환한다.

catch 또한 동일하다.

  <script>
    const firstPromise = new Promise((resolve) => resolve(1)); 
    const secondPromise = firstPromise.then((value) => value);

    setTimeout(() => {
      console.log(firstPromise); // 생김새는 동일함
      console.log(secondPromise); // 생김새는 동일함
      console.log(firstPromise === secondPromise); // false
    });
  </script>

해당 과정을 통해 두 Promise 의 객체는 같지만 같은 객체는 아닌 것을 보아 명확하게 then , catch 메소드가 반환하는 Promise 객체는, 메소드가 실행된 후 새로 생성되어 만들어진 Promise 객체임을 알 수 있다.

정리

Promise 체이닝이 가능한 이유는 then , catch 메소드의 반환값 또한 내부에서 정의된 콜백 함수의 반환값이 담긴 새로운 Promise 객체를 생성하여 반환하기 떄문이다.

그러면 프로미스 체이닝을 통해 이전 콜백 지옥을 경험했던 코드를 체이닝을 통해 변경해보자

	// 이전 콜백 지옥을 경험했던 코드  
    const get = (url, callback) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.send();

      xhr.onload = () => {
        if (xhr.status === 200) {
          callback(JSON.parse(xhr.response));
        } else {
          console.error(`Error ! ${xhr.status} ${xhr.statusText}`);
        }
      };
    };

    const URL = 'https://jsonplaceholder.typicode.com';
    get(`${URL}/posts/1`, ({ userId }) => { // 처음 parsing 해온 값에서 userId 를  뽑아냄
      get(`${URL}/users/${userId}`, (userInfo) => {
    // 뽑아낸 userId 를 가지고 URL 를 변경해서 다시 parsing 해옴
        console.log(userInfo);
      });
    });
	// 프로미스 체이닝을 이용하기 
    const URL = 'https://jsonplaceholder.typicode.com';
    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)); // 파싱한 JSON 객체가 담김
          } else {
            reject(xhr.status);
          }
        };
      });
    };

    promiseGet(`${URL}/posts/1`)  
		// 1. [[PromiseResult]] 내부에는 `id 가 1인 JSON 객체가 들어가있음`
      .then(({ userId }) => promiseGet(`${URL}/users/${userId}`))
		// 2. JSON 객체에서 userId 프로퍼티 값을 가지고 새로 Promise 객체를 생성 
		// 생성된 Promise의 [[PromiseResult]]에는 인수로 받은 URL 의 JSON 객체를 가져옴 
      .then((result) => console.log(result)); 
		// 3. 2번에서 생성된 Promise 객체의 [[PromiseResult]] 에 존재하는 

프로미스 체이닝을 이용하니 가독성 양상에서는 더 좋아진 것을 확인 할 수 있다.

다만 이 또한 가독성 양상에만 좋을 뿐, 여전히 비동기 함수들을 콜백 함수로 주기 때문에 콜백 패턴을 이용한다는 점은 동일하다.

이는 ES8 에 도입된 async/await 를 이용해 해결 할 수 있다.

아직 얘네가 뭔진 모르겠지만 동기 처리처럼 프로미스가 처리 결과를 반환 할 수 있다고 한다.


Promise 의 정적 메소드

생성자 함수로 사용되지만 함수도 객체이므로 메소드를 가질 수 있다.

5가지 정적 메소드를 제공한다.

Promise.resolve , Promise.reject

두 메소드는 이미 존재하는 값을 래핑하여 프로미스를 생성하기 위해 사용된다.

    const obj = { name: 'lee', age: 16 };
    const resolvedPromise = Promise.resolve(obj);
    const rejectedPromise = Promise.reject(obj);

    console.log(resolvedPromise);
    console.log(rejectedPromise);

해당 정적 메소드는 생성자 함수를 이용해 새로운 프로미스 객체를 생성하는 것과 동일하게 작동한다.

Promise.all

여러 개의 비동기 처리를 모두 병렬 처리 할 때 사용한다.

우선 Promise.all 을 사용하지 않는 경우를 먼저 예로 들어보자

    const res = [];
    const firstPromise = () =>
      new Promise((resolve) => setTimeout(() => resolve(1), 1000));
    const secondePromise = () =>
      new Promise((resolve) => setTimeout(() => resolve(2)), 2000);
    const thirdPromise = () =>
      new Promise((resolve) => setTimeout(() => resolve(3)), 3000);

    firstPromise() // 1초 소요
      .then((data) => {
        res.push(data);
        return secondePromise(); // 2초 소요
      })
      .then((data) => {
        res.push(data);
        return thirdPromise(); // 3초 소요
      })
      .then((data) => {
        res.push(data);
        console.log(res); // [1,2,3] 총 6초 소요됨
      });

메소드 체이닝을 할 떄 비동기 함수를 사용하게 되면 말이 비동기 함수지 결국 직렬적으로 연결되기 때문에 오래 걸린다.

하지만 Promise.all 을 이용하면 비동기 함수들을 병렬적으로 처리 할 수 있다.

    const res = [];
    const firstPromise = () =>
      new Promise((resolve) => setTimeout(() => resolve(1), 3000));
    const secondePromise = () =>
      new Promise((resolve) => setTimeout(() => resolve(2)), 2000);
    const thirdPromise = () =>
      new Promise((resolve) => setTimeout(() => resolve(3)), 1000);

    const parallelPromise = Promise.all([
      firstPromise(),
      secondePromise(),
      thirdPromise(),
    ]); // 모든 프로미스 객체들이 resolve 될 때 마다 parallelPromise 의 [[PromiseResult]] 에 저장

    parallelPromise.then((obj) => { 
      // then 은 모든 값들이 저장되면 실행됨
      console.log(parallelPromise);
      [...obj].forEach((num) => res.push(num));
      console.log(res);
    });

Promise.all 은 이터러블한 객체를 인수로 받는다. 이후 이터러블한 객체 내에 존재하는 모든 Promise 객체들 내의 [[PromiseResult]] 내에 존재하는 값들을 배열에 저장하고 새로 생성되는 Promise 객체의 [[PromiseResult]] 내에 저장한다.

이는 가장 오래 걸리는 비동기 처리의 소요 시간 + 모든 객체에 비동기 처리를 실행하는 시간 만큼의 소요 시간만 걸리기 때문에 매우 효율적이다.

다만 몇 가지 포인트는 다음과 같다.

  • [[PromiseResult]] 내의 배열에 저장 될 때는 Promise.all 에게 건내준 인수의 순서를 보장한다

  • 인수로 받은 배열에서 Promise 객체의 [[PromiseState]]rejected 이면 에러를 반환한다.

    const res = [];
    const firstPromise = () =>
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Error 1')), 3000),
      );
    const secondePromise = () =>
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Error 2')), 2000),
      );

    const thirdPromise = () =>
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Error 3')), 1000),
      );

      
    Promise.all([firstPromise(), secondePromise(), thirdPromise()])
      .then(console.log)
      .catch(console.log); // Error: Error 3

해당 코드를 살펴보면 Promise.all 에서 객체들의 비동기 결과를 배열에 저장하다가 thirdPromise 에서 제일 먼저 비동기 처리가 실패하니 , 에러가 발생하고, 발생한 에러 값을 catch 메소드가 캡쳐 한 것을 볼 수 있다.

  • Promise.all 의 인수에는 Promise 객체가 아닌 것이 들어 올 수 있으나 모두 암묵적으로 Promise.reslove() 로 생성된 값이 담긴다.

    Promise.all([1, 2, 3]).then(console.log); // [1,2,3]

Promise.all 으로 생성되는 Promise 객체는 배열에 값들을 저장하고 있기 때문에 배열의 프로토타입을 이용 할 수 있다.

깃허브 API 를 이용해 userId 들을 가지고 userName을 가져와보자

  <script>
    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(xhr.status);
          }
        };
      });
    };

    const githubIds = ['mojombo', 'defunkt', 'pjhyett'];
    const promiseArr = githubIds.map((id) =>
      promiseGet(`https://api.github.com/users/${id}`),
    ); // map 을 이용해 Promise 객체가 담긴 배열로 생성

    Promise.all(promiseArr)
    // users는 Promise.all 로 인해 생성되어 배열 안에 JSON 파일들이 담겨있음
      .then((users) => users.map((user) => user.name))
      // then 절에서는 JSON 파일들이 담긴 배열을 받아 JSON 파일들에서 name 값을 담은 배열을 생성
      // 생성된 배열을 새로 생성한 Promise 객체의 [[PromiseResult]] 에 담음 
      // Promise ['Tom Preston-Werner', 'Chris Wanstrath', 'PJ Hyett']
      .then(console.log)
      // [[PromiseResult]] 내에 존재하는 값들을 모두 로그
      // Tom Preston-Werner  Chris Wanstrath PJ Hyett 
      .catch((error) => console.error(error));
  </script>

구우웃 ~~

Promise.race

말 그대로 경주한다.

비동기 처리 한다는 것은 Promise.all 과 동일하지만 다른 점은 가장 먼저 비동기 처리가 끝난 Promise 객체 하나만 반환한다.

도중에 비동기 처리가 실패 할 경우에는 Promise.all 과 같이 [[PromiseState]]rejected 인 프로미스를 반환한다.

Promise.allSettled

ES11 에 나온 따끈따끈한 메소드이다.

이 또한 Promise 들이 담긴 배열을 인수로 받는다. (Promise.all , Promise.race와 같이 비동기 처리도 지원한다.)

담긴 배열에서 모든 Promise 들이 settled 된 이후

settledPromise 들의 배열 안에 settlePromise 들의 정보를 담은 객체 배열을 반환한다.

여기서 settled되었다는 것은 fulfiled 되었거나 rejected 되었다는 것이다.

  <script>
    Promise.allSettled([
      new Promise((resolve) => setTimeout(() => resolve(1)), 1000),
      new Promise(
        (_, reject) => setTimeout(() => reject(new Error('error!'))),
        1000,
      ),
    ]).then(console.log);
  </script>

이 때 [[PromiseState]] 에 따라 생성되는 객체의 형태가 다르다.

fulfilledPromisestatus , value 가 담긴 객체를 반환하고
rejectedPromisereason , status 가 담긴 객체를 반환한다.


마이크로 태스트 큐

아 이건 또 뭐야 진짜

  <script>
    setTimeout(() => console.log(1), 0);
    Promise.resolve()
      .then(() => console.log(2))
      .then(() => console.log(3));
  </script>

다음과 같은 코드가 있을 때 로그 되는 순서를 보면 상식적으로

아 ~ 전부 비동기 함수고 , 모두 즉시 비동기 처리됐다가 콜백 함수가 콜스택으로 넘어와서 호출 될테니까 1 -> 2 -> 3 이겠구나 ? 싶다.

근데 결과는 2 -> 3 -> 1 이다.

ㅋㅋ

그 이유는 setTimeout 은 이벤트 루프에서 태스크 큐 로 넘어가 콜스택이 비면 해당 콜백 함수를 콜스택으로 넘기지만

프로미스의 메소드는 이벤트 루프에서 태스크 큐가 아닌 마이크로 태스트 큐 로 넘어가 콜스택이 비면 해당 콜백 함수를 콜스택으로 넘긴다.

위의 세 비동기 함수 모두 실행되면 각 태스크 큐 , 마이크로 태스크 큐 에 담기고 콜스택이 비기만을 호시탐탐 기회를 노리고 있다.

then 절이 모두 끝나고 콜스택이 비면

태스크 큐 에 존재하는 콜백함수가 콜 스택이 갈 차례가 되어도 마이크로 태스크 큐 에 담긴 콜백 함수가 우선적으로 콜스택에 들어간다.

ㅋㅋ 그렇다

마이크로 태스크 큐가 태스크 큐보다 우선순위가 더 높다.

태스크 큐는 콜백 함수가 비었는지도 확인하고, 마이크로 태스크 큐가 모두 비었는지도 확인해야 한다.

불쌍한 태스크 큐 친구들 ..


profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글