Async / Await Under the Hood

2021. 6. 29. 10:49Frontend

 

 

Overview

자바스크립트 ES6부터 비동기 코드 처리를 위해 async / await 문법이 추가되었습니다. ES6(=ES2015) 문법 자체가 도입된 지 꽤 많은 시간이 지났기 때문에 최근의 웹 개발은 async / await 을 통해 대부분의 비동기 처리를 하는 것이 일반적입니다. 서버로 네트워크 요청을 보내고, 그 결과를 가지고 다시 서버에 요청을 보내는 등의 비동기 처리를 할 때, ES6이후의 문법에서는 async / await을 사용해 다음과 같이 처리할 수 있습니다.

 

 

async function getResult(id) {
  const result = await getSearchResult(id);
  const video = await getVideo(result);
  return video;
}

 

 

async / await 문법을 사용하면 비동기 코드의 처리를 동기 코드처럼 작성할 수 있도록 도와줌으로써, 가독성 높은 코드를 작성할 수 있도록 합니다. 하지만 async / await은 자바스크립트의 새로운 문법이 아니며, class 문법과 같이 기존의 자바스크립트 기능을 사용해서 만든 syntactic sugar입니다. 즉, babel 등의 트랜스파일러를 사용해서 async await 문법을 트랜스파일링 하면 내부적으로는 이를 조금 다르게 처리하고 있다는 뜻입니다. (아래에서 살펴보겠지만 Promise, Generator, IIFE를 사용해서 이를 구현합니다) 이번 포스팅에서는 async / await문법이 실제로는 어떤 식으로 동작하는지에 대해서 살펴보려고 합니다.

 

 

Promise

프로미스 또한 자바스크립트에서 비동기 처리를 위해 제공하는 ES6 객체입니다. 프로미스는 정의상 대기(pending), 이행(fulfilled), 거부(rejected)의 3가지 상태를 가지며, 아래와 같이 'then'이라는 내부 메서드를 필수로 포함하게 됩니다. 'then'메서드를 필수적으로 구현해야 한다는 점에서 Promise는 'Thenable' 객체라고도 불리며, 대기(pending) 상태에서 벗어나 이행되거나 거부되었을 때, 이 then(or catch) 메서드를 호출해서 async action을 수행하거나 error를 처리합니다. (공식적으로 MDN문서에서는 'thenable'이라는 표현을 사용하고 있지 않지만 then 메서드를 정의하는 객체 또는 함수를 Thenable 객체라고 일반적으로 표현하기 때문에 위와 같은 표현을 사용했습니다.)

 

 

Promise는 then이 구현된 객체일 뿐이다.

 

Promise workflow (MDN)

 

 

이처럼 Promise는 어떤 특별한 기능을 가진 마법같은 문법이 아닌, 그저 내부적으로 then, catch, finally 등의 메서드가 정의된 객체일 뿐입니다. 따라서 아래와 같이 then method가 구현된 객체를 사용해서 async / await method를 적용해도 제대로 동작하는 것을 확인할 수 있습니다.

export default function App() {
  const thenableObj = {
    then: (resolve, reject) => resolve({ success: true, status: 200 })
  };

  const onClick = async () => {
    const result = await thenableObj;
    console.log(result);
  };

  return (
    <div className="App">
      <button onClick={onClick}>Click me!</button>
    </div>
  );
}

 

 

Generator

제너레이터는 함수와 비슷하지만 기존의 함수와는 다르게 호출자와 제어권을 주고받을 수 있는 특별한 함수입니다. 제너레이터 또한 ES6에서 제공하는 문법이며, 이터레이터(iterator) 프로토콜을 구현한 Iterable 객체를 리턴합니다. 호출자는 .next()라는 메서드를 통해 파라미터를 넘기고, 제너레이터 함수에게 제어권을 넘겨 내부 코드를 실행시킬 수 있으며, 제너레이터 함수는 yield라는 명령어를 통해 제어권을 호출자에게 다시 돌려주고, 실행된 결과를 호출자에게 iterable객체의 형태로 전달할 수 있습니다.

 

 

 

