front-end/ES6

비동기 프로그래밍에 관하여 - 3. async / await

정구 2022. 2. 21. 10:23
728x90

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로 변환이 된다.

 

똑같이 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);

병렬적으로 코드가 실행되어 대략 3000ms가 걸렸다.

 

하지만 비동기적인 코드가 늘어날수록 쓸데없이 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);
  }
}
728x90