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();
우리, 일반적인 비동기의 상황을 떠올려 볼까요?
일단 코드가 비동기면 그냥 넘어갈테고... 스케줄링에 따라 서버에서 통신을 할 거에요.
그러다 데이터를 받으면 받은 데이터를 메모리에 저장하겠죠?
그런데 반대로 오류가 나면, 에러를 호출하여 메시지를 전달해줄 거에요.
즉, 비동기에는 크게
가 존재합니다.
일단 이것이 중요해요.
Promise은 이러한 상태에 따라 애플리케이션을 제어해주기 용이하기 위해 나온 것입니다!.
따라서 대기는 서버에서 기다리는 역할이니 크게 설정할 것은 없고, 성공과 실패했을 때의 로직 처리가 중요하겠죠?
따라서 이 친구는 콜백 함수를 받는데요, 콜백 함수는 다음 2가지를 인수로 전달 받아요.
resolve
: 해결했을 시 resolve
와 함께 데이터를 전달하면 돼요!reject
: 실패했을 시에는 reject
와 함께 에러를 전달해주면 돼요!기존의 콜백의 문제는 무엇이었죠? 바로 콜백 지옥과 같이 너무나 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
객체로 만들기 위해 사용해요.
Promise.resolve()
Promise
에는 여러 비동기 로직의 결과를 처리할 유용한 메서드들이 있는데요. 대표적인 것이 all
과 allSettled
, race
에요.
이 친구들은 다음과 같은 특징을 지녀요.
이터러블을 받고, 동시에 병렬적으로 수행해요. 만약 실패한 게 있다면 제일 먼저 에러 나온 것을 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
만약 하나라도 실패하면 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
]
*/
이 친구는 가장 빨리 처리되는 것을 받아와요. 아마 내부에는 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
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
우리, 이벤트 루프에서 마이크로 태스크 큐 이야기를 했죠?
그 마이크로 태스크 큐가 담당하는 친구가 바로 프로미스 객체입니다.
따라서, 이 친구는 태스크 큐보다 우선해서 처리가 돼요. (물론 예외는 있습니다)
프로미스가 헷갈릴 법 한데, 한 번 정리하고 나니까 그렇게 헷갈릴 게 아니었죠? 😉
저도 간만에 포폴 만들다가 공부를 했는데, 다시 헷갈리지 않을 것 같아서 기분이 좋아요.
역시 틈틈이 공부를 섞어서 해야 한다는 것을 다시금 느꼈어요!
눈 깜짝할 새 11월이네요. 이제 본격적으로 회사들에 지원해봐야겠어요.
다들 즐거운 공부하시길 바라며, 이상! 🌈