Fetch API나 Axios를 이용해 비동기 처리를 해본 경험이 있다면 프로미스를 접해 본적이 있을 것이다.
프로미스는 무엇이고 왜 Fetch는 비동기 통신의 결과로 프로미스를 반환할까?
자바스크립트는 전통적으로 비동기 처리를 위해 콜백 함수를 이용하는 패턴을 사용해왔다. 하지만 콜백 함수를 이용한 비동기 처리방식은 콜백함수의 가장 큰 문제인 콜백 지옥을 낳게 된다.
콜백 지옥
콜백 지옥은 콜백 함수의 중첩으로 인해 코드의 가독성이 떨어지는 것으로, 코드 유지보수에 악영향을 끼친다.
콜백 함수의 단점은 이뿐만이 아니다.
try {
setTimeout(() => {
throw new Error("에러!");
}, 1000);
// 비동기 함수인 setTimeout을 이용해서 에러를 발생하는 콜백 함수를 전달했다.
} catch (e) {
console.error(e);
}
위 코드는 콜백 함수에서 에러를 발생시키는 코드다. 작성자가 원했던 코드의 동작은 setTimeout가 1초 뒤 콜백 함수를 실행시키면 try/catch에서 에러를 캐치해 콘솔에 출력하기를 원했을 것이다.
하지만 이 에러는 캐치되지 않는다. 자바스크립트의 콜백 함수를 호출하는 것은 setTimeout이 아니기 때문이다.
이처럼 콜백 패턴은 에러 처리가 곤란하다는 단점이 있다.
참고 : 콜백 함수는 자바스크립트 엔진의 태스크 큐에 대기하다가 콜 스택이 비었을 경우 이벤트 루프에 의해서 실행된다. 콜 스택이 비었다는 말은 이미 setTimeout은 실행을 마치고 콜 스택에서 제거되었다는 뜻이다.
이러한 콜백 패턴의 단점을 보완하기 위해 ES6에서는 프로미스가 도입되었다.
프로미스는 비동기 처리를 수행할 콜백 함수를 인수로 전달받는다.
이 콜백 함수는 인수로 resolve와 reject함수를 인수로 가지는데 이는 추후 프로미스의 상태를 변경시켜서 프로미스 후속 처리 메서드가 실행될 수 있게 한다.
const promise = new Promise((resolve, reject)=> {
// 비동기 작업
if() { // 비동기 처리 성공 시
resolve("성공");
}
else { // 비동기 처리 실패 시
reject("실패");
}
})
프로미스는 다음과 같은 상태를 가지고 있다.
프로미스 상태 | 의미 | 조건 |
---|---|---|
pending | 비동기 처리가 완료되지 않은 상태 | resolve나 reject함수가 호출되기 전 기본상태 |
fulfilled | 비동기 처리가 성공적으로 수행된 상태 | 콜백 함수 내부에서 resovle 함수가 실행된 상태 |
rejected | 비동기 처리가 실패한 상태 | 콜백 함수 내부에서 reject 함수가 실행된 상태 |
이때까지의 프로미스의 진행 과정은 다음과 같다.
자 이제 프로미스의 목적을 떠올려 보자. 프로미스의 목적은 비동기 작업에 대한 결과를 처리하는 것이다.
3의 과정에서 비동기 작업은 종료되었고, 이제 결과를 처리할 일만 남았다.
프로미스의 상태가 pending에서 다른 상태로 변화하게 되면 상태의 종류에 따라 후속 처리 메서드로 전달한 콜백 함수가 호출된다.
const promise = new Promise((resolve, reject)=> {
// 비동기 작업
if() { // 비동기 처리 성공 시
resolve("성공");
}
else { // 비동기 처리 실패 시
reject("실패");
}
})
promise.then(
v => console.log(v), // 비동기 처리 성공 시 (resolve 함수 실행 시)
e => console.error(e) // 비동기 처리 실패 시 (reject 함수 실행 시)
);
then은 콜백 함수를 두개까지 받을 수 있다.
콜백 함수의 매개변수는 resolve,reject함수 호출 시 전달된 값이 인수로 전달된다.
catch 메서드는 프로미스의 상태가 rejected일 때만 호출된다. 즉 then의 두번 째 콜백 함수로 전달하는 것과 동일하게 동작한다.
promise.catch((e) => console.error(e));
비동기 처리에서 발생한 에러도 catch로 처리할 수 있기 때문에 프로미스를 사용하면 콜백 패턴에서 발생하는 에러 처리의 어려움을 해결할 수 있다.
프로미스 체이닝을 통해 then 메서드를 모두 호출한 이후에 catch를 호출하면 비동기 처리에서 발생한 에러 뿐만 아니라 then에서 발생한 에러도 한번에 처리할 수 있다.
finally 메서드는 프로미스의 성공, 실패와 상관없이 단 한번만 호출된다.
프로미스의 상태와 상관없이 공통적으로 수행해야 하는 작업이 있을 경우 유용하게 사용할 수 있다.
promise.finally(() => console.log("프로미스 완료!"));
후속 처리 메서드는 언제나 프로미스를 반환하는 특징이 있다, 만약 프로미스가 아닌 값을 반환하면 암묵적으로 resolve나 reject에 담아서 프로미스를 생성해 반환한다.
후속 처리 메서드는 항상 프로미스를 반환하기 떄문에 연속적으로 후속 처리 메서드를 호출할 수 있다. 이를 프로미스 체이닝 이라고 한다.
프로미스를 이용하면 여러 비동기 작업들을 병렬 처리할 수도 있다. 만약 각각의 비동기 처리가 서로에게 관련 없이 개별적으로 수행된다면, Promise.all메서드를 이용해서 병렬로 비동기 작업을 수행할 수 있다.
const promise1 = () => new Promise((resolve) => setTimeout(() => resovle(1), 3000));
const promise2 = () => new Promise((resolve) => setTimeout(() => resovle(2), 2000));
Promise.all([promise1(), promise2()]).then(console.log).catch(console.error);
Promise.all은 프로미스 배열(혹은 이터러블)을 인수로 전달받고,
모든 프로미스가 fullfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스를 반환한다.
Promise.all의 장점은 비동기 처리 작업의 순서를 보장한다는 것이다.
위의 예제에서 promise1
은 최소 3초 이상 걸릴 것이고, promise2
는 최소 2초 이상 걸릴 것이다.
대부분의 경우 promise2
가 먼저 끝날 것이다.
Promise.all은 프로미스가 먼저 끝난 순서가 아니라 메서드의 인자로 전달된 프로미스 배열의 순서 대로 그 결과를 저장하기 때문에 처리 순서를 보장한다.
all은 프로미스 중 하나라도 rejected 상태가 되면 나머지 프로미스가 아직 pending 상태일지라도 즉시 종료한다. 만약 모든 프로미스가 settled 상태가 될 때까지 종료되지 않기를 원한다면 es11에 도입된 allsettled 메서드를 사용하면 된다.
Promise.race는 Promise.all처럼 프로미스를 요소로 갖는 배열(혹은 이터러블)을 인수로 전달받는다.
race메서드의 차이점은 가장 먼저 fulfilled 상태가 되는 프로미스의 결과를 반환한다는 점이다.
Fetch API나 axios 같은걸 많이 사용하면서 then, catch 같은 메서드들을 많이 사용했지만,
누가 프로미스가 왜 필요한지, 프로미스가 어떻게 비동기 후속 처리를 하는지에 대한 질문에는 대답하지 못할 정도의 지식을 갖고 있었던 것 같다.
이전에 공부했던 파트지만 시간이 지나서 다시 보니 이해가 되는 부분도 많고 비동기 처리를 하는데 많은 도움이 될것 같다.