
Promise 는 비동기 처리에서 발생하는 콜백 지옥을 극복하기 위해 도입된 문법입니다.
하지만, Promise 역시 지나치게 많이 then() 체이닝을 사용하다 보면 코드가 복잡해지고, 가독성이 떨어지는 프로미스 지옥이 발생합니다.
아래 코드를 보시죠
fetch("url")
.then((response) => {
if (!response.ok) { throw new Error("네트워크 에러"); }
return response.json();
})
.then((data) => {
return data.users;
})
.then((users) => {
return users.filter((user) => user.isLogin)).join(", ");
})
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
});
위 코드를 보시면, then() 메서드가 반복적으로 체이닝 하고 있어, 무엇을 하는 코드인지 한눈에 파악하기 어렵습니다.
async/await 는 이런 Promise 를 더 쉽고 간결하게 사용할 수 있도록 도입된 문법입니다.
기본적으로 Promise를 기반으로 동작하지만, then() 과 catch() 같은 메서드를 사용하지 않고도 비동기 작업을 처리할 수 있어 코드가 훨씬 간결하고 가독성이 좋아집니다. 즉, 비동기를 처리하는 방식 자체는 Promise 와 동일하지만, 이를 표현하는 문법이 더 직관적으로 바뀐 것이라고 볼 수 있습니다.
아래 코드를 보시죠
try{
const res = await fetch("url");
if (!res.ok) { throw new Error("네트워크 에러");}
const data = await res.json();
const result = data.users.filter((user) => user.isLogin).join(", ");
console.log(result);
}catch (error){
console.error(error);
}
위 코드를 보면 함수의 리턴값을 변수의 할당하는 형식으로 기존의 then() 메소드를 사용해 처리하는 것보다 더 직관적으로 변한 것을 보실 수 있습니다.
async / await 문법의 사용법은 매우 간단하고 이해가 쉽습니다.
async 키워드를 함수 선언 앞에 붙여주면 되고, 비동기를 처리하는 부분 앞에 await 키워드를 붙여주면 작업이 완료될 때까지 기다릴 수 있습니다. 마치 기존의 절차적으로 코드를 작성하듯이 비동기 작업을 자연스럽게 작성 할 수 있습니다.
아래 코드를 보시죠
function delay(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`${time} 이 지났습니다.`);
resolve();
}, time);
})
}
// Promise 방식
function asyncTask() {
delay(1000)
.then(() => {
return delay(500);
})
.then(() => {
return Promise.resolve("작업 완료");
})
.then((result) => {
console.log(result);
});
}
// async/await 방식
async function asyncTask(){
await delay(1000);
await delay(500);
const result = await Promise.resolve("작업 완료");
console.log(result);
}
먼저 async 키워드에 대해서 살펴보겠습니다.
아래 코드를 보시죠
async function asyncFunc() {
return 10;
}
const result = asyncFunc();
console.log(result);
위 이미지를 보면 알 수 있듯이, 이행(fulfilled) 상태의 프로미스 객체 형태가 반환 되는 것을 볼 수 있습니다. 즉, async 함수는 어떤 값을 리턴하든 무조건 프로미스 객체로 감싸져 반환되는 것입니다.
이외에도 직접 정적 메서드를 통해 다음과 같은 프로미스 상태(state)를 다르게 지정하는 것도 가능합니다.
아래 코드를 보시죠
async function asyncResolve() {
return Promise.resolve(10);
}
async function asyncReject() {
return Promise.reject(10);
}
이렇게 정적메소드를 사용해서 반환할 수 도 있지만,
async function asyncError(){
throw new Error("에러 발생");
}
async function asyncVoid(){
}
예외처리로 인한 에러가 발생하거나, 일부러 `return`을 적용하지 않아도 프로미스 객체를 반환하게 됩니다.
async function asyncFunc() {
return 10;
}
asyncFunc()
.then((res) => console.log(res));
마지막으로 async 함수는 결국 Promise 이기 때문에 async 함수 자체에 then() 핸들러를 사용할 수 있습니다. 하지만 가독성 문제로 await 키워드를 통해 처리합니다.
다음으로 await 키워드에 대해서 살펴보겠습니다.
아래 코드를 보시죠
// Promise 방식
function asyncTask() {
delay(1000)
.then(() => {
return delay(500);
})
.then(() => {
return Promise.resolve("작업 완료");
})
.then((result) => {
console.log(result);
});
}
// async/await 방식
async function asyncTask(){
await delay(1000);
await delay(500);
const result = await Promise.resolve("작업 완료");
console.log(result);
}
위 코드에서 await 키워드는 Promise 객체가 처리(resolve) 될 때까지 실행을 중단시킵니다.
이를 통해 then() 체이닝 없이 비동기 결과를 직접 변수에 할당할 수 있습니다.
또한, async/await 를 사용하면 try/catch 블록으로 비동기 작업의 에러를 처리할 수 있습니다. 이를 통해 기존 catch() 메서드 체이닝보다 간단하게 에러를 관리할 수 있습니다
아래 코드를 보시죠
// async/await 방식
async function asyncFunc() {
try {
const res = await fetch(url);
const data = await res.json();
console.log(data); // 데이터 처리
} catch (err) {
console.error(err); // 에러 처리
}
}
async/await 은 비동기 코드를 직관적으로 작성할 수 있는 강력한 도구이지만, 잘못 사용하면 성능 문제를 유발할 수 있습니다. 특히, 병렬로 처리할 수 있는 작업을 await로 순차적으로 처리하면 비효율적인 동작이 발생할 수 있습니다. 비동기 프로그래밍의 본질은 병렬로 처리 가능한 작업을 효율적으로 수행하는 데 있으며, await를 남용하면 이러한 장점을 상실하게 됩니다.
아래 코드를 보시죠
function getApple(){
return new Promise( (resolve, reject) => {
setTimeout(() => resolve("apple"), 1000);
})
}
function getBanana(){
return new Promise( (resolve, reject) => {
setTimeout(() => resolve("banana"), 1000);
})
}
async function getFruites(){
let a = await getApple();
let b = await getBanana();
console.log(`${a} and ${b}`);
}
getFruites();
위 코드를 보면, 사과와 바나나의 정보를 가져오는 비동기 함수가 서로 독립적임을 알 수 있습니다. 하지만 await 키워드를 두 번 사용하면서, 사과의 정보를 가져온 뒤에야 바나나의 정보를 가져오는 방식으로 동작합니다. 즉, 비동기 함수임에도 await 키워드를 사용했기 때문에 동기 처리가 되어 순차적으로 처리된 것이죠.
그럼 어떻게 병렬 처리를 할 수 있을까요?
아래 코드를 보시죠
async function getFruites(){
let getApplePromise = getApple(); // async함수를 미리 논블록킹으로 실행한다.
let getBananaPromise = getBanana(); // async함수를 미리 논블록킹으로 실행한다.
let a = await getApplePromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.
let b = await getBananaPromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.
console.log(`${a} and ${b}`); // 본래라면 1초+1초 를 기다려야 하는데, 위에서 1초기다리는 함수를 바로 연속으로 비동기로 불려왔기 때문에, 대충 1.01초만 기다리면 처리된다.
})
위 코드는 사과와 바나나 정보를 가져오는 비동기 작업을 병렬로 처리하는 방법을 보여줍니다.
getApple() 과 getBanana() 호출될때 바로 실행되고, 그 결과를 Promise 객체로 받아옵니다. 이후 await 를 사용해 이 Promise 의 결과를 처리합니다.
이렇게하면 두 비동기 작업이 동시에 실행되기 때문에, 각각의 작업을 병렬로 처리 할 수 있습니다.
또 다른 방법으로는 Promise.all() 메서드를 사용하여 처리하는 방법이 있습니다.
위의 방식으로 처리하는 경우 완료 시점을 가늠하기 힘들기 때문에, 실무에선 Promise.all() 을 많이 사용합니다.
async function getFruites(){
console.time();
// 구조 분해로 각 프로미스 리턴값들을 변수에 담는다.
let [ a, b ] = await Promise.all([getApple(), getBanana()]);
console.log(`${a} and ${b}`);
console.timeEnd();
}
getFruites();
https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%B2%98%EB%A6%AC-async-await#async_/_await_%EA%B8%B0%EB%B3%B8_%EC%82%AC%EC%9A%A9%EB%B2%95
https://ko.javascript.info/async-await
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/await
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function