async await 동작원리

Kyle·2022년 1월 6일
10

javascript

목록 보기
18/18

async 와 await는 어떻게 동작할까?

내부에서 어떤 식으로 동작하는지를 파악하기 위해서 babel의 try it out에서 트랜스파일링을 해보고 파악해보자.

아래 코드를 갖고 테스트할 예정이고 비동기 함수는 사용하지 않았다.

async, await는 비동기함수만 사용하는거 아니야? 라고 생각할 수 있지만 그렇지 않다. 어떤 함수에도 await을 적용할 수 있다.

먼저 아래의 코드의 결과값을 예상해보자.

function a() {console.log('a')}

async function b() {
    console.log('b1')
    await a()
    console.log('b2')
}
b()
console.log('c')

사실 위의 결과값을 예상할 수 있으면 아래 글을 볼 필요가 없다.

내부원리가 궁금한 분들만 보면 될 것 같다.

위의 코드를 바벨로 트랜스파일링 한다면

위의 코드를 바벨에서 돌려보면 아래와 같은 코드가 나온다.

끔찍하다.

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
      }
      _next(undefined);
    });
  };
}

function a() {
  console.log('a');
}

function b() {
  return _b.apply(this, arguments);
}

function _b() {
  _b = _asyncToGenerator(function* () {
    console.log('b1');
    yield a();
    console.log('b2');
  });
  return _b.apply(this, arguments);
}

b();
console.log('c');

좀 더 간단히

우리에게 error를 처리하는 부분까지는 볼 여유가 없다. 코드를 조금 더 간단하게 바꾼 뒤에 살펴보자.

async였던 b함수가 b와 _b로 나뉘었으며 await였던 부분제네레이터함수로 변경되면서 내부에서 yield로 변경된 것을 알 수 있다. 이것만 인지하고 아래로 내려가서 코드를 확인해보자.

function asyncGeneratorStep(gen, resolve, _next, key, arg) {
  /**
   * 제네레이터 객체의 next 메소드를 호출한다.
   */
  const genValue = gen[key](arg);

  /**
   * generator가 done이 됐으면 resolve한다.
   * 아니면 뒤의 작업은 then으로 이어서 한다. => 여기서 마이크로 테스크 큐로 들어가기 때문에 순서의 차이가 발생한다.
   */
  if (genValue.done) {
    resolve(genValue.value);
  } else {
    Promise.resolve(genValue.value).then(_next);
  }
}

function _asyncToGenerator(fn) {
  return function () {
    const self = this;
    const args = arguments;

    return new Promise((resolve) => {
      // gen: 제네레이터 객체
      const gen = fn.apply(self, args);
      const _next = (value) => {
        asyncGeneratorStep(gen, resolve, _next, 'next', value);
      };

      // 첫번째 await까지 실행한다.
      _next(undefined);
    });
  };
}

function a() {
  console.log('a');
}

function b() {
  return _b.apply(this, arguments);
}

function _b() {
  function* asyncFunction() {
    console.log('b1');
    yield a();
    console.log('b2');
  }

  _b = _asyncToGenerator(asyncFunction);
  return _b.apply(this, arguments);
}

b();
console.log('c');

코드 알아보기

위의 코드에서 _asyncToGeneratorasyncGeneratorStep를 알아보자.

_asyncToGenerator 에서 asyncGeneratorStep 를 내부적으로 사용하고있다.

_asyncToGenerator

function _asyncToGenerator(fn) {
  return function () {
    const self = this;
    const args = arguments;

    return new Promise((resolve) => {
      const gen = fn.apply(self, args);
      const _next = (value) => {
        asyncGeneratorStep(gen, resolve, _next, 'next', value);
      };

      _next(undefined);
    });
  };
}
  • 함수이름이 asyncToGenerator인 만큼 제너레이트함수로 변경된 async함수를 generator 객체로 변경시키고 실행하는 함수이다. 인자로 generator함수를 받게 된다.
  • Promise객체를 반환하는 함수를 반환하는 함수이다.

어떤 Promise객체를 반환하는 것일까?

