비동기 프로그래밍에 관하여 - 3. async / await
Promise를 사용하면 체이닝을 통해 순차적으로 코드를 수행할 수 있다.
다만, 체이닝이 길어지고 복잡해진다면 코드 가독성이 떨어질 수 있다.
async와 await를 사용하면 마치 동기식 코드를 짜는거 마냥 간편하게 코드를 작성할 수 있다.
async / await은 새로운 기능을 추가하는 것이 아니라 기존의 promise에서 간편한 api를 제공한다.
이와 같이 기존에 존재하던 기능 위에 편리한 기능을 감싸서 사용하는 것을 syntactic sugar라고 한다.
사람이 프로그래밍 언어를 조금 더 '달콤하게'
이해하기 쉽고 간결하게, 명확하게 도와주는 문법이라고 생각하면 될 것같다.
async와 & await은 promise를 보다 깔끔하게 사용할 수 있는 방법이다.
그렇다고해서 promise가 나쁘다거나 async & await로 대체해서 사용해야하는 것이 아니다.
function fetchUser() {
return new Promise((resolve, reject) => {
//do network request in 10 secs...
resolve("username");
});
}
const user = fetchUser();
user.then(console.log);
위와 같은 비동기적 실행을 위한 코드가 있을 때, 이 코드를 수정하여보자.
async function fetchUser() {
return 'username'
}
자신이 비동기적으로 처리할 함수에 'async'키워드를 붙혀주면 자동적으로 Promise로 변환이 된다.
만약 async 키워드로 만들어진 함수 내부에서 동기적인 코드를 작성하고 싶다면 await 키워드를 사용하면 된다.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getSomething1() {
await delay(3000);
return "something1";
}
async function getSomething2() {
await delay(3000);
return "something2";
}
await는 콜백지옥을 해결하는데에도 도움이 된다.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getSomething1() {
await delay(3000);
return "something1";
}
async function getSomething2() {
await delay(3000);
return "something2";
}
function getAll() {
return getSomething1.then((one) => {
return getSomething2.then((two) => `${one} + ${two}`);
});
}
getAll.then(console.log);
위와 같이 promise끼리 콜백으로 물려있는 상황이 있을 수 있다.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getSomething1() {
await delay(3000);
return "something1";
}
async function getSomething2() {
await delay(3000);
return "something2";
}
async function getAll() {
const one = await getSomething1();
const two = await getSomething2();
return `${one} + ${two}`;
}
getAll.then(console.log);
async와 await 키워드만 사용했을 뿐인데, 우리가 익숙한 느낌으로, 마치 동기적인 코드를 작성할 수 있다.
그런데 위의 코드를 살펴보면 큰 문제점이 하나 있다.
바로 비동기적인 실행들이 모두 연결되어 있어, 병렬적 처리가 되지 않는다는 것이다.
getAll() 함수를 통해 결과값을 받아오기 위해서는 3000ms + 3000ms, 총 6초를 꼬박 기다리고 있어야한다.
하지만, 각각의 처리가 서로를 기다려줄 필요는 없다.
웹 페이지에서 서버와 통신하는 api가 늘어날 수록 페이지가 표시되기까지 걸리는 시간은 어마어마하게 늘어나버릴것이다.
해결방안 중 하나는 임의로 Promise 객체를 만들어버리는 것이다.
promise 객체에 작성되어있는 코드 excutor는 promise가 생성되자마자 실행되는 특징이 있다.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getSomething1() {
await delay(3000);
return "something1";
}
async function getSomething2() {
await delay(3000);
return "something2";
}
async function getAll() {
const promise1 = getSomething1();
const promise2 = getSomething2();
const one = await promise1;
const two = await promise2;
return `${one} + ${two}`;
}
getAll().then(console.log);
하지만 비동기적인 코드가 늘어날수록 쓸데없이 promise객체를 생성하는 지저분한 코드가 될 것이다.
그렇기에 promise에서는 이를 해결하기 위한 all() 메서드를 제공한다.
각각의 promise 값들을 배열으로 묶어서 내보낸다.
async function getAll() {
const promise1 = getSomething1();
const promise2 = getSomething2();
const one = await promise1;
const two = await promise2;
return `${one} + ${two}`;
}
getAll().then(console.log);
/////////////////////////////////////////////
function getAll2() {
return Promise.all([getSomething1(), getSomething2()]).then((result) =>
result.join(" + ")
);
}
getAll2().then(console.log);
getAll()과 getAll2()는 동일한 결과를 반환한다.
병렬적인 처리를 진행하면서 가장 처음으로 완료되는 하나의 결과만 반환받을수도 있다.
Promise.race() 메서드를 사용하면 된다.
function getOnlyOne() {
return Promise.race([getSomething1(), getSomething2()]);
}
getOnlyOne().then(console.log);
지난 시간 Promise의 체이닝을 통해 작성한 코드가 있었다.
class UserStorage {
loginUser(id, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (
(id === "user" && password === "user") ||
(id === "admin" && password === "admin")
) {
resolve(id);
} else {
reject(new Error("not found"));
}
}, 2000);
});
}
getRoles(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user === "admin") {
resolve({ name: "admin", role: "admin" });
} else {
reject(new Error("no access"));
}
}, 1000);
});
}
}
const userStorage = new UserStorage();
const id = prompt("enter user id");
const password = prompt("enter user password");
userStorage
.loginUser(id, password)
.then(userStorage.getRoles)
.then((user) => alert(`hello ${user.name}, you have a ${user.role} role`))
.catch(console.log);
위의 코드를 async와 await를 활용하여 수정하면 다음과 같다. (맨 아래부분 실행 로직만 변경시키겠다.)
const userStorage = new UserStorage();
const id = prompt("enter user id");
const password = prompt("enter user password");
//1. Promise
userStorage
.loginUser(id, password)
.then(userStorage.getRoles)
.then((user) => alert(`hello ${user.name}, you have a ${user.role} role`))
.catch(console.log);
//2. async & await
async function checkUser() {
try {
const userId = await userStorage.loginUser(id, password);
const user = await userStorage.getRoles(userId);
alert(`hello ${user.name}, you have a ${user.role} role`);
} catch (error) {
console.log(error);
}
}