본문 바로가기
Front-end/Vanilla JS

JS 근본 공부 - async & await

by devraphy 2020. 11. 9.

1. async와 await란? 

- async와 await는 지난 포스팅에서 설명한 promise를 더욱 간결하고 동기적으로 실행되는 것처럼 보이게 만들어 주는 기능이다. 

 

- 지난 포스팅에서 then을 사용하여 promise chaining을 구현했는데, 직관적이고 사용하기 편하지만 코드 자체를 봤을 때에는 복잡해 보인다는 것이 단점이다.  

 

- 이러한 단점을 보완할 수 있도록, promise를 동기적으로 실행하는 것처럼 보이게 하는 역할이 async와 await다. 

 

- 프로그래밍 용어로, async와 await같은 기능들을 sytactic sugar라고 한다. 

 

*syntactic sugar - async와 await처럼, 기존에 존재하는 기능을 더욱 사용하기 편하도록 제공되는 API를 가리킨다. 

 


2. async 사용법

- promise를 사용할 때, 항상 async와 await를 사용해야만 하는 것은 아니다. 

- promise의 코드가 복잡해 보일 때, 이를 조금 더 깔끔한 형태로 만들어주기 때문에 상황에 맞게 사용한다.

 

a) async 코드구현

- async 사용법을 설명하기 앞서, promise가 어떤식으로 처리되는지 예시를 만들어 보자. 

 

- 네트워크 request를 통해 사용자 정보를 받아오는데 10초가 걸리는 메소드가 있다고 가정하자.

function fetchUser() {
 // 여기서 10초 후에 반환값이 return 된다고 가정해보자.
  return "dev_raphy";
}

const user = fetchUser();
console.log(user);

 

- JavaScript는 동기적인 처리를 하는 언어이기 때문에, fetchUser()처럼 처리가 오래 걸리는 코드를 비동기적 처리를 하지 않으면 다음 코드로 넘어가지 못하여 해당 코드(fetchUser)의 처리가 끝날 때 까지 머무르게 된다. 즉, 사용자가 10초를 기다려야되는 상황이 발생한다.

 

- 이를 해결하기 위해, 비동기적 처리로 사용했던 방법이, 지난 시간에 포스팅했던 promise다. 

function fetchUser() {
  return new Promise((resolve, reject) => {
    return "dev_raphy";
  });
}

const user = fetchUser();
console.log(user);

 

- 만약 promise를 작성한 후 resolve와 reject를 이용해 완전히 처리하지 않으면 promise는 pending상태가 된다. 

pending - 진행 중

 

- 그러므로 promise를 사용한다면, 반드시 resolve와 reject를 이용하여 성공/실패 케이스의 처리를 완료해야 한다. 

fulfilled - 만족

 

- resolve 또는 reject를 이용하여 처리를 한다면, 위의 사진처럼 조건을 만족한다는 fulfilled 상태로 바뀌게 된다. 

function fetchUser() {
  return new Promise((resolve, reject) => {
    resolve("dev_raphy");
  });
}

const user = fetchUser();
console.log(user);
// user.then(변수명) => console.log(변수명)
// * 변수명 = 개발자가 직접 설정, fetchUser의 return값을 담는다.
user.then((userName) => console.log(userName));
user.then(console.log); // 위의 코드와 동일한 코드, 매개변수가 동일하다면 생략가능

출력 결과

 

- promise를 작성하지 않고도 간편하게 비동기식 처리를 하는 방법이 있는데, 이것이 async다. 

async function fetchUser() {
  return "dev_raphy";
}

const user = fetchUser();
console.log(user);
user.then((userName) => console.log(userName));
user.then(console.log);

 

- promise를 작성하지 않아도 async를 사용하면 함수안의 코드가 내부적으로 promise 형식으로 사용된다.

- 즉, promise를 작성하지는 않아도 간편하게 promise를 사용할 수 있게 되는 것이다. 

 

출력 결과


3. await 사용법

-  await는 async가 붙은 함수 안에서만 사용할 수 있는 기능이다. 

- 예시를 보면서 이해해보자. 

 

 

a) awiat 코드구현

- ms(밀리세컨드)를 매개변수로 하는 delay()라는 함수가 있다. 

- 매개변수에 따라 delay되는 시간을 설정할 수 있는 기능의 함수이다. 