호출자의 관점에서는 제너레이터 함수를 생성한 뒤에. next() 메서드를 이용해서 제너레이터 함수 내부의 이전 yield가 끝난 지점부터 다음 yield까지의 코드를 실행하고, yield를 통해 반환되는 값을 이터러블의 형태( { value, done } )로 받아옵니다. 이때 이. next() 메서드 안에는 파라미터를 넘길 수 있으며, 이 파라미터는 제너레이터가 내부 로직을 처리하는 데에 사용할 수 있습니다.

 

 

제너레이터 함수의 관점에서는 .next()를 통해 제어권을 넘겨받으면 yield가 나올 때까지 함수를 실행하며, 호출자에게 반환하고 싶은 결과를 yield를 통해 리턴할 수 있습니다. next()를 통해 파라미터가 넘어왔을 경우, 이 파라미터를 할당받아서 사용할 수 있습니다.

 

function* myGenerator() {
  console.log("myGenerator");
  const result1 = yield 1;
  console.log("result1: ", result1);
  const result2 = yield 2;
  console.log("result2: ", result2); 
  return;
}

const myGen = myGenerator();
myGen.next();                 // { value: 1, done: false }
myGen.next('first result')    // { value: 2, done: false }
myGen.next('second result');  // { value: undefined, done: true }

 

 

 

Async Await Under the Hood

비동기 함수를 동기 함수처럼 처리할 수 있도록 도와주는 async await는 내부적으로 위의 2가지 기능(Promise, Generator)를 사용하여 구현됩니다. 다음과 같은 간단한 예시를 통해 살펴보도록 하겠습니다.

 

async function getPropsForSearchResult(user_id) {
  const membership = await getMembership(user_id);
  const solution = await getSolution(membership);
  const video = await getVideo(solution);
  const recommendation = await getRecommendation(video);
  
  return {
    membership,
    solution,
    video,
    recommendation
  }
}

getPropsForSearchResult('12958713');

 

유저 아이디를 가지고 멤버쉽을 가져온 후에, 멤버십으로부터 이미 가지고 있는 해설을 가져오고, 이 해설로부터 동영상 풀이를 가져오고, 이 동영상 풀이와 유사한 다른 콘텐츠들을 가져오는 학습 솔루션 서버가 있다고 하면, 위와 같은 흐름으로 순차적인 비동기 처리를 통해 데이터를 가져올 수 있습니다.

 

 

하나의 비동기 함수의 결과가 다른 비동기 함수의 파라미터로 넘어가는 구조이기 때문에 비동기 함수의 처리는 순차적으로 되어야 하며, 이를 동기 함수를 작성하듯이 작성하기 위해서 async await 문법을 사용하였습니다. 이 코드의 실행 내용을 제어권의 관점에서 조금 풀어보자면, 다음과 같이 설명할 수 있습니다.

 

 

  1. getPropsForSearchResult가 호출된 시점에서 getMembership이 실행되면, 제어권은 getMembership으로 넘어가서, 값이 Resolve될때까지 기다린다.

  2. 값이 Resolve되면, 이 값을 리턴하고 제어권을 getPropsForSearchResult에게 돌려준다. 이때, 리턴된 값을 membership 변수에 저장한다.

  3. membership 값을 가지고 getSolution을 실행하면, 제어권은 다시 getSolution으로 넘어가고, 값이 Resolve 될 때까지 기다린다

  4. 나머지 함수들도 1~2와 같은 과정을 반복한다.

 

 

제어권을 기준으로 설명하니 async / await 문법에 제너레이터가 들어갈 여지가 조금 보이시나요? 제너레이터는 Caller, Callee사이의 제어권 교환 및 값의 전달을 가능하게 해 주고, 함수를 원하는 시점에서 중단시키고 다시 재개할 수 있다는 점에서 위의 로직을 수행하기에 적합한 조건을 갖추고 있습니다. 위의 getPropsForSearchResult 함수를 제너레이터로 풀어보면 아래와 같이 작성할 수 있게 됩니다.

 

