ES6에서부터 이러한 문제점을 해결하기 위해 프로미스가 만들어졌다.
// 프로미스 생성
const promise = new Promise((resolve, reject) => {
// Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행한다.
if (/* 비동기 처리 성공 */) {
resolve('result');
} else { /* 비동기 처리 실패 */
reject('failure reason');
}
});
프로미스 객체는 new Promise()를 이용하여 생성할 수 있다. 프로미스는 콜백 함수를 인자로 전달받는데 이 콜백 함수가 받는 인자는 resolve와 reject이다.
resolve(value): 작업이 성공했을 때 값을 전달한다.
reject(error): 작업이 실패했을 때 에러 값을 전달한다.
프로미스는 다음과 같은 상태 정보를 갖는다.
| 상태 | 의미 | 상태 변경 조건 |
|---|---|---|
| pending | 비동기 처리가 아직 끝나지 않아 결과가 정해지지 않은 상태 | new Promise()로 생성된 직후의 기본 상태 |
| fulfilled(settled) | 비동기 처리가 성공적으로 수행됨 | resolve() 호출 |
| rejected(settled) | 비동기 처리가 실패함 | reject() 호출 |
프로미스는 비동기 작업이 완료된 후 여러가지 처리를 할 수 있도록 메서드를 제공한다.
// fulfilled
new Promise(resolve => resolve('fulfilled'))
.then(v => console.log(v), e => console.error(e)); // fulfilled
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.then(v => console.log(v), e => console.error(e)); // Error: rejected
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.catch(e => console.log(e)); // Error: rejected
new Promise(() => {})
.finally(() => console.log('finally')); // finally
then, catch를 사용해 에러 처리를 할 수 있지만 가독성을 위해 catch로 에러 처리하는 것을 권장한다. then과 달리 catch는 앞에서 발생한 모든 then의 에러를 한 번에 잡을 수 있다.
promiseGet('https://jsonplaceholder.typicode.com/todos/1')
.then(res => console.xxx(res))
.catch(err => console.error(err)); // TypeError: console.xxx is not a function
프로미스는 다음 코드와 같이 언제나 프로미스를 반환하므로 연속적으로 호출할 수 있다.이를 프로미스 체이닝이라고 한다.
const url = 'https://jsonplaceholder.typicode.com';
// id가 1인 post의 userId를 취득
promiseGet(`${url}/posts/1`)
// 취득한 post의 userId로 user 정보를 취득
.then(({ userId }) => promiseGet(`${url}/users/${userId}`))
.then(userInfo => console.log(userInfo))
.catch(err => console.error(err));
프로미스는 5가지 정적 메서드를 제공한다.
// 배열을 resolve하는 프로미스를 생성
const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [1, 2, 3]
// 에러 객체를 reject하는 프로미스를 생성
const rejectedPromise = Promise.reject(new Error('Error!'));
rejectedPromise.catch(console.log); // Error: Error!
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 ] ⇒ 약 3초 소요
.catch(console.error);
Promise.race([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
])
.then(console.log) // 3
.catch(console.log);
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}
]
*/
예전에는 제너레이터를 이용해 비동기 처리를 했지만 ES8이후 제너레이터보다 간단하고 가독성이 좋은 async / await 가 도입되었다. 프로미스 기반으로, 별개의 후속 처리가 필요없이 동기 처리처럼 프로미스를 사용할 수 있다.
// async 함수 선언문
async function foo(n) { return n; }
foo(1).then(v => console.log(v)); // 1
// async 함수 표현식
const bar = async function (n) { return n; };
bar(2).then(v => console.log(v)); // 2
// async 화살표 함수
const baz = async n => n;
baz(3).then(v => console.log(v)); // 3
// async 메서드
const obj = {
async foo(n) { return n; }
};
obj.foo(4).then(v => console.log(v)); // 4
// async 클래스 메서드
class MyClass {
async bar(n) { return n; }
}
const myClass = new MyClass();
myClass.bar(5).then(v => console.log(v)); // 5
await는 async 함수 내부에서 사용해야 한다.
다만 클래스의 constructor 메서드는 async 메서드가 될 수 없다. 서로 반환하는 값이 인스턴스와 프로미스로 각자 다르기 때문이다.
const fetch = require('node-fetch');
const getGithubUserName = async id => {
const res = await fetch(`https://api.github.com/users/${id}`); // ①
const { name } = await res.json(); // ②
console.log(name); // Ungmo Lee
};
getGithubUserName('ungmo2');
await 키워드는 프로미스가 처리 완료 상태가 될 때까지 대기했다가 처리 결과를 반환한다.
그러나 모든 프로미스에 await키워드를 사용하는 것은 지양해야 한다. 서로 연관이 없는 작업을 순서대로 처리하게 되면 시간 낭비가 발생하기 때문이다.
비동기 작업들이 서로 연관이 없는 경우에는 Promise.all 처럼 병렬적 처리를 하는 것이 좋다.
async function foo() {
const res = await Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)),
new Promise(resolve => setTimeout(() => resolve(2), 2000)),
new Promise(resolve => setTimeout(() => resolve(3), 1000))
]);
console.log(res); // [1, 2, 3]
}
foo();
async await에서 에러 처리는 try catch문을 사용할 수 있다.
async 함수 내에서 catch문을 사용해서 에러 처리를 하지 않으면 async 함수는 발생한 에러를 reject하는 프로미스를 반환한다. 따라서 async함수를 호출하고 Promise.prototype.catch 후속 처리 메서드를 사용해 에러를 캐치할 수 있다
// 함수 안에서 try/catch
const fetch = require('node-fetch');
const foo = async () => {
try {
const wrongUrl = 'https://wrong.url';
const response = await fetch(wrongUrl);
const data = await response.json();
console.log(data);
} catch (err) {
console.error(err); // TypeError: Failed to fetch
}
};
foo();
// 호출하는 쪽에서 .catch
const fetch = require('node-fetch');
const foo = async () => {
const wrongUrl = 'https://wrong.url';
const response = await fetch(wrongUrl);
const data = await response.json();
return data;
};
foo()
.then(console.log)
.catch(console.error); // TypeError: Failed to fetch
| 코드 1 (함수 안에서 try/catch) | 코드 2 (호출하는 쪽에서 .catch) | |
|---|---|---|
| 에러를 잡는 곳 | foo 내부 | foo를 호출한 쪽 |
foo()가 돌려주는 프로미스 | fulfilled (값은 undefined) | rejected |
| 호출한 쪽이 실패를 아는가 | 모름 | 앎 |