반응형

*틀린 부분 있으면 피드백 주시면 감사하겠습니다~

 

async/await를 쓸줄은 아는데 정확히 어떻게 동작하는지 애매하게 알고 있어서 작성해봅니다.

 

동작 원리

 

const a = () => {
  console.log("a 시작");
  b();
  console.log("a 끝");
};

const b = async () => {
  console.log("b 시작");
  await c();
  console.log("b 끝");
};

const c = async () => {
  console.log("c 시작");
  await d();
  console.log("c 끝");
};

const d = () => {
  console.log("d")
};

a();

출처 : https://velog.io/@jjunyjjuny/JavaScript-asyncawait%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C

 

이 코드를 이해하려면 일단 다음 부분에 대한 이해가 있어야한다.

 

- 자바스크립트의 비동기 처리방식

- 이벤트 루프와 테스크 큐, 마이크로 테스크 큐

- promise(ES6)와 async/await(ES8)

- async 함수는 항상 (명시적, 암묵적 방식으로) promise를 반환한다

 

자바스크립트는 비동기 함수를 처리하기 위해서 이벤트루프라는 방식을 사용하는데 이벤트 루프는 지금 콜스텍이 비었는지(실행하고 있는 task가 없는지)와 테스크 큐(들)에 지금 실행 가능한 task가 있는지를 반복적으로 확인한다. 그래서 비동기 함수는 큐에서 대기하다가 실행이 가능해지면 꺼내져와서 실행하는데, promise같은 경우에는 실행 우선 순위가 매우 높아서(마이크로 테스크큐) 실행 가능해지면 먼저 실행되게 된다.

 

async/await 함수의 동작 순서

 

1. await 키워드가 붙은 대상을 실행한다.

 

2. 1번 대상이 함수가 아니거나, 함수의 실행이 끝나면 '1번 대상을 실행한' async 함수를 일시정지하고 콜스텍에서 마이크로 테스크 큐에 옮긴다. 이때 await를 실행한 위치를 기억한다.

 

3. 지금 실행하고 있던 함수가 콜스텍에서 빠져나왔으니 다른 콜스텍을 실행한다. 

 

4. 콜스텍이 비었으면 2번에서 빠져 나왔던 async 함수를 다시 콜스택으로 옮겨 실행한다. 이때 await가 실행된 위치 다음부터 실행된다. 

 

5. 함수가 끝날때까지 반복...!

 

 

그래서 아까 문제를 다시 풀어보자면

 

1. a를 실행하고 콜스텍에 쌓인다 [a]

 

2. a 시작 출력 

 

3. b를 실행하고 콜스텍에 쌓인다 [a, b]

 

4. b 시작 출력

 

5. await keyword를 만났고 함수이므로 C를 실행하고 콜스텍에 쌓인다.  [a, b, c]

 

6. C 시작 출력

 

7.  await keyword를 만났고 함수이므로 D를 실행하고 콜스텍에 쌓인다.  [a, b, c, d]

 

8. D 출력

및 종료하고 콜스텍에서 빠져나온다. [a, b, c]

 

9. 7번에서 D를 시작했고 실행이 끝났으니 C는 마이크로 테스크큐에 들어가게 된다.  [a, b],  micro = [c]

 

10. 5번에서 C를 시작했고 9번에서 C가 일시정지 되었으므로 B도 마이크로 테스크 큐에 들어가게 된다.  [a],  micro = [c, b]

 

11. 콜스텍에서 B가 빠져나갔으므로 a가 이후 흐름을 실행한다.

a 끝 출력

a가 끝나서 콜스텍에서 빠져나온다. [], micro = [c, b]

 

12. 콜스텍에 비었으므로 이벤트루프가 마이크로 테스트큐에 있는 함수를 실행한다.

 

13. C 끝 출력 및 C가 콜스텍에서 빠져나온다. [], micro = [b]

 

14. B 끝 출력 및 b가 콜스텍에서 빠져나온다. [], micro = []

 

답 : 

a 시작
b 시작
c 시작
d
a 끝
c 끝
b 끝

 

결론 : 

async/await를 쓰면 가독성이 좋은 like-동기 방식으로 실행하는거처럼 보이지만 사실은 엄청 복잡한 비동기 방식으로 실행된다는것을 알게 되었다.  

 

 내부적 원리 요약

 

