[REAL Deep Dive into JS] 45. Promise

young_pallete·2022년 11월 1일
0

REAL JavaScript Deep Dive

목록 보기
46/46
post-custom-banner

🚦 본론

Promise 객체의 필요성

Promise는 비동기 처리가 복잡했던 기존의 로직을 탈피하고자 도입된 객체입니다.
이를 통해 콜백 패턴의 단점을 해소할 수 있게 되었어요!

콜백 패턴

우리, 콜백 패턴이 왜 나와야 하는지를 먼저 알아야 해요.
사실, 자바스크립트는 이벤트 루프를 통해 비동기의 한계를 어느정도 극복할 수 있었어요.
따라서 자바스크립트는 싱글 스레드 엔진이므로 블로킹이 되어야 하는데, 마치 병렬적으로 비동기 로직을 해결할 수 있었어요.

const timer1 = () => setTimeout(() => console.log(1), 1000);
const timer2 = () => setTimeout(() => console.log(2), 2000);
const timer3 = () => setTimeout(() => console.log(3), 3000);
const timer4 = () => setTimeout(() => console.log(4), 4000);

timer1();
timer2();
timer3();
timer4();

// 1초마다 1, 2, 3, 4 출력 (총 4초)

그런데 한계가 있었습니다.

만약 데이터 B를 처리하기 위해서는 데이터 A를 가져와야 한다고 했을 때, 이러한 순서는 어떻게 보장해야 하나...?

따라서 이를 어떻게 처리하지...?하다가 나온 것이 바로 콜백 패턴입니다.
마치, 콜백 함수를 통해 쭉 ~ 함수를 타고 들어가 원하는 데이터를 처리해내는 방식이었어요.

const timer1 = (data) => setTimeout(timer2, 1000, data);
const timer2 = (data) => setTimeout(timer3, 2000, data);
const timer3 = (data) => setTimeout(getCharactersFromArray, 3000, data);

const getCharactersFromArray = (data) => setTimeout(() => {
    console.log(`result: ${data.join('')}`)
}, 4000);

timer1([1,2,3,4]) // 10초 뒤에 "result: 1234" 출력

지금은 변수를 잘 처리해서 보기가 좋아요. 하지만 이에 대한 로직을 생각하려면, 머리 속에서는 다시 이렇게 정제할 거에요.

const data = [1,2,3,4];

setTimeout(() => {
	setTimeout(() => {
      setTimeout(() => {
      	setTimeout(() => {
        	console.log(`result: ${data.join('')}`)
        }, 4000)
      }, 3000)
    }, 2000)
}, 1000)

오우... 보기만 해도 어지럽죠? 😭

그래서 이러한 모습이 유지보수도 어렵고... 항상 실수하기 쉬운 모습이라 우리는 흔히 콜백 지옥이라고 부르게 된 것이에요.

프로미스의 탄생

아니, 병렬 처리를 통해 성능은 높아졌지만, 데이터를 직렬로 처리해야 하는 순간은 까다로워졌어요.
따라서, 이러한 문제를 해결하기 위해 나온 것이 바로 Promise 였어요.

이 친구는 엄연한 생성자 함수이며, 따라서 인스턴스 객체로 사용할 수 있답니다.

const promise = new Promise();

3가지 상태

우리, 일반적인 비동기의 상황을 떠올려 볼까요?

일단 코드가 비동기면 그냥 넘어갈테고... 스케줄링에 따라 서버에서 통신을 할 거에요.
그러다 데이터를 받으면 받은 데이터를 메모리에 저장하겠죠?
그런데 반대로 오류가 나면, 에러를 호출하여 메시지를 전달해줄 거에요.

즉, 비동기에는 크게

  • Pending(지연)
  • Fulfilled(이행)
  • Rejected(실패)

가 존재합니다.

일단 이것이 중요해요.
Promise은 이러한 상태에 따라 애플리케이션을 제어해주기 용이하기 위해 나온 것입니다!.

resolve와 reject

따라서 대기는 서버에서 기다리는 역할이니 크게 설정할 것은 없고, 성공과 실패했을 때의 로직 처리가 중요하겠죠?

따라서 이 친구는 콜백 함수를 받는데요, 콜백 함수는 다음 2가지를 인수로 전달 받아요.

  • resolve: 해결했을 시 resolve와 함께 데이터를 전달하면 돼요!
  • reject: 실패했을 시에는 reject와 함께 에러를 전달해주면 돼요!

.then, .catch, .finally

기존의 콜백의 문제는 무엇이었죠? 바로 콜백 지옥과 같이 너무나 scope의 depth가 길어져 가독성이 현저히 낮아지는 문제였습니다.

따라서 Promise는 다음과 같은 제안을 했어요.

💡 나, 인스턴스인데, 이를 메서드를 호출해서 체이닝 방식으로 전개하면 어떨까?

메서드 체이닝의 방식을 사용하면,

  • 일단 선언적이고
  • 콜백의 깊이가 깊어지는 것을 피할 수 있었어요.

따라서 이러한 로직을 처리하는 메서드를 탑재했는데요. 그것이 바로 then, catch finally 입니다.

이 3가지는 다음과 같은 특징을 지녀요.

  • then: 이행했을 시 처리할 콜백 함수를 받는다.
  • catch: 실패했을 시 처리할 콜백 함수를 받는다.
  • finally: 이행과 실패 상관 없이 결론적으로 마지막에 처리할 콜백 함수를 받는다.

응용