- 정해진 시간이 끝난 뒤에 resove를 호출하게 된다. 

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

 

 

- 이 delay()라는 함수를 이용하여 다음과 같은 코드를 작성해보았다. 

async function getApple() {
  await delay(3000);
  return "🍎";
}

async function getBanana() {
  await delay(3000);
  return "🍌";
}

 

- 위의 코드에서, getApple()과 getBanana()는 3초가 지난 후에 resolve를 전달하는 함수다. 

- 여기서 await를 사용하게 되면, delay()가 끝날 때까지 기다린 후에 사과/바나나를 return하는 promise가 실행되는 것이다. 

 

- await를 사용하지않고 위의 코드를 작성하게 된다면 다음과 같다. 

async function getApple() {
  return delay(3000).then(() => "🍎");
}

async function getBanana() {
  return delay(3000).then(() => "🍌");
}

 

- await를 사용했을 때와 사용하지 않았을 때의 코드를 비교해보자. 

- await를 사용하면 delay()가 실행된 후에 return이 되어 동기적인 처리를 하는 것처럼 보이는 코드를 작성한다. 

- 반면에 await를 사용하지 않으면 await를 사용한 코드와 비교했을 때 상대적으로 코드의 가독성이 떨어지고, 복잡한 경우에는 코드를 이해하기 어려울 수 있다. 

 

- 자, 이제 위의 getApple()과 getBanana()함수를 모두 호출하여 출력하는 pickFruits()라는 함수가 있다고 가정해보자. 

- await를 사용하지 않으면 다음과 같이 작성할 수 있다.

function pickFruits() {
  return getApple().then((apple) => {
    getBanana().then((banana) => `${apple} + ${banana}`);
  });
}

pickFruits().then(console.log);

출력 결과

- 위의 pickFruits()의 콜백함수들을 보면 이전 포스팅에서 보았던 코드와 비슷한 양상을 보인다. 무엇일까?

- 바로... 콜백 지옥이다. 콜백 안에도 다시 콜백을 함수하는 콜백 지옥코드와 동일한 양상을 보이게 된다.

 

- 이제, async를 사용해서 위의 코드를 다시 작성해보자. 

async function pickFruits() {
  const apple = await getApple();
  const banana = await getBanana();
  return `${apple} + ${banana}`;
}

출력 결과

 

- async와 await를 사용하면 콜백지옥과 같은 양상을 보였던 코드와는 다르게 깔끔한 코드를 작성할 수 있는 것을 볼 수 있다. 

 

- 그러나, 위의 코드에서도 개선해야할 점이 한가지 있다. getApple()과 getBanana()를 실행하는데 각각 3초, 총 6초의 시간이 걸리는 부분이다. 

 

- getApple()과 getBanana()는 전혀 연관이 없는 함수이기 때문에, 하나가 완료될 때까지 기다리는 것이 아니라 병렬적으로 처리할 수 있기 때문이다. 

 

 

c) 에러처리

- 지금까지는 resolve를 구현하는 코드를 작성해보았다. 그렇다면 reject 또는 에러처리는 어떻게 작성할 수 있을까?

- 에러처리 부분은 try ~ catch를 사용하여 구현할 수 있다. 

async function pickFruits() {
    try {
        const apple = await getApple();
        const banana = await getBanana();
    } catch {
        // 에러처리 구문 작성부분 
    }
    return `${apple} + ${banana}`;
  }

 

 

b) 병렬처리

- promise를 선언하면 hoisting으로 인해, 바로 코드가 읽힌다는 것을 지난 포스팅에서 배웠다. 이 특성을 사용하여 병렬처리를 구현할 수 있다. * 병렬처리 - 다른 연산이 동시에 수행되는 것

 

async function pickFruits() {
    const applePromise = getApple();
    const bananaPromise = getBanana();

    const apple = await applePromise;
    const banana = await bananaPromise;

  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);

- 위의 코드처럼 작성하게되면, getApple()과 getBanana()가 동시에 처리되어 6초를 기다리는 것이 아니라 3초만에 출력되는 것을 확인할 수 있다. 

 

 

d) 개선된 병렬처리 방법

- 위에서 설명한 병렬처리 방법은 실제로 많이 사용되는 병렬처리 방법이 아니다. 