그래서 마이크로 테스크큐에 넣고 다시 콜스텍에서 실행될때 돌아올 위치를 어떻게 결정하는데? 의문이 떠올랐는데 Generator와 Promise의 원리를 섞어서 처리했다고 한다.

 

참고한 글) https://medium.com/sjk5766/async-await-%EC%9B%90%EB%A6%AC-cc643f18526d

async function fun() {
  await getDrink();
  await haveMeal();
  await drinkSoju();
}

예를 들어 위와 같은 코드가 있다면

 

function* fun() {
  yield getDrink(); // 1
  yield haveMeal(); // 2
  yield drinkSoju();// 3
}

위와 같은 함수로 변경되고 

yield를 만나 실행되고

내부에서 Promise 실행을 한 뒤 

Promise가 끝난다면 내부에서 resolve를 실행하고

Generator의 next()(step 함수 자체구현)를 호출해서 체인을 걸어주는식으로 동작한다고 한다. 

 

그런데 3년 전 글이라 실제로 바벨 홈페이지에 들어가서 돌려보니 (개념상으로는 비슷한데) 약간 달라진점이 있었다.

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);
    });
  };
}

바벨을 돌려봤더니 위 코드를 보면 

 

step() 부분이 변경되어서

 

asyncGeneratorStep 안에서 만약 Generator가 끝난다면 (예를들어 코드 맨끝줄 상황) 그대로 resolve하는것은 동일한데 

아니라면 Promise의 then (Promise.resolve(value).then(_next, _throw);)을 사용해 첫번째 await 함수까지 실행하고 다음 체인은 마이크로 테스크큐에 넣어주는것으로 구현한것으로 보인다.

 

직접 해보고싶은 분들은 아래 주소로..?

 

https://babeljs.io/repl#?browsers=&build=&builtIns=false&corejs=3.6&spec=false&loose=false&code_lz=FAMwrgdgxgLglgewgAhgQwM4GsCMAKASgG9gBIKJDBAGwFMA6ahAczwHIZbsc2CBuZMmABfYKEixEKdNgBMhZCUEAnWjDDKUEWgHdkABWUIAtnAy08eVRgLIAvAD5ggl67fvkCx8nMwAKnDGtAhgMFZceBQQVHSMLOwyWLK8BAA0yDgADNkE_CJiwJgAntDI4NDwSMgARgpKLlExDEysbNU8ea5oOmhwMKiYuIR8zg2UNM3xbcmdyKTdvf2J8rPk47Et7NUAzLwjosCNE3GtGOjKMHvAtQSH65OttBAAJgD8V8BAA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env&prettier=false&targets=&version=7.17.8&externalPlugins=&assumptions=%7B%7D 

 

Babel · The compiler for next generation JavaScript

The compiler for next generation JavaScript

babeljs.io

 

 

P.S) 바벨로 트렌스파일 말고 진짜 Generator로 변환하는가 찾아봤는데 V8 주석의 경우 변환한다고 적혀있긴 하다..

 

// ES#abstract-ops-async-function-await
// AsyncFunctionAwait ( value )
// Shared logic for the core of await. The parser desugars
//   await value
// into
//   yield AsyncFunctionAwait{Caught,Uncaught}(.generator_object, value)
// The 'value' parameter is the value; the .generator_object stands in
// for the asyncContext.

주석 발췌 from  https://github.com/v8/v8/blob/17a99fec258bcc07ea9fc5e4fabcce259751db03/src/builtins/builtins-async-function-gen.cc#L247-L254

 

reference

 

https://velog.io/@jjunyjjuny/JavaScript-asyncawait%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C

 

[ JavaScript ] async/await는 어떻게 동작할까

await가 만드는 동기처럼 보이는 비동기 코드

velog.io

 

https://medium.com/sjk5766/async-await-%EC%9B%90%EB%A6%AC-cc643f18526d

 

async-await 원리

async-await 을 처음 봤을 때, 기존 call-back 구조와 비교해 소스를 가독성 좋고 간단하게 짤 수 있구나에 감탄했고 어떻게 이렇게 될까 라는 생각이 들었습니다. async-await은 내부적으로 Generator와 Promi

medium.com

 

 

반응형

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

commonjs와 ESM의 차이  (0) 2023.07.25
pnpm 도입기 (feat: 모노 레포)  (0) 2023.07.15
JS로 Single Page Application 구현  (0) 2022.03.12
Generator  (0) 2022.02.02
호이스팅과 var, let에 대하여  (0) 2022.01.29

+ Recent posts