자바스크립트 프로미스(Promise)
자바스크립트에서 비동기 작업을 처리하는 방식으로 대표적으로 콜백 함수가 있다.
콜백 함수란 함수에 매개변수로 넘겨준 함수를 말하는데, 매개변수로 넘겨 받은 함수는 일단 넘겨 받고 때가 되면 나중에 호출하는 것이 콜백의 개념이다.
[예시코드]
function getData(callback) {
let title = "";
fetch(url)
.then((res) => res.json())
.then((json) => callback(json));
}
function callBackFunc(data) {
title = data[0].title;
console.log(title);
}
콜백의 한계
콜백을 이용해 전달 받은 결과 값을 그대로 다시 비동기 요청을 하기 위해선 콜백 내부에 다시 콜백 함수를 사용해야 되는데, 이때 콜백의 개수가 많아지면 내부에 존재하는 콜백의 깊이가 깊어지게 되는데 이러한 현상을 콜백 지옥이라고 한다.
가독성도 안좋고 에러가 발생한다면 어디서 발생했는지도 헷갈릴 것 같다.
콜백 지옥하면 떠오르는 가장 유명한 이미지가 아닐까 싶다..
이를 해결하기 위해 또 다른 비동기 처리 패턴으로 ES6에서 Promise가 등장했다.
Promise
Promise 생성자 함수를 new 연산자와 함께 호출하면 프로미스 객체를 생성한다.
Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 매개 변수로 전달 받는데 이 콜백 함수는 resolve와 reject 함수를 매개 변수로 전달 받는다.
const promise = new Promise((resolve, reject) => {
try {
// 비동기 작업 처리
resolve(결과);
} catch (err) {
reject(err);
}
});
Promise가 전달 받은 콜백 함수 내부에서 비동기 처리를 하고, 성공하면 결과를 전달하면서 resolve 함수를 호출하고, 실패하면 에러를 전달하면서 reject 함수를 호출한다.
프로미스 상태
프로미스는 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태 정보를 갖는다.
pending - 비동기 처리가 아직 수행되지 않은 상태
fulfilled - 비동기 처리 성공
rejected - 비동기 처리 실패
프로미스는 생성된 직후에는 pending 상태이고 비동기 처리가 수행되면서 처리 결과에 따라 상태가 변경된다.
프로미스 객체가 비동기 처리를 하면 성공하거나 실패하거나 할텐데, 그 결과를 가지고 무언가를 해줘야할 것이다.
그래서 프로미스는 후속 처리를 위한 메서드로 then, catch, finally를 제공한다.
위에서 살펴본 프로미스 객체는 다음과 같이 결과를 받는다.
promise
.then((result) => {
// result 처리
}).catch((err) => {
console.error(err);
}).finally(()=> cosnole.log('성공 실패랑 상관없이 무조건 한 번 호출');
resolve(결과)는 then의 result로 가고, reject(err)는 catch의 err로 간다.
finally는 비동기 처리 성공, 실패 여부와 상관없이 무조건 한 번 호출한다.
Promise에서의 에러 처리
Promise에서 에러 처리에는 두 가지 방법이 있다.
첫 번째로는 then()의 두 번째 파라미터로 처리하는 방법이 있고, 두 번째로는 catch()로 에러를 처리하는 방법이 있다.
function getData(){
return new Promise((resolve,reject) => {
reject("ERROR!!");
});
}
// 1. then()의 두 번째 파라미터로 처리하는 방법
getData()
.then(res=>console.log(res))
.then(_,err=>console.log(err))
);
// 2. catch()로 에러를 처리하는 방법
getData().then().catch((err)=>{
console.log(err);
})
첫 번째 방법은 then()의 첫 번째 콜백 함수 내부에서 오류가 나면 오류를 제대로 잡아내지 못한다.
반면에, 두 번째 방법은 똑같은 오류를 catch()로 처리하면 then()에서의 에러와 then() 호출 이후에 에러 모두 잡아낼 수 있다.
따라서 두 번째 방법으로 에러 처리를 하는 것이 더 효율적이다.
그런데 Promise에서도 단점이 있다.
const add =(val)=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(val+10);
},1000);
})
}
add(5).then((res)=>{
console.log(`first : ${res}`);
return add(res);
}).then((res)=>{
console.log(`second : ${res}`);
return add(res);
}).then((res)=>{
console.log(`third : ${res}`);
}).catch(err=>console.log(err));
위의 예제 코드와 같이 Promise로 복잡한 비동기 작업을 처리하면 then()이 계속 반복적으로 나오게 되고 맨 마지막 catch에서 에러 처리가 일어나기 때문에 정확히 어디서 에러가 발생했는지 알기 어렵다.
Promise.all, Promise.allSettled, Promise.race 라는 것도 있다.
Promise.all
Promise.all의 특징은 다음과 같다.
-여러 개의 비동기 처리를 모두 병렬 처리 할 때 사용한다.(여러 개의 비동기 처리가 서로 의존하지 않고 개별적으로 수행 되어야 할 때)
-프로미스를 요소로 갖는 배열을 파라미터로 받는다.
-파라미터로 전달 받은 배열의 모든 프로미스가 fulfilled 상태가 되면 종료한다.
다음 예제를 보자
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]
.catch(console.err);
requestData3 -> requestData2 -> requestData1 순서대로 resolve 하는데, 가장 마지막으로 requestData1이 resolve하고 나서야 처리 결과([1,2,3])을 배열에 저장해 새로운 프로미스를 반환한다.
이때 걸리는 시간은 1+2+3이 아니라 가장 오래 걸리는 프로미스(requestData1)의 시간인 3초가 소요된다.
또 다른 예제를 보자
Promise.all([
new Promise((_, reject) =>
setTimeout(() => reject(new Error("error 1")), 3000)
),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("error 2")), 2000)
),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("error 3")), 1000)
),
])
.then(console.log) // error 3
.catch(console.log);
전달받은 배열의 프로미스 중 하나라도 rejected 상태가 되면 나머지 프로미스 상태가 어떻든간에 즉시 종료한다.
즉, Promise.all은 모든 프로미스가 성공적으로 fulfilled 되어야 성공으로 간주하고 하나라도 실패하면 즉시 종료한다.
Prormise.allSettled
const p1 = () =>
new Promise((resolve) => setTimeout(() => resolve(1), 3000));
const p2 = () =>
new Promise((resolve) => setTimeout(() => resolve(2), 2000));
const p3 = () =>
new Promise((_,reject) => reject(new Error("error")));
Promise.allSettled([p1(),p2(),p3()]).then((results) => {
results.forEach((result) => {
console.log(result);
})
})
// {status: 'fulfilled', value: 1}
// {status: 'fulfilled', value: 2}
// {status: 'rejected', reason: Error: error
// at <anonymous>:8:36
// at new Promise (<anonymous>)
// at p3 (<anonymous>:8:3)
// …}
Promise.all에서는 하나라도 rejected 상태가 되면 그냥 다 종료가 되어 다시 요청을 시도를 해야했는데, Promise.allSettled를 사용하면 result에 성공, 실패 상관없이 모든 프로미스의 결과 값이 나오게 된다.
Promise.race
Promise.all 과 비슷하지만 가장 먼저 fulfilled 상태가 되거나 rejected 상태가 된 프로미스의 처리 결과를 resolve하는 프로미스를 반환한다.