자바스크립트에서 코드가 순차적으로 실행되는 처리를 동기 처리라 하며, 특정 코드가 처리될 때 까지 기다리지 않고 다음 코드를 먼저 처리하는 것을 비동기 처리라 한다.
네트워크에서 데이터를 받아오거나 파일에서 큰 데이터를 읽어오는 과정은 꽤 오랜 시간이 소요된다. 그런 과정을 동기적으로 처리하게 되면 이 작업을 하는동안 다음 라인의 코드가 실행되지 않기 때문에, 시간이 꽤 소요되는 일이라면 비동기적으로 처리하는 것이 좋다.
자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용하는데 콜백 패턴은 아래와 같은 문제점들을 가지고 있다.
ES6에서는 이런 콜백 함수의 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있는 Promise를 도입했다.
Promise는 정해진 기능을 수행하고 나서 정상적으로 기능이 수행되었다면 성공의 메세지와 함께 처리된 결과값을 전달하고, 기능을 수행하다가 예상치못한 문제가 발생했다면 에러를 전달한다.
Promise는 클래스이기 때문에 new와 함께 생성하고, 두 개의 콜백 함수를 받는다.
하나는 fulfilled 상태에서 실행되는 resolve 함수이고, 다른 하나는 rejected 상태일 경우 실행되는 reject 함수이다.
// 일반 함수
let promise = new Promise(function(resolve, reject) {
// 비동기 작업 로직
});
// 화살표 함수
let promise = new Promise((resolve, reject) => {
// 비동기 작업 로직
});
Promise는 다음과 같이 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태 정보를 갖는다.
State: pending → fulfilleed or rejected
Promise로 구현된 비동기 함수를 호출하는 측에서는 Promise 객체의 후속 처리 메서드를 통해 비동기 처리 결과 또는 에러 메세지를 전달받아 처리한다. 이를 위해 Promise는 후속 메서드 then, catch, finally를 제공한다. 모든 후속 처리 메서드는 모두 Promise를 반환하기 때문에 체이닝이 가능하다.
두 개의 콜백 함수를 인수로 전달받는다.
// fulfilled
new Promisee(resolve => resolve('fulfilled'))
.then(v => console.log(v), e => console.error(e));
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.then(v => console.log(v), e => console.error(e));
catch 메서드는 한 개의 콜백 함수를 인수로 전달받는다.
Promise가 rejected 상태인 경우에만 호출된다.
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.catch(e => console.log(e));
성공/실패 여부와 상관없이 프로미스가 처리되면 호출된다.
Promise의 상태와 상관없이 공통적으로 수행해야 할 처리 내용이 있을 때 유용하다.
new Promise(() => {})
.finally(() => console.log('finally'));
Promise를 이용한 에러 처리 방법은 두 가지가 있다.
에러 처리는 가급적 catch 메서드를 사용하는 것을 권장한다.
then 메서드의 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못하고 코드가 복잡해져서 가독성이 좋지 않다.
catch 메서드는 모든 then 메서드를 호출한 이후에 호출하면 비동기 처리에서 발생한 에러(rejected 상태) 뿐만 아니라 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다.
또한 catch 메서드를 사용하는 것이 가독성이 좋고 명확하기 때문에 에러 처리는 catch 메서드에서 하는 것을 권장한다.
Promise는 주로 생성자 함수로 사용되지만 함수도 객체이므로 메서드를 가질 수 있다.
여러 개의 비동기 처리를 모두 병렬 처리할 때 사용한다.
Promise를 요소로 갖는 배열 등의 이터러블을 인수로 전달받아 전달받은 모든 Promise가 fulfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 Promise를 반환한다.
첫 번째 프로미스가 가장 나중에 fulfilleed 상태가 되어도 Promise.all 메서드는 첫 번째 Promise가 resolve한 처리 결과부터 차례대로 배열에 저장해 그 배열을 resolve하는 새로운 Promise를 반환한다. 즉, 처리 순서가 보장된다.
인수로 전달받은 배열의 Promise가 하나라도 rejected 상태가 되면 나머지 Promise가 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));
// 순차적으로 처리
// [1, 2, 3] => 약 6초 소요
const res = [];
requestData1()
.then(data => {
res.push(data);
return requestData2();
})
.then(data => {
res.push(data);
return requestData3();
})
.then(data => {
res.push(data);
console.log(res);
})
.catch(console.error);
// 병렬적으로 처리
// [ 1, 2, 3 ] => 약 3초 소요
Promise.all([requestData1(), requestData2(), requestData3()])
.then(console.log)
.catch(console.error);
Promise를 요소로 갖는 배열 등의 이터러블을 인수로 전달받아 가장 먼저 fulfilled 상태가 된 Promise의 처리 결과를 resolve하는 새로운 Promise를 반환한다.
전달된 Promise가 하나라도 rejected 상태가 되면 에러를 reject하는 새로운 Promise를 즉시 반환한다.
Promise.race([
new Promise(resolve => setTimeout(() => resolve(1), 3000)),
new Promise(resolve => setTimeout(() => resolve(2), 2000)),
new Promise(resolve => setTimeout(() => resolve(3), 1000)),
])
.then(console.log) // 3
.catch(console.log);
Promise를 요소로 갖는 배열 등의 이터러블을 인수로 전달받아 전달받은 모든 Promise가 모두 settled 상태(비동기 처리가 수행된 상태. 즉 fulfilled 또는 rejected 상태)가 되면 처리 결과를 배열로 반환한다.
Promise 처리 결과를 나타내는 객체는 다음과 같다.
Promise.allSettled([
new Promise(resolve => setTimeout(() => resolve(1), 2000)),
new Promise((_, reject) => setTimeout(() => reject(new Error('Error!')), 1000))
]).then(console.log);
/*
[
{status: "fulfilled", value: 1},
{status: "rejected", reason: Error: Error! at <anonymous>:3:54}
]
*/
Promise의 후속 처리 메서드의 콜백 함수는 마이크로태스크 큐에 저장된다.
마이크로태스크 큐는 태스크 큐와는 별도의 큐다.
마이크로태스크 큐에는 프로미스의 후속 처리 메서드의 콜백 함수가 일시 저장된다. 그 외의 비동기 함수의 콜백 함수나 이벤트 핸들러는 태스크 큐에 일시 저장된다.
마이크로 태스크 큐는 태스크 큐보다 우선순위가 높다. 마이크로태스크 큐에서 대기하고 있는 함수를 먼저 실행하고, 이후 마이크로태스크 큐가 비면 태스크 큐에서 대기하고 있는 함수를 가져와 실행한다.
// 2 -> 3 -> 1 순으로 출력
setTimeout(() => console.log(1), 0);
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
참고 자료
youtube 드림코딩 https://youtu.be/JB_yU6Oe2eE
이웅모, 『모던 자바스크립트 Deep Dive』, 위키북스