
자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다. 하지만 전통적인 콜백 패턴은 콜벡 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러의 처리가 곤란하여 여러 개의 비동기 처리를 한번에 처리하는 데도 한계가 있다.
ES6에서는 비동기 처리를 위한 또 다른 패턴으로 프로미스(Promise)를 도입했다. 프로미스는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.
콜백 패턴은 함수의 결과를 비동기적으로 처리할 수 있다는 장점이 있다. 하지만 함수가 중첩될수록 코드의 깊이가 증가하며, 이로 인해 “콜백 헬”이라고 불리는 가독성 문제가 발생하게 된다. 이는 유지 보수 및 코드 흐름 파악을 어렵게 만든다.
setTimeout(() => {
console.log('1초 경과');
setTimeout(() => {
console.log('2초 경과');
setTimeout(() => {
console.log('3초 경과');
}, 1000);
}, 1000);
}, 1000);
콜백 패턴에서는 에러를 처리하려면 각 단계마다 명시적으로 에러 처리 로직을 추가해야 한다. 에러가 발생하면 어디에서 처리해야 할지 모호한 경우도 많다.
프로미스는 Promise 생성자 함수를 사용하여 만들 수 있다. 생성자 함수는 두 개의 매개변수, resolve 와 reject 를 가진 콜백함수를 인수로 받는다.
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('성공');
} else {
reject('실패');
}
});
Promise 생성자 함수가 인수로 전달받은 콜백 함수 내부에서 비동기 처리를 수행한다.
프로미스는 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태 정보를 갖는다.
| 프로미스의 상태 정보 | 의미 | 상태 변경 조건 |
|---|---|---|
| pending | 비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 기본 상태 |
| fulfilled | 비동기 처리가 수행된 상태(성공) | resolve 함수 호출 |
| rejected | 비동기 처리가 수행된 상태(실패) | reject 함수 호출 |
resolve 함수를 호출해 프로미스를 fulfilled 상태로 변경한다.reject 함수를 호출해 프로미스를 rejected 상태로 변경한다.이처럼 프로미스의 상태는 resolve 또는 reject 함수를 호출하는 것으로 결정된다.
fulfilled 또는 rejected 상태를 settled 상태라고 한다. settled 상태는 fulfilled 또는 rejected 상태와 상관없이 pending 이 아닌 상태로 비동기 처리가 수행된 상태를 말한다.
프로미스는 비동기 처리 상태가 변화하면 이에 따른 후속 처리를 해야한다. 이를 위해 프로미스는 후속 처리 메서드 then ,catch , finally 를 제공한다.
프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달할 콜백 함수가 선택적으로 호출된다. 이때 후속 처리 메서드의 콜백 함수에 프로미스의 처리 결과가 인수로 전달된다.
모든 후속 처리 메서드는 프로미스를 반환하며, 비동기로 동작한다.
fulfilled 상태가 되면 호출된다.rejected 상태가 되면 호출된다.rejected 상태인 경우만 호출된다.promise
.then(result => {
console.log(result); // "성공"
})
.catch(error => {
console.error(error); // "실패"
})
.finally(() => {
console.log("end"); // "end"
});
비동기 처리를 위한 콜백 패턴은 에러 처리가 곤란하다는 문제가 있다. 하지만 프로미스는 catch 메서드를 사용하여 체인 중간에 발생한 모든 에러를 한 곳에서 처리할 수 있다.
new Promise((resolve, reject) => {
throw new Error('에러 발생');
})
.then(() => console.log('성공'))
.catch(error => console.error(error)); // "에러 발생"
프로미스는 then 메서드를 통해 연속적으로 호출할 수 있어, 단계적인 비동기 처리를 간단하게 작성할 수 있다.
new Promise(resolve => resolve(1))
.then(result => result * 2)
.then(result => result * 3)
.then(result => console.log(result)); // 6
Promise는 주로 생성자 함수로 사용되지만 함수도 객체이므로 메서드를 가질 수 있다. Promise는 5가지 정적 메서드를 제공한다.
즉시 이행 / 거부 된 프로미스를 생성한다.
Promise.resolve('즉시 완료').then(result => {
console.log(result); // "즉시 완료"
});
Promise.reject('즉시 실패').catch(error => {
console.error(error); // "즉시 실패"
});
여러 프로미스를 병렬로 실행하고 모든 프로미스가 완료될 때 결과를 반환한다.
const promise1 = Promise.resolve(10);
const promise2 = new Promise(resolve => setTimeout(() => resolve(20), 1000));
const promise3 = new Promise(resolve => setTimeout(() => resolve(30), 2000));
Promise.all([promise1, promise2, promise3]).then(results => {
console.log(results); // [10, 20, 30]
});
모든 프로미스가 완료될 때 (성공 또는 실패 상관없이) 각각의 상태와 결과를 반환한다.
const promise1 = Promise.resolve('성공');
const promise2 = Promise.reject('실패');
const promise3 = new Promise(resolve => setTimeout(() => resolve('완료'), 1000));
Promise.allSettled([promise1, promise2, promise3]).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: '성공' },
// { status: 'rejected', reason: '실패' },
// { status: 'fulfilled', value: '완료' }
// ]
});
가장 먼저 완료된 프로미스의 결과를 반환한다.
const promise1 = new Promise(resolve => setTimeout(() => resolve('1초 후 완료'), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve('2초 후 완료'), 2000));
Promise.race([promise1, promise2]).then(result => {
console.log(result); // "1초 후 완료"
});
성공한 프로미스 중 가장 먼저 완료된 결과를 반환한다. 모든 프로미스가 실패할 경우 AggregateError 를 발생시킨다.
const promise1 = Promise.reject('실패1');
const promise2 = Promise.reject('실패2');
const promise3 = new Promise(resolve => setTimeout(() => resolve('성공'), 1000));
Promise.any([promise1, promise2, promise3]).then(result => {
console.log(result); // "성공"
}).catch(error => {
console.error(error);
});
setTimeout(() => console.log(1), 0);
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
위 코드는 프로미스의 후속 처리 메서드도 비동기로 동작하므로 1 → 2 → 3의 순으로 출력될 것 처럼 보이지만 2 → 3 → 1 의 순으로 출력된다. 그 이유는 프로미스의 후속 처리 메서드의 콜백 함수는 태스크 큐가 아니라 마이크로태스크 큐(Microtask queue)에 저장되기 때문이다.
마이크로태스크 큐는 태스크 큐보다 우선순위가 높다. 즉, 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에서 대기하고 있는 함수를 가져와 실행한다. 이후 마이크로태스크 큐가 비면 태스크 큐에서 대기하고 있는 함수를 가져와 실행한다.