Promise와 async/await 사용법 정리
지난 글에서 다룬 비동기 처리를 코드로 다루는 방법인 Promise와 async/await를 정리했다.
Promise
자바스크립트 Promise 객체는 비동기 작업의 최종 완료 또는 실패를 나타내는 독자적인 객체이다. 비동기 작업이 끝날 때까지 결과를 기다리는 것이 아니라, 결과를 제공하겠다는 ‘약속’을 반환한다는 의미에서 Promise라는 이름이 붙었다고 한다.
Promise 객체를 생성하려면 new 키워드와 Promise 생성자 함수를 사용하면 된다. 이때 Promise 생성자 안에 두개의 매개변수를 가진 콜백 함수를 넣게 되는데, 첫 번째 인수는 작업이 성공했을 때 성공(resolve)임을 알려주는 객체이며, 두 번째 인수는 작업이 실패했을 때 실패(reject)임을 알려주는 오류 객체이다.
const myPromise = new Promise((resolve, reject) => {
// 비동기 작업 수행
const data = fetch('서버로부터 요청할 URL');
if(data)
resolve(data); // 만일 요청이 성공하여 데이터가 있다면
else
reject("Error"); // 만일 요청이 실패하여 데이터가 없다면
})
이렇게 만들어진 Promise 객체는 비동기 작업이 완료된 이후에 다음 작업을 연결시켜 진행할 수 있다. 작업 결과 따라 .then() 과 .catch() 메서드 체이닝을 통해 성공과 실패에 대한 후속 처리를 진행할 수 있다.
만일 처리가 성공하여 프로미스 객체 내부에서 resolve(data)를 호출하게 되면, 바로 .then()으로 이어져 then 메서드의 콜백 함수에서 성공에 대한 추가 처리를 진행한다. 이때 호출한 resolve() 함수의 매개변수의 값이 then 메서드의 콜백 함수 인자로 들어가 then 메서드 내부에서 프로미스 객체 내부에서 다룬 값을 사용할 수 있게 된다.
반대로 처리가 실패하여 프로미스 객체 내부에서 reject(“Error”) 를 호출하게 되면, 바로 .catch()로 이어져 catch 메서드의 콜백 함수에서 성공에 대한 추가 처리를 진행한다.
myPromise
.then((value) => { // 성공적으로 수행했을 때 실행될 코드
console.log("Data: ", value); // 위에서 return resolve(data)의 data값이 출력된다
})
.catch((error) => { // 실패했을 때 실행될 코드
console.error(error); // 위에서 return reject("Error")의 "Error"가 출력된다
})
.finally(() => { // 성공하든 실패하든 무조건 실행될 코드
})
위와 같이 프로미스 객체를 변수에 바로 할당할 수도 있지만, 보통은 다음과 같이 별도 함수로 감싸서 사용하는 것이 일반적이다.
// 프로미스 객체를 반환하는 함수 생성
function myPromise() {
return new Promise((resolve, reject) => {
if (/* 성공 조건 */) {
resolve(/* 결과 값 */);
} else {
reject(/* 에러 값 */);
}
});
}
// 프로미스 객체를 반환하는 함수 사용
myPromise()
.then((result) => {
// 성공 시 실행할 콜백 함수
})
.catch((error) => {
// 실패 시 실행할 콜백 함수
});
함수를 만들고 그 함수를 호출하면 프로미스 생성자를 return 함으로서, 곧바로 생성된 프로미스 객체를 함수 반환값으로 얻어 사용하는 기법이다. 프로미스를 생성하여 반환하는 함수를 ‘프로미스 팩토리 함수’라고 부르기도 한다.
그런데 콜백 못지않게 프로미스의 then() 메서드가 지나치게 체인되어 반복되면 코드가 장황해지고 가독성이 굉장히 떨어질 수가 있다. 이를 또다시 극복하기 위해 나온 자바스크립트의 신세대 문법이 있는데 바로 async/await 키워드이다.
Async/Await
async/await 키워드는 ES8에서 도입된 비동기 처리를 위한 문법으로, 프로미스를 기반으로 하지만 then과 catch 메서드를 사용하지 않고 비동기 작업을 수행할 수 있다. async/await 키워드를 사용하면 비동기 작업을 마치 동기 작업처럼 쓸 수 있어서 코드가 간결하고 가독성이 좋아지게 된다.
사용법은 function 키워드 앞에 async를 붙이고, 비동기로 처리되는 부분 앞에 await를 붙여주면 된다.
// 기존 Promise.then() 형식
function main() {
delay(1000)
.then(() => {
return delay(2000);
})
.then(() => {
return Promise.resolve('끝');
})
.then(result => {
console.log(result);
});
}
// async/await 방식
async function main() {
await delay(1000);
await delay(2000);
const result = await Promise.resolve('끝');
console.log(result);
}
async 키워드는 어렵게 생각할 필요없이 await를 사용하기 위한 선언문 정도로 이해하면 된다. 즉, function 앞에 async을 붙여줌으로써, 함수내에서 await 키워드를 사용할 수 있게 된다. 이는 await 키워드를 사용하기 위해선 반드시 async function 정의가 되어 있어야 한다는 말과 같다.
async function은 어떤 값을 리턴하든 무조건 프로미스 객체로 감싸져 반환된다는 특징이 있다.
기존에는 await 키워드를 사용하기 위해선 무조건 async function를 정의해야 했기 때문에 굳이 함수로 감싸지 않아도 될 코드를 어거지로 함수로 감싸 IIFE로 호출하는 식으로 사용해왔다. 이러한 한계점 때문에 Top-level await 란 async 함수나 모듈 외부에서도 await 키워드를 사용할 수 있게 해주는 편의 기능이 추가되었다. 이 기능은 ES2022에 추가되었으며, 현재 대부분의 최신 브라우저와 Node.js에서 지원된다.
// 기존의 async function
(async function func1() {
const res = await fetch(url); // 요청을 기다림
const data = await res.json(); // 응답을 JSON으로 파싱
console.log(data);
})();
// Top Level에선 async funciton 정의없이 곧바로 await 키워드 사용이 가능하다
const res = await fetch(url); // 요청을 기다림
const data = await res.json(); // 응답을 JSON으로 파싱
console.log(data);

