이전 편에 이어서 작성해보는 비동기의 HighLight..
비동기의 꽃 🌸 async / await 🌸에 대해 정리해보고자 한다.
Promise
를 조금 더 간결하고 간편하게 작성해주기 위한 문법..!
아래 사진처럼 "동기적"으로 실행되는 것처럼 보이게 만드는 문법이다.
출처) 드림코딩
사진처럼 Promise.then
을 사용하여 체이닝을 계속해서 하게 되면, 조금 코드가 난잡해질 수 있는데 이런 거 위에 조금 더 간편한 async/await
을 사용하게 되면 우리가 그냥 동기식으로 코드를 순서대로 작성하는 것처럼 간단하고 간편하게 작성할 수 있다!
완전히 새로운 문법을 작성하는 것이 아니라 기존의 Promise
위에 조금 더 "설탕"을 뿌린 것이라고 생각하면 된다.
👉 syntatic sugar: 이렇게 기존에 존재하는 거 위에 또는 기존에 존재하는 것을 감싸서 우리가 조금 더 간편하게 쓸 수 있는 API를 제공하는 것.
ex) class 문법 -> 기존에 사용되어진 prototype을 베이스로 한 그 위에 살짝 덧붙여진 개념처럼.
// async / await
// clear style of using promise :) 깔끔하게 프로미스를 사용할 수 있는 방법
// ❗️ 그렇다고 무조건 프로미스가 나쁘고, async / await으로 대체하여 사용해야한다는 것이 아님. 프로미스를 써야 맞는 경우가 있고, async / await을 사용하여야 더 깔끔해지는 경우가 있는데 이는 프로젝트를 통해 감을 익혀가야함.
// 🚀 1. async 써보기
function fetchUser() {
// do network reqeust in 10 secs..
// 사용자의 데이터를 백엔드에서 받아오는 함수 & 네트워크 통신을 하여 백엔드에서 데이터를 받아오는데 10초가 걸리는 예제라고 생각해보자.
return "ellie";
}
const user = fetchUser();
console.log(user);
//
⭐️ 이렇게 무언가 오래 걸리는 코드를 비동기적인 처리를 전혀하지 않으면 자바스크립트 엔진은 동기적으로 코드를 수행하기 때문에 즉, 한 줄이 끝나야 다음 코드가 넘어가는 동기적인 처리를 하기 때문에 10초가 끝날때까지 위 코드에서 머무르다가 해당 시간이 지나고 네트워크 데이터를 성공적으로 받아오게 되면 다음 코드를 읽게 된다.
비동기적인 처리를 전혀하지 않으면 사용자 데이터를 받아오는데 10초가 걸리기 때문에 만약 이후 코드에서 웹 페이지에 UI를 표시하는 그런 기능을 수행하는 코드들이 있다면 이것이 끝나는 동안 데이터가 웹페이지에 표시되지 않기 때문에 사용자는 10초동안 텅텅 빈 웹페이지만 보게 되는 셈이다.
👉 때문에 이렇게 오래걸리는 작업들은 비동기적으로 처리할 수 있게 해주어야한다!
지난 시간은 아래처럼 Promise
를 사용하여 만들어주었는데
function fetchUser() {
// 내가 언제 유저의 데이터를 받아올지는 모르겠지만, 내가 약속할게..
// ⭐️ 프로미스라는 오브젝트를 가지고 있으면 여기에 너가 then이라는 callback함수만 등록해놓으면 유저에 데이터가 준비되는대로 니가 등록한 callback함수를 불러줄게! 하는 것처럼 약속의 개념.
return new Promise((resolve, reject) => {
// do network reqeust in 10 secs..
// 때문에 이 코드블럭 안에 있는 코드들이 비동기적으로 수행되는 원리.
return "ellie";
});
}
const user = fetchUser();
console.log(user);
위 예시에서 Promise
내부에 resolve
와 reject
를 사용하지 않고 return
하게 되면 콘솔에서 확인해볼 시 pending 단계인 것을 확인할 수 있다.
이 전에 작성한 포스트에서도 적어놨지만, 프로미스는 아래와 같은 3단계로 나뉘어진다.
- pending: 대기
- fulfilled: 완료
- rejected: 실패
❗️ 때문에, 꼭 프로미스 안에는 resolve
나 reject
를 사용하여 완료를 해주어야함..!
function fetchUser() {
return new Promise((resolve, reject) => {
// do network reqeust in 10 secs..
resolve("ellie"); // 콘솔을 통해 확인해보면 fulfilled상태로 바뀐 것을 확인할 수 있다.
});
}
const user = fetchUser();
// 때문에 user는 결국 프로미스를 리턴하기 때문에 then, catch와 같은 메서드 사용가능!!
user.then(console.log);
console.log(user);
자 이제 위처럼 작성해주지않아도 더 간편하게 비동기를 작성해줄 수 있는 방법이 있다.
바로 함수 앞에 async
라는 키워드를 붙여주는 것이다.
async function fetchUser() {
return "ellie";
}
const user = fetchUser();
user.then(console.log);
console.log(user);
이전처럼 번거롭게 내부 코드 블럭에 프로미스를 쓰지 않아도 자동적으로 함수 안에 있는 코드 블럭들이 프로미스로 변환되어 지는 것이다!!
위 코드를 실행하게 되면 fetchUser
가 Promise
를 리턴하는 것을 볼 수 있다 👀
// 🚀 2. 더 유용한 await 기다려!
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
가 붙은 함수 안에서만 쓸 수 있다.
여기서 위처럼 await
을 쓰게되면 비동기작업이 끝날때까지 기다려준다. 따라서 위 코드를 해석해보면 getApple
,getBanana
함수 내에서 사용되어지는 각각의 delay
가 끝날 때까지 기다리는 것이다.
즉, 3초가 있다가 사과를 리턴하는 프로미스가 만들어지게 되고, 또 3초가 있다가 바나나를 리턴하는 프로미스가 만들어지는 것.
위 예제를 프로미스를 이용하여 다시 작성해보면,
function getBanana() {
return delay(3000).then(() => "🍌");
}
이렇게 체이닝을 하는 것보다 async
예제처럼 "동기적인 코드를 쓰는 것처럼" 만들게 되면 더 쉽게 이해를 할 수 있을 것이다.
자 그럼 아래 예시를 다시보자.
// 🚀 2. 더 유용한 await 기다려!
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000);
return "🍎";
}
async function getBanana() {
await delay(3000);
return "🍌";
}
// ⛔️ 아래와 같이 프로미스 체이닝을 이용하게 되면 like callback hell..
function pickFruits() {
return getApple.then(apple => {
return getBanana().then(banana => `${apple} + ${banana}`);
});
}
// 위 함수를 최종적으로 부를 때에는 아래처럼 작성하여 다 받아와지면 콘솔이 출력되는 예제
// 6초를 기다린 후 결과 값이 출력이 될 것이다.
pickFruits().then(console.log);
위 pickFruits
함수를 보면 콜백지옥을 떠올리게 된다..
프로미스 또한 너무너무 중첩적으로 체이닝을 하게되면 마찬가지로 콜백지옥과 비슷한 문제점에 빠지게 되는 것이다. 그래서 위를 아래와 같이 async / await
을 사용하여 간단하게 만들어 줄 수 있다.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000);
return "🍎";
}
async function getBanana() {
await delay(3000);
return "🍌";
}
async function pickFruits() {
const apple = await getApple();
const banana = await getBanana();
return `${apple) + ${banana}`;
}
pickFruits().then(console.log);
프로미스를 사용하였던 위 예제와 비교해보자. 무엇이 다른가?
너무 간단해 진 것을 알 수 있고, 가독성 또한 더 좋아진 느낌..?!
그렇다면, 에러핸들링은 또 어떻게 해줄 수 있을까??
바로 아래처럼 try/catch
를 이용하여 핸들링을 해줄 수 있다!
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000);
return "🍎";
}
async function getBanana() {
await delay(3000);
return "🍌";
}
async function pickFruits() {
try {
const apple = await getApple();
const banana = await getBanana();
} catch(error) {
console.error(error);
}
return `${apple) + ${banana}`;
}
pickFruits().then(console.log);
들여쓰기 개똥같은 점 양해바랍니다.. ㅠㅠ
그런데, 이렇게 await을 통해 기다려! 하고 기다린 뒤에 또 코드를 실행하고 await을 사용하여 기다리게 되면 코드가 조금 비효율적이게 된다 🤔
바나나와 애플을 받아오는 데는 서로 연관이 없기에 서로 기다릴 필요가 없지 않을까?
때문에 위 코드를 더 개선해보면,
// 🚀 3. await 병렬 처리
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000);
return "🍎";
}
async function getBanana() {
await delay(3000);
return "🍌";
}
async function pickFruits() {
// 1. apple promise 만들기
// ❗️ 아래처럼 바로 만들면, 프로미스를 만드는 순간 바로 프로미스 안에 들어있는 코드 블럭이 실행되어짐.
const applePromise = getApple();
const bananaPromise = getBanana();
// 이제 아래처럼 여기에서 동기화를 시켜줌.
const apple = await applePromise;
const banana = await bananaPromise;
return `${apple) + ${banana}`;
}
pickFruits().then(console.log);
위처럼 실행하게 되면, 더 빠르게 코드가 실행되어지는 것을 확인할 수 있다.
즉, 사과와 바나나를 동시에 따서 한 번에 기다렸다가 출력하게 되는 것이다.
⭐️ 서로 연관이 없어 병렬적으로 기능을 수행할 수 있는 경우에는 위처럼 더럽게 코드를 작성하지 않고
유용한 Promise API
-> Promise.all
을 사용하면 된다.
// 🚀 4. useful Promise APIs -> Promise.all 사용하기
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000);
return "🍎";
}
async function getBanana() {
await delay(3000);
return "🍌";
}
function pickAllFruits() {
// 이것은 프로미스 배열을 전달하게 되면 모든 프로미스들이 병렬적으로 다 받을 때까지 모아주는 아이.
return Promise.all([getApple(), getBanana()]).then(fruits =>
fruits.join(' + ')
);
}
pickAllFruits().then(console.log);
// or
// 먼저 따지는 첫 번째 과일만 받아오고 싶다면 -> Promise.race APIs
// 사과를 따는데 2초 바나나 따는데 1초로 가정
function pickOnlyOne() {
return Promise.race([getApple(), getBanan()]);
}
pickOnlyOne().then(console.log);
// 콘솔에 바나나가 출력되는 것을 볼 수 있다.
Promise
에서 then
과 catch
, finally
API가 있는 것은 알았으나 이 외에 all
과 race
가 있는 것은 이번 강의를 통해 처음 알게 되었다. 호옹..🤔
지난 시간에 callback
을 Promise로 만들어주었던 부분을 다시 async/await
으로 깔끔하게 만들어보기
// 🚀 이전에 작성한 promise 코드
const userStorage = new UserStorage();
const id = prompt("enter your id");
const password = prompt("enter yout password");
userStorage.loginUser(id, password)
.then(userStorage.getRoles)
.then(user => alert("어쩌구 저쩌구 전하고자 하는 경고 내용.."));
.catch(console.error);
위와 아래 코드 비교해보기!
// 🚀 async / await 로 변경한 코드
const userStorage = new UserStorage();
const id = prompt("enter your id");
const password = prompt("enter your password");
async function checkUser() {
try {
const userId = await userStorage.loginUser(id, password);
const userInfo = await userStorage.getRoles(userId);
alert(`Hello ${userInfo.name}, you have a ${userInfo.role}`);
} catch (error) {
console.error(error);
}
}
checkUser();