async의 의미에 대해서는 앞선 포스트에서 다루어 보았지만, 다시한번 간단하게 살펴보기로 하자.
async의 사전적 정의를 보면, "동시에 존재[발생]하지 않는" 이라는 뜻을 가지고 있다.
무엇이 동시에 발생하지 않는 것인고 하니, asynchronous를 의미별로 나누면 그 의미를 알수 있다.
a-(부정의 의미) + syn ( together) + chrono (time) = 시간을 함께하지 않는다.
여기서 시간은 함수의 끝과 다른 함수의 시작을 의미하는데, 함수간의 시작과 끝을 맞추지 않고 실행되는 코드, 이전 함수의 끝과 다음 함수의 시작이 꼭 동시에 존재하지 않는 코드, 코드를 비동기적으로 처리하겠다는 의지를 담은 코드라고 볼 수 있다.
(쉽기는 개뿔...)
코드 내에서 await를 붙이게 되면, 동기식 코드처럼 await가 붙은 코드의 내용의 return값을 기다린다.
즉, "이게 끝날 때 까지 다 기다려! 이게 다 받아지면 밑에 코드를 실행해!"
라는 의미이다. 비동기적인 코드이지만 내부에서는 동기적으로 실행되기도 하는 것이다.
async를 사용할 때는 아래와 같이 함수 앞에 붙여주게 되는데, 이는 자동으로 Promise를 return해 주는 역할을 한다.
async function fetchUser() {
// do network reqeust in 10 secs....
return "Noah";
}
위 코드를 콘솔에 출력해보면 아래와 같이 나오게 되는데, 이는 Promise를 return 하고 값이 fulfilled, 성공적으로 완료되어 "Noah"라는 값을 return 하고 있다.
이게 무슨 말인고 하면, 위 코드를 Promise 버전으로 작성하면 아래와 같이 되는데,
function fetchUser() {
return new Promise((resolve, reject) => {
// do sth heavy work that takes some time
resolve('Noah')
})
}
즉, 단순히 async를 함수 앞에 붙이기만 하면 함수 내에서 Promise를 만들어서 resolve와 reject를 작성하지 않아도 Promise를 return 할 수 있게 된 것이다. (return은 resolve와 같은 역할을 한다.)
정리하자면,
async를 함수 앞에 붙이게 되면 자동으로 코드 블럭이 Promise를 return 해준다. 단순히 그뿐이다.
await는 사용할 때는 아래와 같이, 꼭 async 함수 안에서만 사용해야 한다.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000);
return "🍎";
}
async function getBanana() {
await delay(3000);
return "🍌";
}
위의 코드들로 사과와 배를 출력하는 코드를 작성하게 될때도 await는 async안에 있어야 한다.
async function pickFruitsbad() {
const apple = await getApple(); // 3초 소요
const banana = await getBanana(); // 3초 소요
return `${apple} + ${banana}`; // 총 6초 소요
}
위의 코드 내용으로 실행하게 되면 pickFruitsbad이라는 함수는 비동기적으로 실행되지만, 각각사과와 바나나를 받아오는 함수는 동기적으로 실행된다. 즉 사과를 받아오기까지 기다리면 3초, 바나나를 받아오기 까지 기다리면 3초가 걸려 총 6초가 걸리게 된다.
이는 조금 비효율적으로 보인다. 사과와 바나나를 받아오는 것은 서로 전혀 연관성이 없기 때문에 하나를 받아오는 동안 다른 하나를 실행하지 못할 이유가 없기 때문이다.
생산성을 높이기 위해서 위의 코드를 아래와 같이 수정할 수 있다.
async function pickFruits() {
const applePromise = getApple();
const bananaPromise = getBanana();
const apple = await applePromise;
const banana = await bananaPromise;
return `${apple} + ${banana}`;
}
위 변경된 코드를 보면 pickFruits()라는 함수는 내부에서도 비동기적으로 실행되기 때문에 getApple()을 실행하고 getBanana()를 실행하는 순간, 바로 각각의 프로미스 안에 들어있는 코드블럭이 동시에 실행되고 그 return값을 가져오게 된다. 그래서 각각의 return 값을 할당해주는 과정을 await를 통해 진행하게 되는 것이다. 이렇게 되면 총 3초가 걸리게 되는 것이다.
그러면 에러 핸들링은 어디서 해주는 것이란 말인가.
좋은 질문이다. 예를 들어 getApple에서 Error가 발생했다고 해보자.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
await delay(2000);
throw 'error'
return "🍎";
}
async function getBanana() {
await delay(1000);
return "🍌";
}
그리고 아래처럼 함수를 작성했다고 했다면,
async function pickFruitsbad() {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
}
콘솔에는 아래와 같이 나오게 된다. 이는 지난 Promise 포스트 에서도 얘기했던 것처럼, 에러 핸들링을 아무곳에서도 하지 않았기 때문에 발생하는 일이다.
이런경우에는 기존에 에러 핸들링을 하는 방법 중에 하나인 try-catch를 아래와 같이 사용하면 에러를 잡을 수 가 있다.
async function pickFruitsbad() {
try {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
} catch (error) {
console.log(error);
}
}
이렇게 하면, 우리가 정의한 에러가 콘솔에 나오게 되는 것을 볼 수 있다.
우리가 Callback 지옥을 벗어나서 Promise로 왔지만, Promise도 너무 많은 Chaining을 경험하다 보니 조금 더 간결하고 효율적인 코드를 찾게 되었다고 볼 수 있다.
예를 들면, 아래와 같이 코드를 작성하게 되면 코드가 지저분하고 직관적이지 않게 된다.
//Callback hell, even though it is Promise
function pickFruitsbad() {
return getApple().then((apple) => {
return getBanana().then((banana) => `${apple} , ${banana}`);
});
}
이를 수정하게 되면 아래와 같이 수정 하여 가독성을 높일 수 있다.
async function pickFruitsbad() {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
}
또한 async와 awiat는 다른 포스트에서 설명한 async의 의미와 기능과 더불어 Promise Chaining을 조금 더 간결하고 간편하게 사용할 수 있도록 하고, 동기적으로 실행 되는 것 처럼 보이게 만들어 준다.
근데 왜 비동기 코드인데 동기적으로 실행되는 것 처럼 보여야 할까?
이는 동기적으로 보이는데 목적을 둔 것은 아니고, async와 await를 사용하게 되면 우리가 그냥 동기식으로 코드를 순서대로 작성하는 것처럼 간편하게 작성할 수 있다는 점에서 사용한다는 의미이다.
또한 async와 awiat는 새롭게 추가된 것이 아니라, Promise 위에 조금 더 간편한 API를 제공하는 것이다. 이렇게 기존에 존재하던 것 위에 간편하게 쓸 수 있는 API를 제공하는 것을 "Syntatic sugar" 라고 한다.
Promise.all API를 사용하면 Promise의 배열을 전달하게 되고, 모든 Promise 들이 병렬적으로 다 받을때까지 기다렸다가 전달해준다.
function pickAllFruits() {
return Promise.all([getApple(), getBanana()]).then(fruits =>
fruits.join(' + ')
);
}
pickAllFruits().then(console.log);
위 코드를 실행하면 콘솔에는 아래와 같이 보이게 된다.
Promise.race API를 사용하게 되면 둘중에 먼저 처리되는 것 부터 return 한다.
먼저, 두 함수가 실행되는 시간이 아래와 같이 다를 경우.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
let count = 0;
const intervalId = setInterval(() => console.log(++count, "apple"), 1000);
await delay(4000);
clearInterval(intervalId);
return "🍎";
}
async function getBanana() {
let count = 0;
const intervalId = setInterval(() => console.log(++count, "banana"), 1000);
await delay(3000);
clearInterval(intervalId);
return "🍌";
}
두 함수를 실행 했을 때 먼저 실행되는 것을 return 되는 것을 찾기 위해서는
function pickOnlyOne() {
return Promise.race([getApple(), getBanana()]);
}
pickOnlyOne().then(console.log);
이렇게 작성하게 되면 콘솔에는 아래와 같이 보이게 된다.
getApple은 4초가 걸리고 getBanana는 3초가 걸리기 때문에 3초까지 두가지 모두 실행되다가 Banana가 먼저 return 되기 때문에 콘솔에 먼저 찍히게 된다.
Callback을 지나 Promise를 거쳐 async&await에 오면서 조금은 모호했던 개념들이 확실해 짐을 느꼈다. sync와 async의 차이점도 잘 이해되지 않았지만, 어원을 찾아들어가 보니 이해가 빠르게 된것도 이번 글을 적으면서 얻은바이다.
-끝-
[출처]
https://www.youtube.com/watch?v=aoQSOZfz3vQ (자바스크립트 13. 비동기의 꽃 JavaScript async 와 await 그리고 유용한 Promise APIs | 프론트엔드 개발자 입문편 (JavaScript ES6))
https://www.youtube.com/watch?v=1z5bU-CTVsQ (JavaScript - async & await)