on
[Javascript]Promise
콜백 지옥을 해결해보자
비동기 함수의 문제점
비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료된다. 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않는다.
let g = 0;
// 비동기 함수인 setTimeout 함수는 콜백 함수의 처리 결과를 외부로 반환하거나
// 상위 스코프의 변수에 할당하지 못한다.
setTimeout(() => {
g = 100;
}, 0);
console.log(g); // 0
콜백 패턴
자바스크립트에서 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 수행해야 한다. 이때, 비동기 함수를 범용적으로 사용하기 위해 비동기 함수에 비동기 처리 결과에 대한 후속 처리를 수행하는 콜백 함수를 전달하는 콜백 패턴을 사용한다.
// GET 요청을 위한 비동기 함수
const get = (url, successCallback, failureCallback) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// 서버의 응답을 콜백 함수에 인수로 전달하면서 호출하여 응답에 대한 후속 처리를 한다.
successCallback(JSON.parse(xhr.response));
} else {
// 에러 정보를 콜백 함수에 인수로 전달하면서 호출하여 에러 처리를 한다.
failureCallback(xhr.status);
}
};
};
// id가 1인 post를 취득
// 서버의 응답에 대한 후속 처리를 위한 콜백 함수를 비동기 함수인 get에 전달해야 한다.
get("https://jsonplaceholder.typicode.com/posts/1", console.log, console.error);
/*
{
"userId": 1,
"id": 1,
"title": "sunt aut facere ...",
"body": "quia et suscipit ..."
}
*/
1. 콜백 지옥
만약에 다음 예제와 같이 비동기 함수의 후속 처리를 수행하는 콜백 함수가 비동기 처리 결과를 가지고 또 다시 비동기 함수를 호출해야 한다면 콜백 함수 호출이 중첩되어 복잡도가 높아지고 가독성이 나빠지는데 이것을 콜백 헬 또는 콜백 지옥이라고 한다.
get("/step1", (a) => {
get(`/step2/${a}`, (b) => {
get(`/step3/${b}`, (c) => {
get(`/step4/${c}`, (d) => {
console.log(d);
});
});
});
});
2. 에러 처리의 한계
콜백 패턴의 또 다른 문제점은 에러 처리가 어렵다는 것이다.
try {
setTimeout(() => {
throw new Error("Error!");
}, 1000);
} catch (e) {
// 에러를 캐치하지 못한다
console.error("캐치한 에러", e);
}
위 예제에서 콜백 함수에서 에러를 발생시켰지만 catch 코드 블록에서 캐치되지 않는다. 그 이유는 위 예제 코드가 다음과 같은 순서로 동작하기 때문이다.
try-catch
문이 실행되어call stack
에 push 됨setTimeout
이 실행되어 콜백 함수가event loop
에 등록 됨try-catch
문에서 에러가 발생하지 않았고 다음 코드도 없기때문에 실행 종료되어call stack
에서 pop 됨setTimeout
의 콜백 함수가event loop
에 등록된지 1초 뒤에task queue
로 push 됨call stack
에 실행중인 컨텍스트가 없으므로 콜백 함수는event loop
에 의해서call stack
으로 이동되고 실행 됨- 콜백 함수가 에러를 발생시킴
- 에러를 캐치할 실행 중인 실행 컨텍스트의 상위 컨텍스트가 없기 때문에 에러를 캐치하지 못하고 종료
에러는 호출자 방향으로 전파된다. 즉, 콜 스택의 아래 방향(살향 즁안 실행 컨텍스트가 푸시되기 직전에 푸시된 상위 실행 컨텍스트 방향)으로 전파된다. 위 예제에서 setTimeout 함수의 콜백 함수를 호출한 것은 setTimeout 함수가 아니고, 상위 컨텍스트가 없기 때문에 에러가 캐치되지 않는 것이다.
프로미스
1. 프로미스의 생성
콜백 함수에서 에러를 처리하기 어렵다는 문제를 극복하기 위해 ES6에서 Promise
가 도입되었다. Promise
생성자 함수를 new 연산자와 함께 호출하면 Promise
객체를 생성한다. Promise
생성자 함수는 비동기 처리를 수행할 콜백 함수에 resolve와 reject 함수를 인수로 전달 받는다.
// 프로미스 생성
const promise = new Promise((resolve, reject) => {
// Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행한다.
if (/* 비동기 처리 성공 */) {
resolve('result');
} else { /* 비동기 처리 실패 */
reject('failure reason');
}
});
Promise
생성자 함수가 인수로 전달받은 콜백 함수의 내부에서 비동기 처리를 수행하는 것이다. 이때, 비동기 처리가 성공하면 resolve
함수를 호출하고, 실패하면 reject
함수를 호출한다.
만약에 비동기 처리의 결과를 받고 싶다면, resolve 함수에 처리 결과를 인수로 전달하면서 호출하고, 실패 결과는 reject 함수에 인수로 전달하면 된다.
// 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 함수를 호출한다.
resolve(JSON.parse(xhr.response));
} else {
// 에러 처리를 위해 reject 함수를 호출한다.
reject(new Error(xhr.status));
}
};
});
};
// promiseGet 함수는 프로미스를 반환한다.
promiseGet("https://jsonplaceholder.typicode.com/posts/1");
2. 프로미스의 상태
Promise
는 현재 비동기 처리가 어떻게 진행되고 있는지 나타내는 상태 정보를 갖는다.
프로미스의 상태 | 의미 | 상태 변경 조건 |
---|---|---|
pending | 비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 상태 |
fulfilled | 비동기 처리 수행이 완료되고 결과가 성공인 상태 | resolve 호출 |
rejected | 비동기 처리 수행이 완료되고 결과가 실패인 상태 | reject 호출 |
이때, 비동기 처리가 완료된 상태인 fulfilled
상태와 rejected
상태를 settled
상태라고 한다. 한 번 pending
상태에서 settled
상태가 되면 다시 pending
상태로 되돌릴 수는 없다.
또 Promise
객체는 처리 상태와 함께 결과 값인 result를 상태로 갖는다.
<그림 45-1>