본문 바로가기

JavaScript

JavaScript - async/await

Promise객체로 인해 callback hell문제를 해결하고, 비동기 실행코드를 보기좋게 작성하는 것이 가능해졌다.

그럼에도 자바스크립트 문법은 계속 발전해서 Promise객체를 더 편하게 다룰 수 있는 문법이 생겨났고

그것이 바로 async/await이다.

// fetch의 경우
fetch('https://learn.codeit.kr/api/members/1')
    .then((response) => response.text())
    .then((result) => {console.log(result);})

// async/await의 경우
async function fetchAndPrint(){
    const response = await fetch('https://learn.codeit.kr/api/members/1');
    const result = await response.text();
    console.log(result);
}

fetchAndPrint();

위 코드의 fetch와 async/await은 같은 기능을 한다.

async/await 경우를 보면, 함수의 앞에는 async, promise객체를 return하는 함수 앞에는 await 붙어있다.

 

async

  • asynchronous 의 줄임말.
  • 함수 안에 비동기적으로 실행되는 부분이 있다는 것을 알려준다.
  • 비동기적으로 실행될 부분이란, 위 예시에서 await 오른편으로 작성된 로직이다.

await

  • Promise객체를 리턴하는 코드의 앞에 붙여서, await뒤의 코드를 수행한 이후 리턴된 Promise객체가 settled될 때 까지 기다린다.
  • settled되면 그 결과의 response객체를 추출해서 리턴한다.
  • async 함수의 안에서만 사용 가능하다.

 

async가 붙어있는 함수 안에 비동기 실행을 하는 부분이 있다 = async가 붙어있는 함수 안에 Promise객체를 리턴하는 코드가 있다.

 

async/await구문의 실행 원리

async function fetchAndPrint(){
    console.log(2);
    const response = await fetch('https://learn.codeit.kr/api/members/1');
    console.log(7);
    const result = await response.text();
    console.log(result);
}

console.log(1);
fetchAndPrint();
console.log(3);
console.log(4);
console.log(5);
console.log(6);

위 코드를 실행해보면, 숫자 순서대로 출력된다.

그 실행의 순서는 아래와 같다.

  1. console.log(1)
  2. fetchAndPrint함수의 console.log(2)
  3. await를 만나(fetch() 앞) 함수밖으로 실행 순서 이동
  4. console.log(3)
  5. console.log(4)
  6. console.log(5)
  7. console.log(6)
  8. fetch() 완료
  9. console.log(7)
  10. await(text() 앞)를 만나 함수 밖으로 실행 순서 이동
  11. 하지만 함수밖에 실행할 로직이 없다.
  12. response.text()완료
  13. console.log(result);

첫번째 await을 만나고, 실행순서가 함수 밖으로 이동하면서 함수밖의 로직들이 콜스택에 쌓여있기때문에

fetch작업이 먼저 끝났더라고 함수 밖 로직이 끝나기 전까지는 함수 내부로직으로 다시 돌아갈 수 없다.

 

async함수는 함수밖의 코드들이 실행되고 나서 또 실행될 코드들이 남아있다는 것을 의미한다.

그리고 함수밖의 코드들이 실행되고 나서 실행될 코드들이 바로 await이 붙은 로직들을 의미한다.

 

async/awiat함수는 마치 동기 함수처럼 생겼는데,

구문 자체가 기존의 Promise chaining을 

  1. 개발자가 더 편하게 작성할 수 있도록 하기 위해서,
  2. 코드의 가독성을 높이기 위해서

도입된 일종의 Syntatic sugar(기존 문법을 더 편하게 사용할 수 있도록 하는 문법적 장치)에 해당하기 때문이다.

우리는 async/await문법으로 Promise객체를 우리에게 익숙한 동기 실행 코드 방식으로 다룰 수 있게 되었다.

동기 실행 코드처럼 생겼으나, async/await은 비동기로 실행되며 promise객체를 다룬다는 것을 기억하자.


async / await 에서의 에러 처리 방법

Promise객체에서는 catch메서드를 사용했었는데, async / await에서는 try-catch문을 사용한다.

async function fetchAndPrint(){
    try {
        const response = await fetch('https://learn.codeit.kr/api/members/1');
        const result = await response.text();
        console.log(result);
    } catch (error){
        console.log(error);
    }
}

promise객체의 상태가 rejected가 되는 순간, catch문으로 이동한다.

 

finally문도 추가할 수 있다.

async function fetchAndPrint(){
    try {
        const response = await fetch('https://learn.codeit.kr/api/members/1');
        const result = await response.text();
        console.log(result);
    } catch (error){
        console.log(error);
    } finally {
        console.log('exit');
    }
}

async / await 의 리턴값

async함수는 항상 promise객체를 리턴한다.

async function fetchAndPrint(){
      return 3;
}

위 코드는 async함수이고 숫자 3을 리턴중임에도

작업성공결과로 숫자 3을 가지는 fulfilled상태의 promise객체를 리턴하게된다.

 

이러한 결과는 then메서드의 리턴값과 유사하다.

아주 똑같지만 아래와 같이 정리해본다.

1. promise객체를 리턴

async function fetchAndPrint() {
  return Promise.resolve('Success');
}

fetchAndPrint();
  • async함수 안에서 promise객체를 리턴하는 경우에는 해당 Promise객체와 동일한 상태과 결과를 가지는 promise객체를 리턴한다.

2. promise객체가 아닌 값을 리턴

