ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JS/Asynchronous] Async / Await 다루기
    Frontend 2019. 8. 10. 23:48

     

    Promise에 이어서 오늘은 Async/Await에 대해서 정리해보려고 한다.

     

    Async/Await

    Async/await은 ES8에서 새롭게 도입되었다. async/await은 promise를 좀 더 이해하기 쉽게 사용하기 위해서 탄생하였다. 그러니까 promise와 전혀 다른 별개의 개념이 아니라 promise를 간단하게 사용하기 위한 문법인 것이다.

     

    그래서 async/await은 기존에 우리가 알던 promise 구문보다 훨씬 간단한 구조로 되어있다.

     

     

    function makeWordPlural (word) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          word += "'s ";
    
          resolve(word);
        }, 1000);
      });
    }

     

    먼저 promise 객체를 리턴하는 함수, makeWordPlural을 선언해주었다.

     

     

    async function add1 (person1, person2, something) {
      const a = await makeWordPlural(person1);
      const b = await makeWordPlural(person2);
    
      return a + b + something;
    }
    
    add1('Julia', 'Mom', 'cake').then(result => {
      console.log(result);
    });

     

    이제 비동기 작업을 할 함수 앞에 async라고 적으면 이 함수는 AsyncFunction객체를 반환한다. 비동기 함수는 이벤트 루프를 통해 비동기적으로 작동하는 함수이지만 마치 동기 함수를 사용하는 것과 문법이 매우 비슷하다.

     

    위 예제를 보면 await식을 사용하여 promise를 리턴받고 있는데, 이 await식은 async 함수의 실행을 일시적으로 중지하고 전달된 promise가 해결된 후에 다시 다음 구문으로 넘어간다. 또한 변수에 promise의 결과값을 저장할 수 있다.

     

    따라서 위 코드는 약 2초 후에 최종 결과를 리턴한다.

     

    여기서 꼭 기억해야할 점은 await 키워드는 async 함수에서만 유효하다는 점이다. 이 점 때문에 배열을 iterate하여 await 구문을 사용할 때 주의해야 한다. (자세한 내용은 아래에서 다룬다.)

     

    또한 async 함수는 리턴되는 모든 결과값이 promise.resolve로 암묵적으로 감싸진다.

    즉, 위 코드에서 a + b + something을 리턴하면 자동으로 그 값이 promise.resolve의 인자로 전달된 것이 리턴된다.

     

     

    async function add2 (person1, person2, something) {
      const a = makeWordPlural(person1);
      const b = makeWordPlural(person2);
    
      return await a + await b + something;
    }
    
    add2('Julia', 'Mom', 'cake').then(result => {
      console.log(result);
    });

     

    만약 위 코드를 이렇게 바꾼다면 어떻게 될까?

    이번엔 await식을 return문에서 한꺼번에 실행한 것과 마찬가지기 때문에

    promise 실행이 병렬로 이루어지고 최종 결과값은 약 1초 후에 리턴된다.

     

     

     


     

    Async/await - sequential execution

    const numbers = [2, 5, 1, 3, 4];
    
    function printNumber (n) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(n * 100);
          console.log(n * 100);
        }, n * 100)
      });
    }

     

    만약에 우리에게 위 코드와 같은 배열과 프로미스를 리턴해주는 함수가 주어졌다고 생각해보자.

    우리는 배열의 숫자들을 순차적으로 printNumber()에 넣고 실행하려고 한다.

     

    다만 모든 요소를 병렬로 처리하지 않고 첫 번째 요소가 실행이 끝난 후 두 번째 요소를 실행시키고,

    두 번째 요소의 실행이 끝난 후 세 번째 요소를 실행시키는 방식으로 하고 싶다고 해보자.

     

     

    async function processPrint (numbers) {
      let result;
    
      numbers.forEach((n) => {
        result = await printNumber(n);
      });
    
      console.log('finished', result);
    }
    
    processPrint(numbers)
      .then((final) => console.log(final));

     

    가장 생각하기 쉬운 코드는 바로 forEach문으로 loop을 돌려서 처리하는 것이다.

     

     

    그러나 위 코드는 에러를 발생시킨다.

    이유는 아까 살짝 언급했듯이 await 키워드는 async 함수에서만 유효하기 때문이다.

    위 코드에서 processPrint 함수는 비동기 함수이지만

    forEach 함수 내부에서 실행되는 iterator는 동기 함수이기 때문에 에러가 발생한다.

     

     

    async function processPrint (numbers) {
      let result;
    
      numbers.forEach(async (n) => {
        result = await printNumber(n);
      });
    
      console.log('finished', result);
      return result;
    }
    
    processPrint(numbers)
      .then((final) => console.log(final));

     

    위 방법의 해결책은 forEach문 안에서 다시 async를 선언하는 것이다.

     

     

    그러나 이 방법의 문제점은

    배열의 각 요소가 실행이 끝나야만 다음 요소가 실행되는 것이 아니라

    모든 요소가 거의 동시에 실행된다는 점이다.

     

    그리고 async 함수 내에서 또 다른 async 함수들이 비동기로 실행되기 때문에

    마지막 결과값을 출력하는 console.log()가 먼저 실행된 후에 

    비동기로 숫자가 콘솔 창에 출력된다.

     

     

    1. for...of문

    async function processPrint (numbers) {
      let result;
    
      for (n of numbers) {
        result = await printNumber(n);
      }
    
      console.log('finished', result);
      return result;
    }
    
    processPrint(numbers)
      .then((final) => console.log('resolved', final));

     

    해결책은 for문이나 for...of문을 사용하는 것이다.

    이 방법을 사용하면 원하던 대로 배열의 요소가 하나씩 차례대로 처리되고

    마지막 결과값도 출력할 수 있다.

     

     

     

    2. reduce()

    async function processPrint (numbers) {
      return numbers.reduce(async (prevPromise, n) => {
        await prevPromise;
    
        return printNumber(n);
      }, Promise.resolve());
    }
    
    processPrint(numbers)
      .then((final) => console.log('resolved', final));

     

    for loop을 사용하지 않고 그냥 메소드를 사용하여 순차 처리하는 방법으로 바로 reduce()가 있다.

     

    위 코드를 실행하면 첫 번째 인자로 들어간 Promise.resolve()를 prevPromise로 하여 코드를 실행한게 된다.

     

    첫 번째 프로미스의 반환값은 없으므로 배열의 첫 번째 숫자 2를 printNumber() 함수에 넣어 프로미스를 리턴한다.

    리턴된 프로미스는 다음 차례에서 prevPromise로 넘어가므로 await식을 만나 결과값이 반환될때까지 기다린다.

     

    결과값인 200이 반환되면 다시 다음 인자인 5를 printNumber()의 인자로 넣고 반환된 promise를 리턴한다.

    리턴된 프로미스는 다음 차례에서 prevPromise로 넘어가므로 await식을 만나 다시 기다린다.

     

    이 과정을 반복하다가 마지막 요소 4의 결과값이 반환되면 그 결과값인 400을 리턴하는데,

    async 함수는 결과로 리턴된 값이 프로미스가 아니어도 자동으로 프로미스로 감싸서 반환한다.

    그렇기 때문에 processPrint()의 결과값은 마지막 요소의 결과값을 담은 프로미스가 리턴되어

    then 구문이 살행되고 콘솔 창에 'resolved' 400이 마지막으로 찍히게 된다.

     

     

     

    Async/await - Parallel execution

    async function processPrint (numbers) {
      const promises = numbers.map(n => printNumber(n));
    
      const result = await Promise.all(promises);
      console.log(result);
      
      return result;
    }
    
    processPrint(numbers)
      .then((final) => console.log('resolved', final));

     

    반대로 병렬로 처리하는 방법은 매우 쉽다.

    배열을 map 함수를 이용하여 각 배열의 요소를 인자로 하는 프로미스를 배열로 만들고

    Promise.all을 사용하면 된다.

     

     

     

    Async/await - Parallel execution을 이용하여 AJAX 처리

     

    async/await을 연습해보기 위해 간단하게 Hacker news API를 사용하여

    뉴스 데이터를 받아와 출력하는 연습을 해보았다.

     

     

    function createList (newsDatas) {
      $ul.innerHTML = '';
    
      newsDatas.forEach((news, i) => {
        const $li = document.createElement('li');
        const $innerLi = `<span class="list-num">${i + 1}.</span>`
        +`<a href="${news.url}" class="title" target="_blank">${news.title}</a>`
        +`<span class="sub-text"><span class="mr5">${(news.score) ? news.score : 0} points</span>`
        +`<span class="name mr5"><a href="#">by ${news.by}</a></span>`
        +`<span class="time bar"><a href="#">${getTimeGap(news.time)}</a></span>`
        +`<span class="bar"><a href="#">hide</a></span>`
        +`<span class="comments"><a href="#">${(news.kids) ? news.kids.length : 0} comments</a></span></span>`;
    
        $li.innerHTML = $innerLi;
        $ul.appendChild($li);
      });
    }

     

    먼저 받아온 데이터를 이용하여 ul 태그안에 li 태그를 동적으로 생성하는 함수를 만들었다.

     

     

    const HACKER_URL = 'https://hacker-news.firebaseio.com/v0/';
    const LIST_URL = `${HACKER_URL}topstories.json`;
    
    const $ul = document.querySelector('.news-list');
    
    function getNewsIds () {
      let allNewsData;
    
      return new Promise((resolve, reject) => {
        if (!allNewsData) {
          $.get({
            url: LIST_URL,
            success: (data) => {
              allNewsData = data;
              resolve(data);
            },
            error: (error) => {
              reject(error);
            }
          });
        } else {
          resolve(allNewsData);
        }
      });
    }
    
    function getNewsDetail (id) {
      return new Promise((resolve, reject) => {
        $.get({
          url: `${HACKER_URL}item/${id}.json`,
          success: (newsData) => {
            resolve(newsData);
          },
          error: (error) => {
            reject(error);
          }
        });
      });
    }

     

    getNewsIds는 API로 뉴스 400개 가량의 id 정보를 배열로 받는 Promise를 리턴한다.

    getNewsDetail은 위에서 받은 id 정보를 바탕으로 id에 해당하는 디테일한 뉴스 정보를 JSON 객체로 받는 Promise를 리턴한다.

     

     

    async function processRender () {
      try {
        const newsIds = (await getNewsIds()).slice(0, 20);
        const allNewsPromises = newsIds.map(id => getNewsDetail(id));
    
        return await Promise.all(allNewsPromises);
      } catch (error) {
        throw error;
      }
    }
    
    processRender()
    .then(response => createList(response));

     

    이제 processRender라는 이름의 async 함수를 선언하고

    news id 정보를 받아오는 작업을 완료한 후, 20개만 잘라서

    map을 이용하여 20개의 id를 인자로 하여 뉴스 정보를 반환하는 프로미스들을 배열로 만든다.

     

    그리고 Promise.all()을 이용하여 모든 요청을 동시다발적으로 보내고

    모든 요청에 대한 응답이 완료되면 해당 응답들을 배열로 다시 리턴한다.

     

    리턴받은 응답 resolve되어 then 구문으로 전달되고

    createList에 인자로 전달하여 실행한다.

     

     


    참고자료:

     

    Hacker News API:

    반응형

    COMMENT