https://youtu.be/aoQSOZfz3vQ
드림코딩 by 엘리 님의 유튜브 강의를 보며 정리한 내용입니다.
비동기의 하이라이트! 비동기의 핵심! 비동기의 꽃 🌷✨
async 와 await 은 이전에 살펴본 promise 를 더 간결하고, 간편하게! 그리고 동기적으로 실행되는 것처럼 보이게 👀 만들어준다.
promise 들은 여러가지 chaining을 할 수 있는데, (promise .then에 또 다른 promise .then ...) 이런식으로 chaining을 계속 계속하다 보면 코드가 난잡해질 수가 있다. 그래서 이런 것 위에 조금 더 간편한 API
로 async 와 await 을 사용하면 우리가 그냥 동기식으로 코드를 순서대로 작성하는 것처럼 간편하게 작성할 수 있도록 도와준다.
🤚🏻 async와 await은 새로운 것이 추가된 게 아니라 기존에 존재하는 promise위에 조금 더 간편한 API를 제공하는 것이다. 이렇게 기존에 존재하는 것 위에 또는 기존에 존재하는 것을 감싸서 우리가 좀 더 간편하게 쓸 수 있는 API를 제공하는 것을 syntactic sugar
라고 한다. (같은 예로 class가 있다. JavaScript 에서 class는 전혀 새로운 것이 아니라 프로토타입을 베이스로 한, 그 위에 살짝 덧붙여진 syntactic sugar이다.)
clear style of using promise ✨🙂
async와 await은 깔끔하게 promise를 사용할 수 있는 방법인데, 그렇다고 무조건 promise가 나쁘고 async와 await으로 대체해서 사용해야 된다는 것은 절대 아니다!
promise를 유지해서 써야 맞는 경우가 있고, 또는 async와 await으로 변환해야지 조금 더 깔끔해지는 경우가 있다. 이런 차이점은 계속 코딩을 해나가면서 감을 찾아가자 👌🏻
function fetchUser() {
//do network request in 10 secs....
return 'eden';
}
가정 : fetchUser은 사용자의 데이터를 backend에서 받아오는 함수이다. 네트워크 통신을 해서 백엔드로부터 데이터를 받아오는 데 한 10초정도 걸리는 코드가 있다고 가정하고, 그래서 10초가 지나면 우리가 받아온 사용자의 이름을 return 한다.
const user = fetchUser();
console.log(user); //eden
→ 이렇게 무언가 오래 걸리는 코드를 비동기적인 처리를 전혀 하지 않으면 자바스크립트 엔진은 동기적으로 코드를 수행하기 때문에(즉, 한 줄 한 줄씩! 한 줄이 끝나야 그 다음 줄로 넘어가는,,) 그래서 "fetchUser가 호출이 됐네?" → 함수가 선언된 곳으로 가서 fetchUser 함수의 코드블럭을 실행 → 그래서 한 줄이 수행되면서 꼭 10초가 걸리니 10초 동안 거기에 머무르고 있다가 10초가 지나서 성공적으로 네트워크 데이터를 받아오게 되면 → 그제서야 다음 줄로 넘어가면서 'eden'이 return된다. → 그리고 이 return된 코드가 user에 할당이 되고 → 그다음 라인으로 넘가서 → eden이 출력된다.
🤚🏻 우리가 여기서 비동기적인 처리를 전혀 하지 않으면 사용자가 데이터를 받아오는 데 10초가 걸리기 때문에 만약 이 뒤에 웹페이지의 UI를 표시하는 기능을 수행하는 코드들이 있다면, 이것이 끝나는 동안 데이터가 웹페이지에 표시되지 않기 때문에 사용자는 10초 정도 텅텅 빈 웹페이지만 보게 될 것이다. 그래서 이렇게 오래 걸리는 일들은 비동기적으로 처리할 수 있게 해주어야 한다!! ✨
function fetchUser() {
return new Promise((resolve, reject) => {
//do network request in 10 secs...
resolve('eden');
})
}
const user = fetchUser();
user.then(console.log); //= (user) => console.log(user)
promise를 이용하지 않고도 간편하게 비동기를 작성할 수 있는 방법이다.
async function fetchUser() {
//do network request in 10 secs...
return 'eden';
}
const user = fetchUser();
user.then(console.log); //eden
await 이라는 키워드는 async 가 붙은 함수 안에서만 사용이 가능하다!
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000); //3초 전달
return "🍎";
}
async function getBanana() {
await delay(3000);
return "🍌";
}
→ delay 라는 함수는 promise를 return하는데, 정해진 ms(밀리세컨)이 지나면 resolve를 호출하는 그런 프로미스를 리턴한다. 그래서 3초를 전달했기 때문에 → 3초가 지나면 resolve를 호출하는 프로미스가 전달이 되는 것이다.
바나나 함수를 동일하게 promise를 쓰는 함수로 만들어 보자면
function getBanana() {
return delay(3000) //우선 먼저 3초가 된 다음에
.then(() => '🍌'); //어떤 값을 받았는지는 상관없고 결국에는 🍌를 리턴한다.
}
✨ // 이렇게 chaining을 하는 것보다 async, await을 사용해 동기적 코드를 쓰는 것처럼 만들게 되면 더 쉽게 이해할 수 있다.
getApple와 getBanana를 한번에 다 처리하는 함수를 promise chaining으로 이용해 만들어보자!
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000); //3초 전달
return "🍎";
}
async function getBanana() {
await delay(3000);
return "🍌";
}
//promise chaining 으로 작성
function pickFruits() {
return getApple().then((apple) => {
return getBanana().then((banana) => `${apple} + ${banana}`);
});
} // ..looks like callback Hell 😅
pickFruits().then(console.log); //🍎 + 🍌
→ 하지만 이렇게 promise도 너무 중첩적으로 chaining을 하게되면 콜백지옥과 비슷한 문제점이 발생한다.
그럼 이것을 async
라는 키워드를 이용해서 간단하게 바꿔보자! ✨
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000); //3초 전달
return "🍎";
}
async function getBanana() {
await delay(3000);
return "🍌";
}
// 이렇게 간단하게 바꿔줄 수 있다!
async function pickFruits() {
const apple = await getApple(); //받아오는동안 3초 걸리고
const banana = await getBanana(); //받아오는동안 얘도 3초걸린다.
return `${apple} + ${banana}`;
}
pickFruits().then(console.log); //(6초가 지난 후..) 🍎 + 🍌
→ async와 await으로 코드가 깔끔하게 바뀌었지만 사과를 받는 데 3초가 걸리고, 바나나를 받는 데 3초가 걸린다. 그래서 총 6초가 소요가 되는데 이건 비효율적이다. 이 경우에서는 사과와 바나나를 받아오는 데 서로 연관이 되어있지 않기 때문에 서로를 기다릴 필요가 전혀 없다. 그렇기 때문에 await을 병렬적으로 처리해주는 방법을 사용해주면 좋다!
async function pickFruits() {
const applePromise = getApple();
const bananaPromise = getBanana();
//이렇게 하면 promise를 만드는 순간 바로 promise안에 들어있는 코드블럭이 실행이 된다.
const apple = await applePromise;
const banana = await bananaPromise;
//그리고 여기서 실행된 것들을 동기화 시켜주면
return `${apple} + ${banana}`;
}
pickFruits()<.then(console.log); //🍎 + 🍌 ... 3초만에 병렬적으로 실행이 된다!
🤚🏻 하지만 여기서 또! 사과를 따는 데 바나나가 필요없고, 바나나를 따는데 사과가 필요없어서 병렬적으로 기능을 수행할 수 있는 경우에는 이렇게 코드를 작성하지 않는다. 왜냐하면 promise에서 제공하는 아주 유용한 API
가 있기 때문이다!
promise 배열을 전달하게 되면 모든 promise들이 병렬적으로 다 받을 때까지 모아주는 기능을 한다.
→ 얘네들이 다 받아지게 되면 그러면(.then) 받아진 배열이 다시 전달이 된다.
function pickAllFruits() {
return Promise.all([getApple(), getBanana()])
.then(fruits => fruits.join('+'));
}
pickAllFruits().then(console.log); //🍎 + 🍌
어떤것이든 상관없이! 먼저 받아지는 첫번째 과일만 받겠다!
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
await delay(2000);
return "🍎";
}
async function getBanana() {
await delay(1000);
return "🍌";
}
function pickOnlyOne() {
return Promise.race([getApple(), getBanana()]);
}
pickOnlyOne.then(console.log); //🍌