프론트엔드/JavaScript

[JavaScript] 이벤트 루프 (Event Loop)

JavaScript는 싱글 스레드 언어로써 한 번에 하나의 작업만 처리할 수 있습니다.

하지만 실제 웹을 보면 여러 작업들이 동시에 처리되는 것을 볼 수 있습니다.

 

JavaScript는 어떻게 여러 작업을 처리할 수 있었을까요?

답은 이벤트 루프(Event Loop)에 있습니다.

 

이 글을 통해 이벤트 루프의 개념과 JavaScript의 비동기 처리 과정을 알아보겠습니다.

 


이벤트 루프 (Event Loop)

이벤트 루프 과정

 

JavaScript는 이벤트 루프를 통한 비동기 처리동시성(Concurrency)을 지원합니다.

 

다만 그림을 보면 알듯이 JavaScript 엔진 내부에서 비동기 요청을 처리하지는 않습니다.

엔진은 그저 호출 스택(Call Stack)에 들어오는 요청을 순차적으로 처리하는 역할만 합니다.

비동기 요청은 런타임 환경(브라우저, Node.js)에서 제공하는 Web API가 처리합니다.

 


 

🚀 이벤트 루프 과정

  1. 호출 스택이 비동기 요청을 만나면 비동기 작업의 정보콜백 함수를 함께 Web API에 전달합니다.
  2. Web API에서 비동기 작업을 처리하고, 작업이 완료되면 전달받았던 콜백 함수콜백 큐에 집어 넣습니다.
  3. 콜백 큐는 이벤트 루프 과정을 통해 호출 스택이 비었을 때마다 콜백 큐의 콜백 함수를 넘겨줍니다.
  4. 호출 스택에 쌓인 콜백 함수가 실행되면 호출 스택에서 제거됩니다.

 


 

🔎 콜백 함수 (Callback Function)

콜백 함수는 다른 함수에 파라미터로 넘겨지는 함수를 의미합니다.

넘겨진 함수는 필요에 따라 호출(Call back)되기 때문에 콜백 함수라 불립니다.

 

어떻게 함수를 다른 함수의 인자로 전달할 수 있을까요?

그것은 JavaScript의 함수가 일급 객체이기 때문입니다.

 


 

🔎 일급 객체 (First Class Object)

일급 객체란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 의미합니다.

아래와 같은 조건을 만족하면 일급 객체라고 할 수 있습니다.

  • 변수에 할당할 수 있다.
  • 매개 변수(parameter)로 전달할 수 있다.
  • 함수의 반환 값으로 사용될 수 있다.

JavaScript의 함수는 모든 조건을 만족하는 일급 객체입니다.

이러한 일급 객체의 특징을 활용해 고차 함수, 콜백 함수를 구현할 수 있습니다.

 


Web API

Web API는 비동기 작업을 처리하기 위해 브라우저가 제공하는 API를 의미합니다.

처리 가능한 비동기 작업들은 다음과 같습니다.

  • DOM (Document)
  • AJAX (XMLHttpRequest)
  • Timeout (setTimeout, setInterval..)

브라우저는 Web API의 비동기 작업을 JavaScript 엔진과 별개의 스레드에 위임합니다.

해당 스레드가 비동기 작업을 완료하면 함께 전달받은 콜백 함수를 콜백 큐에 넣습니다.

 


콜백 큐 (Callback Queue)

콜백 큐는 Web API에서 비동기 작업이 처리된 후의 콜백 함수가 보관되는 영역입니다.

또한 이벤트 루프 과정을 통해 콜백 함수들을 호출 스택으로 전달하기도 합니다.

 

콜백 큐 내부는 여러 개의 큐들로 구성되어 있습니다.

각각의 큐는 이벤트 루프가 실행되는 우선순위가 다릅니다.

  • 우선순위 : Microtask Queue > Animation Frames > Task Queue
  • (브라우저마다 다를 수 있음)

 


1. Task Queue (Macrotask Queue, Event Queue)

Task Queue에는 다음과 같은 Timer 관련 비동기 콜백 함수들이 저장됩니다.

이벤트 루프의 우선순위는 가장 낮습니다. 🥉

  • setTimeout()
  • setInterval()
  • setImmediate()

 


2. Microtask Queue (Job Queue)

Microtask Queue에는 다음과 같은 Promise 등의 비동기 콜백 함수들이 저장됩니다.

이벤트 루프의 우선순위는 가장 높습니다. 🥇

  • Promise
  • async / await
  • process.nextTick
  • Object.observe
  • MutationObserver

 


3. Animation Frames

Animation Frame에는 다음과 같은 애니메이션 관련 콜백 함수들이 저장됩니다.

이벤트 루프의 우선순위는 Microtask Queue와 Task Queue의 중간입니다. 🥈

  • requestAnimationFrame

 


🔎 Promise는 동기다.

공부하면서 인상 깊었던 것 중 하나는 Promise 자체는 동기라는 사실입니다.

.then, .catch, .finally를 통해 전달되는 콜백 함수가 비동기입니다.

이 콜백 함수들은 Web API에서 처리됩니다.

 


🎯 requestAnimationFrame vs setTimeout

[JSConf] Jake Archibald's presentation

  • T : task queue
  • rAF : requestAnimationFrame
  • S : Style (렌더 트리 생성)
  • L : Layout
  • P : Paint

 

애니메이션 작업에 있어 requestAnimationFrame와 setTimeout의 차이는 1 프레임 당 호출이 보장되는가입니다.

만약 애니메이션을 위해 setTimeout을 사용한다면 정확한 주기마다 프레임 호출을 보장할 수 없습니다.

이런 현상은 버벅거리는 애니메이션을 제공하여 사용자 경험을 떨어뜨릴 수 있습니다.

 

그림에서 볼 수 있듯이 setTimeoutTask Queue에 할당됩니다.

그래서 다른 태스크에 의해 호출이 지연되어 프레임 호출을 보장하기 어려울 수 있습니다.

 

반면 requestAnimationFrame렌더링 과정 직전에 위치합니다.

즉 렌더링 과정과 반드시 동반되기 때문에 1 프레임 당 호출을 보장할 수 있습니다.

 


Event Loop(이벤트 루프)

이벤트 루프는 호출 스택에 쌓여있는 함수가 있는지, 콜백 큐에 콜백 함수가 있는지를 반복하여 확인합니다.

만약 호출 스택이 비었다면 콜백 큐에서 대기하고 있던 콜백 함수를 호출 스택으로 넘깁니다.

 

스택으로 넘기는 순서는 콜백 큐의 우선순위를 따르게 됩니다.

 


비동기 예제

이제 다음 코드의 결과를 예측할 수 있습니다.

console.log("script start");

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

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

requestAnimationFrame(function() {
    console.log("requestAnimationFrame");
})
console.log("script end");

 

결과는 다음과 같습니다.

script start
script end
promise1
promise2
requestAnimationFrame
setTimeout