한 번 책에 있던 예제를 들고 와보겠습니다!
이를 https://jsonplaceholder.typicode.com/나, 프록시가 설정된 개발 서버에서 한 번 돌려보시면 돼요! (CORS는 항상 염두하시길 바라요! 😉)

const promiseAjax = (method, url, payload) =>
  new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    
    xhr.open(method, url);
    xhr.setRequestHeader('Content-type', 'application/json');
    xhr.send(JSON.stringify(payload));

    xhr.onreadystatechange = function () {
      if (xhr.readyState !== XMLHttpRequest.DONE) return;

      if (xhr.status >= 200 && xhr.status < 400) {
        resolve(xhr.response); // Success!
      } else {
        reject(new Error(xhr.status)); // Failed...
      }
    };
  });
  
promiseAjax('GET', 'https://jsonplaceholder.typicode.com/posts/1')
  .then(JSON.parse)
  .then(
    console.log,
    console.error
  );

// {userId: 1, id: 1, title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', body: 'quia et suscipit\nsuscipit recusandae consequuntur …strum rerum est autem sunt rem eveniet architecto'}

잘 나오시나요?

어떻게 보면 비동기 로직이 좀 길어서 어려워 보일 수 있지만, 중요한 건 체이닝으로 한 번의 depth만에 끝난 것처럼 보인다는 것입니다. (실제로는 2번 콜백을 실행한 것이지만요)
이렇게 하면 결과적으로는, 몇 번째 단계에서 무엇을 하였는지가 좀 더 명확해지죠.

즉, 가독성에서 현저한 차이를 보인다는 것이 바로 Promise의 장점이겠어요!

프로미스의 정적 메소드

Promise.resolve/Promise.reject

이 친구들은 존재하는 값을 Promise 객체로 만들기 위해 사용해요.

Promise.resolve()

Promise.all, Promise.allSettled, Promise.race, Promise.any

Promise에는 여러 비동기 로직의 결과를 처리할 유용한 메서드들이 있는데요. 대표적인 것이 allallSettled, race에요.

이 친구들은 다음과 같은 특징을 지녀요.

Promise.all

이터러블을 받고, 동시에 병렬적으로 수행해요. 만약 실패한 게 있다면 제일 먼저 에러 나온 것을 reject하고 새로운 프로미스를 반환합니다.

예제
Promise.all([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then(console.log) // [1,2,3]

// 만약 에러가 처리되는 게 있다면 먼저 에러 처리된 것부터 반환합니다.
Promise.all([
  Promise.reject(1),
  Promise.reject(2),
  Promise.reject(3)
]).then(console.log) // write:1 Uncaught (in promise) 1

Promise.allSettled

만약 하나라도 실패하면 all은 거부하는데, 굳이 크리티컬 한 게 아니라면 일단 받아오는 게 좋지 않을까요? 그럴 때 사용합니다. 이 친구는 어찌되었든 실행을 하면 결과를 모두 전달해줘요.

예제
Promise.allSettled([
  Promise.resolve(1),
  Promise.reject(2),
  Promise.resolve(3)
]).then(console.log)

/* 
  [
	{status: 'fulfilled', value: 1}, 
	{status: 'rejected', reason: 2},
    {status: 'fulfilled', value: 3
  ]
*/

Promise.race

이 친구는 가장 빨리 처리되는 것을 받아와요. 아마 내부에는 resolve되는 순간 이를 바로 take할 수 있도록 하는 로직이 탑재되어 있을 것 같아요. (오, 나중에 한 번 구현해봐야겠어요!)

예제
Promise.race([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then(console.log) // 1

// 만약 에러가 처리되는 게 있다면 먼저 에러 처리된 것부터 반환합니다.
Promise.race([
  Promise.reject(1),
  Promise.reject(2),
  Promise.reject(3)
]).then(console.log) // write:1 Uncaught (in promise) 1

Promise.any

all에는 allSettled가 있으니, race에도 오류가 나도 가장 빠른 것을 take하는 메서드가 있겠죠?

그 친구가 바로 any입니다! MDN에 있는 예시를 보면, 이해가 쉽게 되실 거에요! 이 친구는 오류가 나도 가장 빨리 resolve되는 값을 갖고 옵니다. 만약 모두가 에러가 나면 AggregateError을 반환해요.


// 가장 빨리 오류가 발생
const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'quick'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, 'slow'));

const promises = [promise1, promise2, promise3];

Promise.any(promises).then((value) => console.log(value));

// 오류는 일단 제쳐두고, resolve가 가장 빨리 되는 것을 찾아냄.
// "quick"


Promise.any([promise1, promise1])
// AggregateError: All promises were rejected

Promise는 다른 비동기보다 우선적으로 처리한다.

우리, 이벤트 루프에서 마이크로 태스크 큐 이야기를 했죠?
그 마이크로 태스크 큐가 담당하는 친구가 바로 프로미스 객체입니다.

따라서, 이 친구는 태스크 큐보다 우선해서 처리가 돼요. (물론 예외는 있습니다)

🎉 마치며

프로미스가 헷갈릴 법 한데, 한 번 정리하고 나니까 그렇게 헷갈릴 게 아니었죠? 😉
저도 간만에 포폴 만들다가 공부를 했는데, 다시 헷갈리지 않을 것 같아서 기분이 좋아요.
역시 틈틈이 공부를 섞어서 해야 한다는 것을 다시금 느꼈어요!

눈 깜짝할 새 11월이네요. 이제 본격적으로 회사들에 지원해봐야겠어요.
다들 즐거운 공부하시길 바라며, 이상! 🌈

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉
post-custom-banner

0개의 댓글