- 현업에서는 promise를 이용한 병렬처리를 할 때, promise API에 있는 함수를 이용한다. 

- 다음과 같은 방법으로 사용한다. 

function pickAllFruits() {
  return Promise.all([getApple(), getBanana()]).then((fruits) =>
    fruits.join(" + ")
  );
}

pickAllFruits().then(console.log);

- Promise.all() 함수는 배열을 이용하여 개발자가 원하는 promise를 매개변수로 전달하여 병렬처리하는 방식이다. 

- Prmoise.all() 함수는 반환된 promise를 배열 형식으로 return해주기 때문에 배열에 담겨있는 요소들을 문자열로 변환해주는 join() 함수를 사용하여 출력해줄 수 있다. 

 

출력 결과

 

e) Promise.race()

- 만약 우선적으로 완료된 promise만 받아오고 싶다면 어떻게 구현할까? 

- getApple()이 1초가 걸리고 getBanana()가 3초가 걸린다면, 더 빠르게 처리된 getApple()만 받아서 출력하는 예시를 코드로 구현해보자. 

function pickOnlyOne() {
  return Promise.race([getApple(), getBanana()]);
}

pickOnlyOne().then(console.log);

출력 결과

- Promise.race() 함수는 Promise.all()과 동일하게 여러개의 promise를 배열형태로 전달하여, 가장 먼저 처리가 끝나는 promise의 반환값만을 받아와서 return하는 함수이다. 

- getApple() 함수가 1초만에 처리되고 getBanana()는 3초가 걸리기 때문에, 사과만 반환되어 출력되는 것을 볼 수 있다.

 


4. 코드개선

- 지난 포스팅에서 콜백지옥이 된 코드를 promise를 사용하여 개선하였다. 

- 이번에는 promise로 개선된 코드의 customer부분을 async와 await를 사용하여 더욱 깔끔하게 만들어보자. 

 

[개선해야 할 코드 - 지난 포스팅에 작성된 코드] 

"use strict";

// Producer
class UserStorage {
  loginUser(id, password) {
    // 기존 코드의 성공,실패여부를 관리하던 메소드를 promise로 대체한다.
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (
          (id === "kevin" && password === "kevin123") ||
          (id === "admin" && password === "master")
        ) {
          resolve(id);
        } else {
          reject(new Error("not found"));
        }
      }, 2000);
    });
  }

  getRoles(user) {
    // 기존 코드의 성공,실패여부를 관리하던 메소드를 promise로 대체한다.
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (user === "kevin") {
          resolve({ name: "kevin", role: "developer" });
        } else {
          reject(new Error("not found"));
        }
      }, 2000);
    });
  }
}

// Customer
const userStorage = new UserStorage();
const id = prompt("enter your id: ");
const password = prompt("enter your password: ");

userStorage
  .loginUser(id, password)
  .then(userStorage.getRoles) // .then((user) => userStorage.getRoles(user));
  .then((user) => alert(`Hello, ${user.name}. You have a ${user.role} role.`))
  .catch(console.log); // .catch((error) => console.log(error));

 

 

 

[async와 await를 사용하여 개선된 코드]

// Customer
async function getUserInfo() {
  const userStorage = new UserStorage();
  const id = prompt("enter your id: ");
  const password = prompt("enter your password: ");
  try {
    const loginId = await userStorage.loginUser(id, password);
    const loginRole = await userStorage.getRoles(loginId);
    return alert(
      `Hello, ${loginRole.name}. You are a fantastic ${loginRole.role}!!!`
    );
  } catch {
    return alert(`error: ${new Error("not found")}`);
  }
}

getUserInfo();

 

 

 

[정보출처]

www.youtube.com/watch?v=aoQSOZfz3vQ&list=PLv2d7VI9OotTVOL4QmPfvJWPJvkmv6h-2&index=13

 

'Front-end > Vanilla JS' 카테고리의 다른 글

JS 근본 공부 - Promise  (2) 2020.11.06
JS 근본 공부 - 콜백(Callback)  (0) 2020.11.05
JS 근본 공부 - JSON  (0) 2020.11.04
JS 근본 공부 - 배열 API 연습문제  (0) 2020.11.03
JS 근본 공부 - 배열(Array)  (0) 2020.11.02

댓글