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

DD·2021년 8월 15일
16

JavaScript

목록 보기
1/5
post-thumbnail

이 글은 기초적인 이벤트루프의 동작 방식과 Promise의 동작 방식 이해하고 있는 독자를 대상으로 합니다. 혹시 이벤트 루프 동작 방식을 모르신다면 이 글을 먼저 읽어보기실 권해드려요.

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

위 코드를 읽고 console이 어떻게 찍힐지 예상할 수 없거나, 예상할 수 있더라도 왜 그렇게 동작하는지 내부적인 동작 원리를 설명할 수 없다면 이 글을 읽어보시길 바랍니다.

이 글에서 다룰 기본 개념 요약

  • 태스크 큐는 마이크로 / 매크로로 나뉘며, 마이크로 태스크 큐가 매크로 태스크 큐보다 우선 순위가 높다

    • 마이크로 태스크는 Promise callback, Process.nextTick (Nodejs), queueMicrotask,

    • 매크로 태스크는 setTimeout, setInterval, setImmediate의 콜백이다.

  • 이벤트 루프는 콜스택이 비워지면 태스크 큐의 작업을 콜 스택으로 옮긴다.

    • 이 때, 마이크로 태스크를 우선 처리한다
    • 콜 스택과 마이크로 태스크 큐가 비었을 때, 이벤트 루프가 매크로 태스크를 하나씩 콜스택으로 옮겨 처리하며, 처리할 때 마다 마이크로 태스크 큐에 작업이 존재하는지 지속적으로 확인한다.
  • async 함수는 항상 promise 객체를 반환하며, 개발자가 명시하지 않더라도 반환값을 자동으로 Promise로 래핑한다

자바스크립트 엔진이 await를 만나면

  • await의 결과를 기다리며 코드 동작을 일시정지한다-고 대답하면 안 된다.

  • 자바스크립트 엔진이 await를 만나면 일어나는 일은 구체적으로 다음과 같다.

    1. await 키워드가 붙은 대상이 함수라면 해당 함수를 실행한다.

    2. 함수가 아니거나/함수의 실행이 끝나면 해당 async 함수를 일시정지하고 콜스택에서 마이크로 태스크 큐로 옮긴다

    3. 이 때 await의 위치를 기억한다.

    4. 해당 함수가 콜스택에서 빠져 나왔으니 나머지 콜스택이 실행된다.

    5. 콜스택이 모두 비워지면 이벤트 루프는 마이크로 태스크 큐에 있는 await 키워드를 만나 옮겨졌던 async 함수를 다시 콜스택으로 옮긴다

    6. 해당 함수가 await 됐던 시점부터 다시 실행된다.

  • 여기서 중요한건 해당 함수가 콜스택에서 빠져나와 마이크로 태스크 큐로 옮겨진다는 것이다. 일시정지된 상태에서!

  • 이게 await의 결과를 기다리며 코드 동작을 일시정지한다는 말과 같은 이야기 같은데, 왜 틀렸다고 하는 걸까

await는 async 함수만 일시정지시킨다

  • await의 역할은 자신이 포함된 async 함수만 일시정지 시키는 것이다. 이제 맨 처음 언급한 코드를 토대로 이게 무슨 소리인지 알아보자
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();
  • a 함수가 호출/실행되어 콜스택에 쌓이고 a 시작이 콘솔에 찍힌다.

  • b 함수가 호출/실행되어 콜스택에 쌓이고b 시작이 콘솔에 찍힌다.

  • b 함수 내에 await를 만났으나, 먼저 c함수를 호출/실행한다.

  • c 함수가 호출/실행되어 콜스택에 쌓이고c 시작이 콘솔에 찍힌다.

  • c 함수 내에 await를 만났으나, 먼저 d함수를 호출/실행한다.

  • d 함수가 호출/실행되어 콜스택에 쌓이고 d가 콘솔에 찍힌다.

  • d 함수가 종료되어 콜스택을 빠져나간다.

  • c 함수 내의 await를 만나게 되어, c 함수가 일시정지 되며 콜스택을 빠져나가 마이크로 태스크 큐에 들어간다

  • 콜스택에서 c 함수가 빠져나갔기 때문에 b 함수가 마저 실행됨과 동시에 await를 만나게 된다.

  • b 함수가 일시정지 되며 콜스택을 빠져나가 마이크로 태스크 큐에 들어간다

  • 콜스택에서 b 함수가 빠져나갔기 때문에 a함수가 마저 실행되며 a 끝이 콘솔에 찍히며 a 함수가 콜스택을 빠져나간다.

  • 콜스택이 모두 비워졌기에 이벤트 루프는 마이크로 태스크 큐에 첫 번째 작업인 c 함수를 콜 스택으로 옮긴다.

  • c 함수가 일시 정지된 await 이후 (할당문이라면 할당이 이루어진다)부터 실행되며 콘솔에 c 끝이 찍히고 콜스택이 비워진다.

  • 콜스택이 모두 비워졌기에 이벤트 루프는 마이크로 태스크 큐에 첫 번째 작업인 b 함수를 콜 스택으로 옮긴다.

  • b 함수가 일시 정지된 await 이후부터 실행되며 콘솔에 b 끝이 찍히고 콜스택이 비워진다.

결과적으로 이 코드의 결과는 아래와 같다

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

결론

  • async / await 가 비동기 코드를 동기처럼 보여주는 원리는 해당 함수를 마이크로 태스크 큐에 옮겨놓고 콜스택이 비워졌을 때 실행하기 때문이다.

  • 위의 예시 처러 await는 해당 함수만 일시정지 하는 것 처럼 보이게 하기 때문에 바깥 로직의 흐름을 막지 않는다. 따라서 a 끝이 먼저 콘솔에 찍히는 것이다.

  • 그렇다면 어떻게 이후 로직에서 비동기 결과값을 받을 수 있냐하면, promise를 사용하기 때문이다.

profile
기억보단 기록을 / TIL 전용 => https://velog.io/@jjuny546

2개의 댓글

comment-user-thumbnail
2021년 12월 24일

맨 처음 참조된 링크를 열면 다시 이 글로 돌아오네요

답글 달기
comment-user-thumbnail
2023년 11월 1일

진짜 좋은글같습니다. 확 와닿네요

답글 달기