new Promise((resolve) => {
      // gen: 제네레이터 객체
      const gen = fn.apply(self, args);
      const _next = (value) => {
        asyncGeneratorStep(gen, resolve, _next, 'next', value);
      };

      // 첫번째 yield(await)까지 실행한다.
      _next(undefined);
    });
  • 인자로 받은 제너레이터 함수를 가지고 제네레이터 객체를 만든다. (gen)
  • _next함수를 실행한다. === asyncGeneratorStep함수를 실행한다. (generator 객체를 내부에서 next해주는 함수이다)

결국 _asyncToGenerator함수는 인자로 받은 제너레이터함수를 한번 next해준다고 생각하면 된다. 즉, 첫번째 await까지 실행한다고 보면된다.

_next함수인 asyncGeneratorStep을 알아보자.

asyncGeneratorStep

인자부터 하나씩 알아보자.

  • gen: 제너레이터 객체
  • resolve: promise의 resolve함수
  • _next: _asyncToGenerator의 _next함수 (재귀적으로 사용된다.)
  • key, arg: 제너레이터의 next또는 throw를 실행시켜주기 위한 key와 인자값인 arg (예제에서는 throw하는 경우는 없다고 가정하고 진행한다. key===’next’, arg===undefined 라고 생각하자.)
  1. 맨 처음 _asyncToGenerator에서 실행된 asyncGeneratorStep에서는 genValue에서 gen(제너레이터 객체)의 next 메소드를 호출한다. (이때 첫번째 await까지 실행한다.)
  2. 이제 genValue가 끝났는지 genValue.done 으로 확인 후 끝났으면 resolve를 끝나지 않았으면 뒤의 작업을 then에 넘겨서 실행한다. (즉, 첫번째 await 이후의 작업은 then에 들어가서 실행되게 된다.)
  3. 위 작업을 반복한다.
function asyncGeneratorStep(gen, resolve, _next, key, arg) {
  /**
   * 제네레이터 객체의 next 메소드를 호출한다.
   */
  const genValue = gen[key](arg);

  /**
   * generator가 done이 됐으면 resolve한다.
   * 아니면 뒤의 작업은 then으로 이어서 한다. => 여기서 마이크로 테스크 큐로 들어가기 때문에 순서의 차이가 발생한다.
   */
  if (genValue.done) {
    resolve(genValue.value);
  } else {
    Promise.resolve(genValue.value).then(_next);
  }
}

asysn await의 원리

async, awai의 원리는 여기에 숨겨있었다.

async 함수는 제너레이터 함수로 변경이되어서 실행된다.

async함수에서 변경된 제너레이터 함수는 내부적으로 yield 될 때 그 뒤의 작업은 then에게 넘겨준다.

이때 뒤의 작업은 마이크로 태스크큐에 들어가게 되고, 콜스택이 비워진 다음에 실행되기 때문에 맨 위 예제와 같은 현상이 발생하는 것이다.

결론!

  • async, await 함수 ⇒ 제너레이터 함수로 변경된다.
  • 제너레이터 함수가 실행될 때 yield를 기준으로 promise에서 then메소드로 넘겨가면서 실행한다.
  • 이때 then메소드로 넘어간 뒷부분이 마이크로태스크큐로 들어가게 되면서 콜스택이 비워진 다음에 이어서 실행된다.

마무리

_asyncToGenerator, asyncGeneratorStep 함수의 원리를 이해하고 처음 babel에서 transfile한 코드를 보게 되면 이해가 될 것이다.

결국, await의 비밀을 제너레이터와 프로미스에 있었다.

이러한 현상은 리액트에서도 예상하지 못한 결과를 가져오게 되는데 이는 다음 포스팅에서 알아보도록하자.

ref

profile
Kyle 발전기

3개의 댓글

comment-user-thumbnail
2022년 4월 22일

이해가 안가요

1개의 답글
comment-user-thumbnail
2023년 1월 28일

안녕하세요! 글 잘 읽고갑니다 ㅎㅎ
마무리에 이러한 현상은 리액트에서도 예상하지 못한 결과를 가져오게 되는데..라고 쓰셨는데 어떤 부분인지 알 수 있을까요?

답글 달기