ES2017에서 생긴 async/await을 이해하기 위해 먼저 이전의 비동기 처리 방법들부터 순서대로 알아보겠습니다.
콜백 함수 -> Promise -> async/await

ES6에서 Promise가 도입되어 지금처럼 널리 사용되기 이전에는 주로 콜백 함수를 다른 함수의 인자로 넘겨서 비동기 처리를 코딩을 했었습니다. 예를 들어, 다음 코드를 보시면
findUserAndCallBack()함수를 호출할 때, 두번째 인자로 콜백 함수가 넘어갑니다. 그리고findUserAndCallBack()함수 안에서 인자로 넘어온 콜백 함수가 호출되고 있습니다.
findUserAndCallBack(1, function (user) {
console.log("user:", user);
});
function findUserAndCallBack(id, cb) {
setTimeout(function () {
console.log("waited 0.1 sec.");
const user = {
id: id,
name: "User" + id,
email: id + "@test.com",
};
cb(user);
}, 100);
}
waited 0.1 sec.
user: {id: 1, name: "User1", email: "1@test.com"}
단순한 코드를 작성할 때는 위와 같이 전통적인 방식으로 콜백 함수를 통해 비동기 처리를 해도 큰 문제가 발생하지 않습니다. 하지만, 콜백 함수를 중첩해서 연쇄적으로 호출해야하는 복잡한 코드의 경우, 계속되는 들여쓰기 때문에 코드 가독성이 현저하게 떨어지게 됩니다. 자바스크립트 개발자들 사이에서 소위 콜백 지옥 이라고 불리는 이 문제를 해결하기 위해 여러 가지 방법들이 논의 되었고 그 중 하나가 Promise 입니다.
Promise 는 현재에는 당장 얻을 수는 없지만 가까운 미래에는 얻을 수 있는 어떤 데이터에 접근하기 위한 방법을 제공합니다. 당장 원하는 데이터를 얻을 수 없다는 것은 데이터를 얻는데까지 지연 시간(delay, latency)이 발생하는 경우를 말합니다. I/O나 Network를 통해서 데이터를 얻는 경우가 대표적인데, CPU에 의해서 실행되는 코드 입장에서는 엄청나게 긴 지연 시간으로 여겨지기 때문에 Non-blocking 코드를 지향하는 자바스크립트에서는 비동기 처리가 필수적입니다.
예를 들면, 위에서 봤던 코드는 Promise 를 이용해서 아래와 같이 재작성할 수 있습니다.
findUser(1).then(function (user) {
console.log("user:", user);
});
function findUser(id) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log("waited 0.1 sec.");
const user = {
id: id,
name: "User" + id,
email: id + "@test.com",
};
resolve(user);
}, 100);
});
}
waited 0.1 sec.
user: {id: 1, name: "User1", email: "1@test.com"}
위 코드는 콜백 함수를 인자로 넘기는 대신에 Promise 객체를 생성하여 리턴하였고, 호출부에서는 리턴받은 Promise 객체에 then() 메서를 호출하여 결과값을 가지고 실행할 로직을 넘겨주고 있습니다. 콜백 함수를 통해 비동기 처리를 하던 기존 코드와 가장 큰 차이점은 함수를 호출하면 Promise 타입의 결과값이 리턴되고, 이 결과값을 가지고 다음에 수행할 작업을 진행한다는 것입니다. 따라서 기존 스타일보다 비동기 처리 코드임에도 불구하고 마치 동기 처리 코드 처럼 코드가 읽히기 때문에 좀 더 직관적으로 느껴지게 됩니다.
실제 코딩을 할 때는 위와 같이 Promise를 직접 생성해서 리턴해주는 코드 보다는 어떤 라이브러리의 함수를 호출해서 리턴 받은 Promise 객체를 사용하는 경우가 더 많았습니다.
REST API 를 호출할 때 사용되는 브라우저 내장 함수인 fetch()가 대표적인데요.
fetch() 함수는 API의 URL을 인자로 받고, 미래 시점에 얻게될 API 호출 결과를 Promise 객체로 리턴합니다. network latency 때문에 바로 결과값을 얻을 수 없는 상황이므로 위에서 설명한 Promise를 사용 목적에 정확히 부합합니다.
Promise 객체의 then() 메소드는 결과값을 가지고 수행할 로직을 담은 콜백 함수를 인자로 받습니다. 그리고 catch() 메서드는 예외 처리 로직을 담은 콜백 함수를 인자로 받습니다.
예를 들어, fetch() 함수를 이용해서 어떤 서비스의 API를 호출 후, 정상 응답 결과를 출력해보겠습니다.
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => console.log("response:", response))
.catch((error) => console.log("error:", error));
response: Response {type: "cors", url: "https://jsonplaceholder.typicode.com/posts/1", redirected: false, status: 200, ok: true, …}
인터넷 상에서 유효한 URL을 fetch() 함수의 인자로 넘겼기 때문에 예외가 발생하지 않고 then()에 인자로 넘긴 콜백 함수가 호출되어 상태 코드 200의 응답이 출력되었습니다.
이와 같이 Promise는 then()과 catch() 메서드를 통해서 동기 처리 코드에서 사용하던 try-catch 블록과 유사한 방법으로 비동기 처리 코드를 작성할 수 있도록 해줍니다.
최근에는 이러한 Promise를 이용하는 코딩 스타일은 자바스크립트의 async/await 키워드를 사용하는 방식으로 대체되고 있습니다. 다음 포스팅에서는 이 async/await 키워드를 사용해서 어떻게 자바스크립트 비동기 처리 코드를 개선할 수 있는지 알아보록 하겠습니다.