흔히 비동기처리의 방법으로 콜백 패턴
을 사용한다고 한다. 왜 콜백 패턴은 비동기처리의 방법으로 사용되었으며 이를 보안하기 위한 프로미스
는 어떤 개념인지 정리해보자.
const get = (url) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
return JSON.parse(xhr.response);
}
console.error(`${xhr.status} ${xhr.statusText}`);
};
};
const response = get("https://jsonplaceholder.typicode.com/posts/1");
console.log(response);
비동기 함수라는 것은 함수 내부에 비동기로 동작하는 코드를 포함한다는 뜻이다. onload 이벤트 핸들러는 비동기로 동작하기 때문에 get 함수는 비동기 함수라고 할 수 있다.
비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료된다. 이러한 이유 때문에 콜백 패턴이 등장하게 된 것이다.
위의 코드에서 get 함수를 호출하면 get 함수의 실행컨텍스트가 생성되고 콜스택에 푸쉬된다. 그리고 비동기 처리가 완료될 때 호출할 이벤트 핸들러가 onload에 바인딩된다.
그리고 나서 get 함수는 비동기처리 즉, xhr.status가 200이 올 때까지 기다려주는가? 아니다. get 함수는 미련없이 콜스택에서 나가버린다.
그리고 나서 response값을 콘솔에 출력하는 코드가 콜스택에 들어오게 되고 실행된다. 하지만 response는 get함수의 응답값이며 명시적으로 get함수의 응답값은 아무것도 리턴하지 않는 undefined이다.
그리고 status가 200이 되고 onload에 바인딩된 이벤트 핸들러가 실행이 된다. 이벤트 핸들러는 태스크 큐에 저장되어 있다가 콜스택이 비었을 때 콜스택으로 이동한다. 하지만 response를 콘솔에 보여주는 함수는 이미 콜스택에서 나간 상태. 즉, 비동기 처리의 결과를 외부에 보여줄 수 없게 된 상황이다.
이를 해결하기 위해서는 비동기 처리의 결과를 비동기 함수 내부에서 처리해야하는데 그러기 위해서 콜백 패턴
이 등장하게 된 것이다.
const get = (url, callback) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
callback(JSON.parse(xhr.response));
}
console.error(`${xhr.status} ${xhr.statusText}`);
};
};
const response = get("https://jsonplaceholder.typicode.com/posts/1");
console.log(response);
하지만 콜백 패턴에는 여러가지 단점이 존재한다. 대표적으로 콜백 헬
과 에러처리
에 문제가 생긴다.
위의 코드에서 해당 url로 유저 ID를 취득하여 유저에 대한 정보를 받아오는 코드를 짠다고 생각해보자.
비동기 처리 결과에 대한 후속 처리로 콜백을 전달하게 되면 다음과 같이 콜백이 꼬리를 물게되는 콜백 헬
이 발생한다.
get(`${url}/posts/1`, ({ userId }) => {
get(`${url}/users/${userId}`, (userInfo) => {
console.log(userInfo);
});
});
또한 에러 처리가 곤란하다. 다음 예제를 살펴보자.
try {
setTimeout(() => {
throw new Error("Error!");
}, 1000);
} catch (error) {
console.error(error);
}
전역 컨텍스트가 실행되고 setTimout 실행 컨텍스트가 콜스택에 들어온다.
setTimeout은 콜백함수에게 1초있다가 실행하라고 말하고 콜스택에서 나가버린다.
비동기로직을 수행하는 콜백함수는 1초있다가 테스크 큐에 들어간다. 그리고 나서 콜스택이 비었는지 계속 확인한다.
전역 컨텍스트까지 콜스택에서 팝되고 콜스택이 비었으면 이벤트루프에 의해서 콜백함수가 콜스택에 들어가게된다. 하지만 에러를 캐치하는 코드는 이미 콜스택에서 집나간지 오래이다. 그렇기 때문에 에러를 잡을 수 없는 것이다.
콜백함수를 호출하는 것은 setTimeout이 아니라는 말이다.
위의 단점들을 해결하기 위해서 프로미스가 도입되었다. 프로미스는 비동기처리 결과에 대한 상태와 값을 관리하고 있는 객체라고 생각하면 된다.
const get = (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));
}
reject(`${xhr.status} ${xhr.statusText}`);
};
});
};
const response = get("https://jsonplaceholder.typicode.com/posts/1");
console.log(response);
비동기처리가 이루어지는 코드를 Promise의 인자로 전달되는 콜백함수에 적는다. 그리고 이 콜백함수는 resolve와 reject 두 가지 인자를 전달받게 된다.
resolve는 비동기처리를 성공했을 때 호출하는 함수이고 reject는 실패했을 때 호출하는 함수이다. resolve를 호출하면 Promise의 상태가 fulfilled이 되고, 실패하면 rejected가 된다.
Promise가 비동기처리의 결과를 알려준다고 약속했다. 콘솔에 출력되는 값은 리턴되는 프로미스값이며 상태는 성공했고, resolve함수의 인자로 전달한 데이터가 프로미스의 결과값으로 들어온 것을 확인할 수 있다.
프로미스는 후속처리 메서드를 통해서 콜백 헬의 단점을 보완한다. 물론 후속처리 메서드에도 콜백함수를 사용한다. 가독성을 위해서는 async/await
을 사용하자.
get("https://jsonplaceholder.typicode.com/posts/1")
.then((res) => {
console.log("promise가 성공하면 실행", res);
})
.catch((err) => {
console.error("실패하면 실행", err);
})
.finally(() => {
console.log("성공이든 실패이든 무조건 실행");
});
또한 catch를 통해서 비동기처리에서 발생하는 에러에 대해 잡아줄 수 있다.