콜백 함수를 이용한 비동기 처리의 단점에서 살펴본 단점들을 해결하기 위해 프로미스를 이용해 비동기 처리를 할 수 있다.
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