본문 바로가기
📌 Front End/└ JavaScript

[JavaScript] 자바스크립트 비동기에 대해서 이해하기

by 쫄리_ 2023. 3. 10.
728x90
반응형

자바스크립트는 싱글쓰레드기반의 언어인데 어떻게 비동기로 동작을 할까?
먼저 아래 영상을 보면 이해가 쉬울 것이다.

https://youtu.be/8aGhZQkoFbQ

[ 한국어 자막 ON. ]


사실 자바스크립트가 비동기로 동작하는 이유는 브라우저에 있다.
브라우저는 Web API, Callback Queue, Event Loop 등으로 구성되어있고 자바스크립트 코드가 실행될 때 브라우저와의 동작은 아래 그림으로 표현할 수 있다.


자바스크립트 엔진의 구성요소

브라우저의 자바스크립트 엔진은 Memory Heap과 Call Stack으로 구성되어있다.
자바스크립트는 싱글쓰레드기반의 언어기 때문에 한 번에 하나의 일만 처리할 수 있다. 즉, 선입후출(LIFO, Last In First Out)방식이다.


Runtime Environment

Web APIs

WebAPIs는 브라우저에 제공하는 API로 DOM(document), AJAX(XMLHttpRequest), Timeout(setTimeout) 등이 있다.

Callback Queue, Event Loop

Callback Queue 동기적으로 실행된 콜백함수가 보관 되는 영역이다.
Callback Queue는 선입선출 FIFO방식이다.

Event Loop는 Call Stack과 Callback Queue의 상태를 체크하여,
Call Stack이 빈 상태가 되면, Callback Queue의 첫번째 콜백을 Call Stack으로 밀어넣는다.


Single Thread

싱글쓰레드는 하나의 함수가 실행되면 이 함수의 실행이 끝날 때까지 다른 작업이 중간에 끼어들지 못한다.
함수가 실행되면 해당 함수는 Call Stack의 가장 상단에 위치하고, 함수의 실행이 끝날 때 해당 함수는 Call Stack에서 제거된다.
Single Thread의 단점은 브라우저에서 호출 스택에 실행할 함수가 쌓여있는 동안은 다른 일을 할 수 없다. 이 상태를 blocked라고 한다. 이 상태에서 브라우저는 렌더링을 할 수도 없고, 다른 코드를 실행할 수도 없다. (브라우저의 alert 창이 떴을 때 blocked된 상태이다.)

//재귀함수로 무한루프에 빠지게되면 최대로 쌓을 수 있는 Stack의 갯수를 넘게된다.
var count = 0;
function stack() {
  console.log(++count);
  stack();
}
stack();

-Stack Overflow-
Stack의 최대 크기는 브라우저에 따라 다르고 버전에 따라 다르다고한다.[1]


Task Queue vs Microtask Queue vs Animation Frames

사실 모든 비동기 동작이 Task Queue에 쌓이는 것은 아니고, 실제로는 여러 Queue가 존재한다.

ES6에 들어오면서 새로운 컨셉인 Microtask Queue가 도입됐다고한다. Microtask Queue는 Task Queue와 동일한 계층에 존재하고 프로미스의 비동기 호출 시 Microtask Queue에 쌓이게 된다.

 

Microtask Queue vs Task Queue

실행 결과를 생각해 보자.

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

Promise.resolve()
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });

console.log("script end");

만약 모든 비동기 호출이 단일 task queue에 의해 관리되고, Event Loop는 호출 스택이 비었을 때 task queue에서 순서대로 꺼낸다면 아래와 같은 결과일 것이다.

script start
script end
setTimeout
promise1
promise2

그러나 실제로는 아래와 같이 출력된다.

script start
script end
promise1
promise2
setTimeout

브라우저의 이벤트 루프가 task와 microtask를 어떻게 다루는지 알아보자.

  • 브라우저의 이벤트 루프 우선순위
  • 이벤트 루프는 실행 순서를 보장하는 여러 queue에서 어떤 task를 꺼내서 실행시킬지 결정한다.
  • 이를 통해 브라우저는 우선순위가 높은 task를 먼저 실행하도록 할 수 있다.
  • microtask는 일반 task보다 높은 우선순위를 가지고 있다.
//1. script 실행 (log)
console.log("script start");

//2. script 실행 (setTimeout callback task queue에 등록)
setTimeout(function () {
  //9. Task 실행
  console.log("setTimeout");
}, 0);

//3. script 실행 (Promise then callback Microtask queue에 등록)
Promise.resolve()
  .then(function () {
    // 6. MicroTask 실행
    console.log("promise1");
  }) // 7. script 실행 (Promise then callback Microtask queue에 등록)
  .then(function () {
    // 8. MicroTask 실행
    console.log("promise2");
  });

//4. script 실행 (log)
console.log("script end");
//5. Stack의 모든 Task 실행완료

 

Microtask Queue vs Task Queue vs Animation Frames

Microtask 외에도 Queue는 또 있다. 바로 requestAnimationFrame에 의해 등록되는 Animation Frames이다.

예제를 일부만 수정해서 Animation Frame을 추가해보자.

//1. script 실행 (log)
console.log("script start");

//2. script 실행 (setTimeout callback task queue에 등록)
setTimeout(function () {
  //11. Task 실행
  console.log("setTimeout");
}, 0);

//3. script 실행 (Promise then callback Microtask queue에 등록)
Promise.resolve()
  .then(function () {
    // 7. MicroTask 실행
    console.log("promise1");
  }) // 8. script 실행 (Promise then callback Microtask queue에 등록)
  .then(function () {
    // 9. MicroTask 실행
    console.log("promise2");
  });

//4. script 실행 (AnimationFrame Animation frames에 등록)
requestAnimationFrame(function () {
  //10. Animation Frame 실행
  console.log("animation");
});

//5. script 실행
console.log("script end");
//6. Stack의 모든 Task 실행완료

결과는 다음과 같다.

script start
script end
promise1
promise2
animation
setTimeout
  • 이벤트 루프의 우선순위
  • Call Stack의 작업을 처리한다.
  • Call Stack이 비어있다면 microtask queue를 확인하고 작업이 있다면 microtask queue의 task를 작업을 Call Stack으로 넣고 실행한다.
  • 만약 microtask가 비어있다면 Animation Frames를 확인하고 브라우저 렌더링이 발생한다.
  • 1, 2, 3번 작업이 완료되었다면 task queue를 확인하고 작업이 있다면 task queue의 작업을 Call Stack으로 넣고 실행한다.

이러한 동작들은 브라우저마다 호출 순서가 다를 수 있다. promise가 ECMA Spec이므로 브라우저마다 처리하는 방식이 다르기 때문이다. 특정 브라우저에서는 promise를 microtask로 처리하는 것이 아니라 task로 처리하는 경우도 있다.(이 글은 크롬 기준이다.)
자세한 비교를 보려면 아래 링크를 참고하자.

Tasks, microtasks, queues and schedules

 

Reference

어쨌든 이벤트 루프는 무엇입니까? | Philip Roberts | JSConf EU
Tasks, microtasks, queues and schedules
https://stackoverflow.com/questions/7826992/browser-javascript-stack-size-limit
https://iamsjy17.github.io/javascript/2019/07/20/how-to-works-js.html

 

 
728x90
반응형