콜백 함수를 이용한 비동기 처리의 단점에서 살펴본 단점들을 해결하기 위해 프로미스를 이용해 비동기 처리를 할 수 있다.
Promise란, 자바스크립트 비동기 처리에 사용되는 객체로, 비동기 함수 호출 또는 비동기 연산이 완료되었을 때, 이후에 처리할 함수나 에러를 처리하기 위한 함수를 설정할 수 있다.
const promise = new Promise((resolve, reject) => {
if(/* 비동기 처리 성공 */){
resolve('result');
} else { /* 비동기 처리 실패 */
reject('failure reason');
}
});
promise().then((value)=> {
}).catch((value) => {
});
위와 같은 방식으로 Promise 객체를 만들 수 있고, Promise 객체를 반환하는 Promise() 함수의 인자로 콜백 함수를 선언할 수 있고, 이 콜백 함수의 인자로는 resolve와 reject가 올 수 있다.
resolve와reject함수 모두 특정 값을 다음 실행으로 전달해 비동기적으로 작업할 수 있도록 해주는 함수로, 우리가 이 함수를 새로 정의해주는 것이 아니라 이미 정의되어있는 이 함수들을 특정 인자와 함께 호출만 하는 것이다.
보통 Promise() 내 인수로 전달된 콜백 함수 내에서 비동기 처리를 수행하고, 이 비동기 처리의 성공과 실패에 따라 resolve 또는 reject를 실행한다.
resolve
promise 내부에서 비동기 상황이 정상적으로 종료될 때 실행시키는 함수로, resolve(value) 이렇게 resolve 함수의 인자로 value 값을 전달하면서 호출하면, 이 값이 then 구문의 콜백함수의 인자로 전달되어 then 구문으로 작업의 흐름을 이어갈 수 있다. 즉, resolve 함수는 비동기 상황이 종료될 때 특정 값을 다음 실행으로 전달하기 위해 사용하는 함수로, 프로미스의 상태를 fulfilled로 변경한다.
reject
promise 내부에서 비동기 상황이 비정상적으로 종료되거나 오류 상황일 때, 호출하는 함수로 reject(value)를 통해 value 값을 catch 구문에서 사용하며 실행을 이어갈 수 있다. reject는 프로미스의 상태를 rejected로 변경한다.
프로미스는 현재 비동기 처리가 어떻게 진행되고 있는지에 따라 상태 정보를 갖는다.
| 프로미스의 상태 정보 | 의미 | 상태 변경 조건 |
|---|---|---|
| pending | 비동기 처리가 아직 수행되지 않음 | 프로미스가 생성된 직후 기본 상태 |
| fulfilled | 비동기 처리가 성공함 | resolve 함수 호출 |
| rejected | 비동기 처리가 실패함 | reject 함수 호출 |
fulfilled 또는 rejected 상태를 settled 상태라고 한다. 비동기 처리가 수행된 상태를 말한다.
프로미스는 pending => settled 상태로 변화할 수 있지만, 일단 settled 상태가 되면 더는 다른 상태로 변화할 수 없다.
지난 글이었던 콜백 함수를 이용한 비동기 처리의 단점에서 보았던 GET 요청 코드를 프로미스를 이용해 작성해보고, 프로미스의 상태가 어떻게 변화하는지 살펴보자.
const promiseGet = url => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET, url');
xhr.send();
xhr.onload = () => {
if(xhr.status === 200){
resolve(JSON.parse(xhr.response))
} else {
reject(new Error(xhr.status));
}
};
});
};
promiseGet('baseURL/posts/1');
promiseGet('baseURL/posts/1');를 통해 생성된 직후의 프로미스는 기본적으로 pending 상태이다.이처럼 프로미스의 상태는 resolve 또는 reject 함수를 호출하는 것으로 결정된다.

위의 그림처럼, 프로미스는 비동기 처리 상태와 더불어 비동기 처리 결과도 가지고 있다.
const fulfilled = new Promise(resolve => resolve(7));
console.log(fulfilled);
위처럼 fulfilled된 프로미스의 코드를 개발자 도구에서 실행시켜보면

