# Promise 프로미스
프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체
미래에 값을 반환할 수도 있는 함수를 캡슐화한 객체
미래에 하나의 값을 받을 수 있는 인터페이스
# 비동기 함수란?
비동기 함수란 함수 내부에 비동기로 동작하는 코드를 포함한 함수를 말한다.
비동기 함수를 호출하면 함수 내부의 비동기로 작동하는 코드가 완료되지 않았다고 해도 기다리지 않고 즉시 종료된다. 즉, 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료된다. 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 소코프의 변수에 할당하면 기대한 대로 동작하지 않는다.
# Why Promise?
자바스크립트는 Promise가 등장하기 이전 비동기 처리를 위해 콜백 함수를 사용했다. 하지만 전통적인 콜백 패턴은 여러 단점을 가지고 있었는데,
# 콜백 헬
콜백 함수를 통해 비동기 처리 결과에 대한 후속 처리를 수행하는 비동기 함수가 비동기 처리 결과를 가지고 또 다시 비동기 함수를 호출해야 한다면 콜백 함수가 중첩되어 복잡도가 높아지는 현상이 발생한다.
# 에러 처리의 한계
비동기 처리에서 가장 어려운 것은 에러 처리가 곤란하다는 것이다.
try {
setTimeout(() => {
throw new Error('Error!');
}, 1000);
} catch (e) {
// 에러 캐치 못 함
console.log('캐치한 에러', e);
}
setTimeout의 콜백이 실행될 때 setTimeout 함수는 이미 콜 스택에서 제거된 상태다. 이것은 setTimeout의 콜백을 실행하는 것이 setTimeout 함수가 아니라는 것을 의미한다. setTimeout 함수의 콜백 함수의 호출자가 setTimeout 함수라면 클 스택의 현재 실행 중인 실행 컨텍스트가 콜백 함수의 실행 컨텍스트일 때** 현재 실행 중인 실행 컨텍스트의 하위 실행 컨텍스트가 setTimeout 함수여야 한다.**
에러는 호출자 방향으로 전파된다. 즉, 콜 스택의 아래 방향(실행 중인 실행 컨텍스트가 푸시되기 직전에 푸시된 실행 컨텍스트 방향)으로 전파된다. 하지만 앞에서 살펴본 바와 같이 setTimeout 함수의 콜백을 호출한 것은 setTimeout이 아니다. 따라서 setTimeout함수의 콜백이 발생시킨 에러는 catch 블록에서 캐치되지 않는다.
이렇게 기존 콜백의 단점인 콜백 헬과 에러 처리의 어려운 문제를 극복하기 위해 Promise가 도입되었다.
# 프로미스의 생성
Promise 생성자 함수를 new 연산자와 함께 호출하면 프로미스 객체를 생성한다. 프로미스는 호스트 객체가 아닌 표준 빌트인 객체다.
Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 인수로 전달받는데 이 콜백 함수는 resolve와 reject함수를 인수로 전달받는다.
const promise = new Promise((resolve, reject) => {
// Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행한다.
if (isSuccess) {
resolve();
} else {
reject();
}
});
# 프로미스의 상태
프로미스는 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태 정보를 갖는다.
- pending: 비동기 처리가 아직 수행되지 않은 상태. 프로미스가 생성된 직후 기본 상태
- fulfilled: 비동기 처리가 성공적으로 수행된 상태 -> resolve 함수 호출
- rejected: 비동기 처리가 실패한 상태 -> reject 함수 호출
생성된 직후의 프로미스는 기본적으로 pending 상태다. 이후 비동기 처리가 수행되면 비동기 처리 결과에 따라 프로미스의 상태가 변경된다.
- 비동기 처리 성공: resolve 함수를 호출해서 프로미스를 fulfilled 상태로 변경한다.
- 비동기 처리 실패: reject 함수를 호출해서 프로미스를 rejected 상태로 변경한다.
fulfilled 혹은 rejected 상태를 settled 상태라고 한다. settled 상태는 성공 실패 여부와 상관 없이 pending이 아닌 상태로 비동기 처리가 수행된 상태를 뜻한다.
# 프로미스의 후속 처리 메서드
then, catch, finally
프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출된다. 이때 후속 처리 메서드의 콜백 함수에 프로미스의 처리 결과가 인수로 전달된다.
# then
then 메서드는 두 개의 콜백 함수를 인수로 전달받는다.
- 첫 번째 콜백 함수는 프로미스가 fulfilled 상태(resolve 함수가 호출된 상태)가 되면 호출된다. 이때 콜백 함수는 프로미스의 비동기 처리 결과를 인수를 전달받는다.
- 두 번째 콜백 함수는 프로미스가 rejected 상태(reject 함수가 호출된 상태)가 되면 호출된다. 이때 콜백 함수는 프로미스의 에러를 인수로 전달받는다.
즉, 첫 번째 콜백 함수는 비동기 처리가 성공했을 때 호출되는 성공 처리 콜백 함수이며, 두 번째 콜백 함수는 비동기 처리가 실패했을 때 호출되는 실패 처리 콜백 함수다.
then 메서드는 언제나 프로미슬르 반환한다. 만약 then 메서드의 콜백함수가 프로미스를 반환하면 그 프로미스를 그대로 반환하고, 콜백 함수가 프로미스가 아닌 값을 반환하면 그 값을 암묵적으로 resolve 또는 reject 하여 프로미스를 생성해 반환한다.
# catch
catch 메서드는 한 개의 콜백 함수를 인수로 전달받는다. catch 메서드의 콜백 함수는 프로미스가 rejected 상태인 경우만 호출된다.
# finally
finally 메서드는 한 개의 콜백 함수를 인수로 전달받는다. finally 메서드의 콜백 함수는 프로미스의 성공 또는 실패 여부와 상관없이 무조건 한 번 호출된다. finally 메서드도 then/catch 메서드와 같이 언제나 프로미슬르 반환한다.
# 프로미스의 한계
프로미스는 프로미스 체이닝을 통해 비동기 처리 결과를 전달받아 후속 처리를 하므로 비동기 처리를 위한 콜백 패턴에서 발생하던 콜백 헬이 발생하지 않는다. 다만 프로미스도 콜백 패턴을 사용하므로 콜백 함수를 사용하지 않는 것은 아니다.
콜백 패턴은 가독성이 좋지 않은데, 이 문제는 ES8에서 도입된 async/await을 통해 해결할 수 있다. async/await를 사용하면 프로미스의 후속 처리 메서드 없이 마치 동기 처리처럼 프로미스가 처리 결과를 반환하도록 할 수 있다.
# 프로미스의 정적 메서드
프로미스는 주로 생성자 함수로 사용되지만 함수도 객체이므로 메서드를 가질 수 있다. 프로미스는 5개의 정적 메서드를 제공한다.
# Promise.resolve
# Promise.reject
# Promise.all
여러 개의 비동기 처리를 모두 병렬 처리할 때 사용한다.
프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달받는다. 그리고 전달받은 모든 프로미스가 모두 fulfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스를 반환한다. 그리고 그 처리 시간은 가장 늦게 fulfilled 되는 프로미스의 완료 시간보다 조금 더 길다.
다만 배열 중 하나라도 rejected 상태가 되면 나머지 프로미스가 fulfilled 되는 것을 기다리지 않고 즉시 종료한다.
# Promise.allSettled
위와 거의 비슷하게 동작한다. IE를 제외한 거의 모든 브라우저에서 지원한다. 다만 차이점은 중간에 하나라도 실패할 경우에도 모든 프로미스를 처리해서 결과를 배열에 담아 리턴한다.
# Promise.race
Promise.all 메서드처럼 모든 프로미스가 fulfilled 상태가 되는 것을 기다리지 않고 가장 먼저 fulfilled 상태가 된 프로미스의 처리 결과를 resolve하는 새로운 프로미스를 반환한다.
Promise.race([
new Promise((resove) => setTimeout(() => resolve(1), 3000)),
new Promise((resove) => setTimeout(() => resolve(2), 2000)),
new Promise((resove) => setTimeout(() => resolve(3), 1000)),
])
.then(console.log) // 3
.catch(console.log);
프로미스가 하나라도 rejected되면 Promise.all 메서드와 동일하게 그 즉시 에러를 reject하는 새로운 프로미스를 반환한다.
# 마이크로태스크 큐
프로미스의 후속 처리 메서드의 콜백 함수는 태스크 큐가 아니라 마이크로태스크 큐에 저장된다.
마이크로태스크 큐는 태스크 큐와는 별도의 큐다. 마이크로태스크 큐에는 프로미스의 후속 처리 메서드의 콜백 함수가 일시 저장된다. 그 외의 비동기 함수의 콜백 함수나 이벤트 핸들러는 태스크 큐에 일시 저장된다.
콜백 함수나 이벤트 핸들러를 일시 저장한다는 점에서 태스크 큐와 동일하지만 마이크로태스크 큐는 태스크 큐보다 우선순위가 높다. 즉, 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에서 대기하고 있는 함수를 가져와 실행한다. 이후 마이크로태스크 큐가 비면 태스크 큐에서 대기하고 있는 함수를 가져와 실행한다.
← 함수 Generator 제너레이터 →