async function fetchAndPrint() {
  return 3;
}

fetchAndPrint();
  • 숫자, 문자열, 배열, 객체 등 promise객체가 아닌 다른 값을 리턴하는 경우, fulfilled상태이면서 리턴시킨 값을 작업성공결과로 가지는 promise객체를 리턴한다.

3. 아무 값도 리턴하지 않음

  • fulfilled상태이면서, undefined를 작업 성공결과로 가지는 promise객체를 리턴한다.

4. async함수 내부에서 에러가 발생

  • rejected상태이면서, 에러객체를 작업 실패 정보로 가지는 promise객체를 리턴한다.

asyn함수 안에서의 async함수 사용

  • 아래 예제는 사용자의 address와 phone프로퍼티를 제외한 나머지 정보만을 가지고 오는 코드이다.
  • async함수 안에서 async함수를 사용하고있다.
  • async함수는 항상 promise객체를 리턴하기때문에 그 앞에 await을 붙여서 사용할 수 있다.
  • async함수안의 async함수안에서도 async함수를 사용할 수 있다.
const applyPrivacyRule = async function (users) {
    const resultWithRuleApplied = users.map((user) => {
        const keys = Object.keys(user);
        const userWithoutPrivateInfo = {};
        keys.forEach((key) => {
            if(key !== 'address' && key !== 'phone'){
                userWithoutPrivateInfo[key] = user[key];
            }
        });
        return userWithoutPrivateInfo;
    });

    // 작업이 오래걸리는 예시를 들기위해 setTimeout함수 사용했다.
    // 아래의 프로미스객체를 리턴한다고 봐도 된다.
    const p = new Promise((resolve, reject) => {
        setTimeout(() => { resolve(resultWithRuleApplied); }, 2000)
    });
    return p;
}

async function getUesers() {
    try{
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        const result = await response.text();
        const users = JSON.parse(result);
        const resultWithPrivacyRuleApplied = await applyPrivacyRule(users);
        return resultWithPrivacyRuleApplied;
    } catch (error) {
        console.log(error);
    } finally {
        console.log('exit');
    }
}

getUesers().then((result) => { console.log(result); });

getUsers라는 async함수안에서, applyPrivacyRule이라는 async함수를 사용하고 있는 모습을 볼 수 있다.


async를 붙이는 위치

// Function Declaration
async function example1(a, b) {
  return a + b;
}

// Function Expression(Named)
const example2_1= async function add(a, b) {
  return a + b;
};

// Function Expression(Anonymous)
const example2_2 = async function(a, b) {
  return a + b;
};

// Arrow Function
const example3_1 = async (a, b) => {
  return a + b;
};

// Arrow Function(shortened)
const example3_2 = async (a, b) => a + b;

// 즉시실행함수
(async function (a, b) {
  return a + b;
}(1, 2));

// 즉시실행함수(shortened)
(async (a, b) => a + b)(1, 2);
  • function키워드가 있으면 키워드 앞에, 없으면 파라미터 앞에 작성

async 함수를 작성할 주의해야할 성능 문제

async function getResponses(urls) {
  for(const url of urls){
    const response = await fetch(url);
    console.log(await response.text());
  }
}
  • 위 코드는 순서대로 response가 console에 찍힌다.
  • await를 만나면서 작업이 중단되기때문이다.
  • 만약 순서 상관 없이 찍히기만되는 로직을 짜려했다면 위 코드는 성능면에서 아쉬운 코드가 되겠다.
  • 아래와 같이 수정가능하다.
async function fetchUrls(urls){
  for(const url of urls){
    (async () => {
      const response = await fetch(url);
      console.log(await response.text());
    })();
  }
}
  • 위 코드는 async를 붙인 즉시실행함수를 for문안에 넣었다.
  • 이러면 await를 만나면서 함수 밖으로 실행순서가 이동되고, 밖의 로직을 먼저 진행시키다보니 결국 작업이 먼저 끝난 response.text부터 콘솔에 찍히게 된다.
  • 순차적인 처리가 필요한게 아니라면, 이처럼 각 작업을 async함수로 묶어주면 된다.

정리

비동기 실행의 의미

  • 특정 작업이 시작되고, 그 작업이 모두 완료되기 전에 바로 다음 코드가 실행되는 방식의 실행, 나머지 작업은 나중에 콜백을 통해 수행되는 방식의 실행
  • 특정 처리를 나중으로 미루는 방식의 실행
  • 콜백을 등록해두고, 추후에 특정 조건이 만족되면 콜백으로 나머지 작업을 수행하는 방식의 실행

 

비동기실행 관련 문법사용에 주의점

  1. 콜백을 함수의 파라미터로 바로 전달하는 전통적인 방식의 비동기 실행 함수들 중에서도 setInterval, addEventListener처럼 콜백이 여러 실행되어야 하는 것들은 Promisify하면 안된다. Promise 객체는 한번 settled되고나면 상태와 결과가 다시는 바뀌지 않기 때문.
  2. async/await 구문의 경우, await async 함수 안에서만 사용할 있고, 코드의 top-level(어떤 함수 블록 안에 포함된 것이 아닌 코드의 최상위 영역)에서는 사용될 수 없다. 그래서 코드의 top-level에서 async 함수가 리턴한 Promise 객체의 작업 성공 결과를 가져오려면 await 사용할 수는 없고, 여전히 then 메소드를 사용해야한다. >> 이게 무슨말일까?? 찾아봐야겠다...