/**
 * getMembership
 * getSolution
 * getVideo
 * getRecommendation
 */
 
function* getPropsForSearchResult() {
  const membership = yield getMembership();
  const solution = yield getSolution();
  const video = yield getVideo();
  const recommendation = yield getRecommendation();
  return {
    membership,
    solution,
    video,
    recommendation
  }
}

 

 

getPropsForSearchResult를 제너레이터 함수로 생성하고, 이를 사용하면 특정 함수(getMembership, getSolution)에서 동작을 마치고 제어권을 다시 넘겨주기 전까지 다음 함수로 넘어가지 못하게 할 수 있습니다. 즉 비동기 함수를 "동기 함수"처럼 작동할 수 있도록 제한을 걸어 줄 수 있다는 의미입니다.

 

 

이를 사용한 전체적인 흐름은 아래와 같이 정리할 수 있습니다. 제너레이터를 사용해서 비동기 코드를 동기 코드처럼 작성할 수 있게 된 것이며, 이것이 async / await 로직을 이루는 근간이라고 할 수 있습니다.

 

  1. getMembership함수를 호출하여 제어권을 넘겨줍니다

  2. getMembership함수는 실행이 완료되고 나서 다시 getPropsForSearchResult의 .next()를 호출해서 제어권을 돌려줍니다. (이때 next에는 getMembership함수의 호출 결과를 파라미터로 넘겨주게 됩니다) 

  3. getPropsForSearchResult는 다시 getSolution을 호출하여 제어권을 넘겨줍니다.

  4. getSolution함수는 실행이 완료되고 나면 .next()를 통해 제어권을 넘겨줍니다. (위 프로세스 반복)

 

 

비동기 함수가 호출이 완료되었을때 제어권을 넘겨주는 방식은 즉시 실행 함수(IIFE: Immediately Invoked Function Expression)와 Promise.then을 사용해서 구현할 수 있으며,  비동기 함수의 리턴 타입인 프라미스가 Resolve 되는 순간, 해당 Resolve 결과 값을 가지고 다시. next()를 호출함으로써 연속적으로 제어권을 이동시켜줍니다.

 

 

const iterator = getPropsForSearchResult();
let ret;

(function runNext(val) {
  ret = iterator.next(val);
  if (!ret.done) {
    ret.value.then(runNext);
  } else {
    console.log("DONE");
  }
})();

 

async await이 제너레이터를 사용한 'syntactic sugar'이며, 제너레이터는 일반적으로 non-blocking 함수이기 때문에 await이 동작하고 있는 중에도 유저 이벤트 처리 등의 작업을 자바스크립트가 여전히 처리할 수 있습니다. 이러한 사실들을 이해하기 위해서는 새롭게 도입되는 자바스크립트 문법이 실제로 어떤 식으로 동작하고 있는지를 이해해야 합니다.

 

 

Reference

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await

 

Making asynchronous programming easier with async and await - Learn web development | MDN

More recent additions to the JavaScript language are async functions and the await keyword, added in ECMAScript 2017. These features basically act as syntactic sugar on top of promises, making asynchronous code easier to write and to read afterwards. They

developer.mozilla.org

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator

 

Generator - JavaScript | MDN

The Generator object is returned by a generator function and it conforms to both the iterable protocol and the iterator protocol.

developer.mozilla.org

 

반응형

'Frontend' 카테고리의 다른 글

[React] Atomic Design Pattern에 대한 고찰  (0) 2021.09.18
React Deep Dive - React Event System (1)  (0) 2021.07.19
[Webpack] Code Splitting  (2) 2021.06.17
[Webpack] Plugins  (0) 2021.06.12
[Webpack] Loaders  (0) 2021.06.11