PromiseState와 더불어 PromiseResult라는 정보도 가지고 있음을 알 수 있다.
즉, 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체이다.
위에서 resolve와 reject를 통해 비동기 처리의 상태를 변화할 수 있음을 배웠고, 이렇게 상태가 변화하면 이에 따른 후속 처리를 해야한다. 이를 위해 프로미스 후속 메서드인 then, catch, finally를 사용할 수 있다.
new Promise(resolve => resolve('fulfilled'))
.then(v => console.log(v), e => console.error(e));
then 메서드는 두 개의 콜백함수를 인수로 받을 수 있다.
첫번째 콜백함수
프로미스가 fulfilled 상태가 되면 호출된다. 이 때 콜백함수는 프로미스의 비동기 처리 결과를 인자로 전달받는다.
위의 개발자도구에서 살펴봤듯이 promise가 가지고 있는 PromiseResult를 인자로 전달해주는 것이다. 위 코드에서는
'fulfilled'값이 콜백함수의 인자로 전달되는 것이다.
두번째 콜백함수
프로미스가 rejected 상태가 되면 호출된다. 이 때에도 콜백함수는 프로미스의 비동기 처리 결과를 인자로 전달받는다.
then 메서드는 언제나 프로미스를 반환한다.
- 만약 then 메서드의 콜백 함수가 프로미스를 반환하면 그 프로미스를 그대로 반환하고
- 콜백 함수가 프로미스가 아닌 값을 반환하면 그 값을 암묵적으로 resolve 또는 reject 하여 프로미스를 생성해 반환한다.
여기서도 우리는 이미 정의된 then, catch, finally 메서드에 인자만 넣어서 호출할 뿐이다. 따라서 then을 호출할 때 콜백함수만 인자로 넣는 것이지, then 함수를 새로 정의하는 것이 아니다.
catch 메서드는 한 개의 콜백함수를 인자로 전달받는다. catch 메서드의 콜백함수는 프로미스가 rejected 상태인 경우만 호출된다.
new Promise((_,reject) => reject(new Error('rejected')))
.catch(e => console.log(e));
사실 catch 메서드는
then(undefined, onRejected)와 동일하게 작동한다. 따라서 then 메서드와 마찬가지로 언제나 프로미스를 반환한다.
따라서 사실 위의 코드는 아래 코드와 동일하게 작동한다.
new Promise((_,reject) => reject(new Error('rejected')))
.then(undefined, e => console.log(e));
catch를 통한 에러처리
비동기 처리에서 발생한 에러는 then의 두번째 인자 콜백함수를 통해 처리할 수도 있지만, 보통 catch를 통해 하는 것이 권장된다.
promiseGet() .then(res => { ~~ }) .catch(err => console.error(err));catch 메서드를 모든 then 메서드를 호출한 이후에 호출하면
- 비동기 처리에서 발생한 에러(rejected 상태)뿐만 아니라
- then 메서드 내부에서 발생한 에러
까지 모두 캐치할 수 있고, then 메서드를 통한 에러처리보다 가독성이 좋고 명확하다.
그렇다면, then 메서드의 두번째 인자로 rejected 되었을 때 호출될 콜백함수를 지정하지 않고, 첫번째 인자만 지정해준다면, rejected 되었을 때는 then 메서드는 실행되지 않는 것?
new Promise(() => reject(new Error('rejected')))
.catch(e => console.log(e));
finally 메서드의 콜백 함수는 프로미스의 성공 또는 실패와 상관없이 무조건 한번 호출된다. 또한, 언제나 프로미스를 반환한다.
=> 그래서 finally 메서드는 프로미스의 상태와 상관없이 공통적으로 수행해야 할 처리 내용이 있을 때 유용하다.
다음과 같은 후속 처리 메서드를 사용해 GET 요청 코드에 대한 후속 처리를 다음과 같이 할 수 있다.
promiseGet('baseURL/posts/1')
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('Bye!'));
콜백함수를 이용한 비동기 처리의 단점이었던
을 프로미스를 통해 해결할 수 있다.
위에서 배운 프로미스의 후속 처리 메서드 then, catch, finally를 사용하여 콜백 헬을 해결할 수 있다.
비동기 작업을 두 번 해야하는 상황이 있다고 생각해보자.
baseUrl/posts/1 경로를 통해 서버에서 특정 포스트에 대한 userId를 비동기적으로 받아오고, 이후 이 작업이 완료되면 userId를 가지고 baseUrl/users/userId 경로를 통해 서버에서 유저에 대한 정보를 받아와야 한다.이 작업을 콜백함수와 프로미스를 사용해 각각 구현해보자.
콜백함수 버전
const url = "baseURL";
get(`${url}/posts/1`, ({ userId }) => { // 1번째 작업
console.log(userId);
get(`${url}/users/${userId}`, userInfo => { // 2번째 작업
console.log(userInfo)
})
})
콜백함수를 사용한다면 콜백 함수를 중첩해서 비동기 요청을 구현해야 한다. 가독성이 별로 좋지 않다.
프로미스 버전
const url = "baseURL";
promiseGet(`${url}/posts/1`)
.then(({userId}) => promiseGet(`${url}/users/${userId}`)) // 1번째 작업
.then(userInfo => console.log(userInfo)) // 2번째 작업
.catch(err => console.error(err));
프로미스를 사용한 방식은 콜백함수 방식보다 훨신 더 직관적이다.
연속적으로 처리하고 싶은 일들을 then 메서드로 연결하고 비동기 처리 중 발생한 오류를 catch문을 통해 잡아낼 수 있다. 이렇게 후속 처리 메서드들을 연결하는 것을 프로미스 체이닝이라고 하고, 이는 각 메서드가 언제나 프로미스를 반환하기 때문에 가능하다.
여기서 각 프로미스는 상태와 처리 결과를 가지고 이 상태에 따라서 then 혹은 catch가 실행되고, 각 메서드 안에서 처리 결과을 인자로 받아 연속적으로 일을 처리할 수 있는 것이다.
다만, 프로미스도 콜백 패턴을 사용하므로 콜백 함수를 사용하지 않는 것은 아니다.
비동기 처리에 대한 후속 처리는 후속 메서드인 then, catch, finally를 사용하여 수행할 수 있고, 비동기 처리에서 발생한 에러는 then 메서드의 두 번째 콜백 함수와 catch 메서드로 처리할 수 있다.
then 메서드 사용
const wrongUrl = "아무거나";
promiseGet(wrongUrl).then(
res => console.log(res),
err => console.error(err)
);
catch 메서드 사용
const wrongUrl = "아무거나";
promiseGet(wrongUrl)
.then(res => console.log(res))
.catch(err => console.error(err));
모던 자바스크립트 Deep Dive