[번역] JavaScript 동작 원리 시각화 - 이벤트 루프, Web APIs, (Micro)task Queue

조하은·2026년 1월 18일
post-thumbnail

이 글은 Lydia Hallie님의 JavaScript Visualized:
Event Loop, Web APIs, (Micro)task Queue
를 한국어로 옮긴 내용입니다.
공부하면서 개인적으로 정리한 번역이라, 일부 내용에 의역 또는 오류가 있을 수 있습니다.

Event Loop는 복잡하다고 잘 알려진 주제입니다. 하지만 한 발 물러서서 보면, 실제로는 JavaScript 런타임 안의 아주 작은 구성 요소일 뿐입니다.

1

Event Loop는 비동기 작업을 처리할 때 아주 중요한 역할을 합니다. JavaScript는 싱글 스레드 언어이기 때문에 단 하나의 콜 스택(Call Stack) 만을 사용하는데, 이 점에서 이벤트 루프가 중요합니다.


Call Stack

Call Stack은 우리 프로그램의 실행을 관리합니다. 함수를 호출하면 새로운 실행 컨텍스트가 생성되고, 이것은 Call Stack에 push됩니다. 콜스택의 최상단에 있는 함수가 실행되며, 이 함수는 또 다른 함수를 호출할 수 있고, 이런 식으로 계속 이어집니다.

함수의 실행이 완료되면 실행 컨텍스트가 Call Stack에서 pop(제거)됩니다.

2

JavaScript는 한 번에 하나의 작업만 처리할 수 있습니다. 만약 어떤 작업이 pop되는 데 너무 오래 걸린다면, 다른 작업들은 처리될 수 없습니다.

이는 오래 걸리는 작업들은 다른 모든 작업의 실행을 차단(block)해버려, 프로그램이 사실상 멈춰버린다는 것을 의미합니다.

3

위의 사진에서 importantTasklongRunningTask의 실행이 끝나 Call Stack에서 제거될 때까지 기다려야 했는데, 함수 내부의 무거운 연산 때문에 시간이 꽤 걸렸습니다.


그런데 잠깐... 실제 어플리케이션을 만들다 보면 네트워크 요청, 타이머, 사용자 입력 기반 작업 등 오래 걸리는 작업들이 필요한 경우가 많습니다.

그렇다면 이런 오래 걸리는 작업을 사용할 때마다 애플리케이션 전체가 멈춰버리는 걸까요?

fetch("https://website.com/api/posts") 
// We don't know when the data gets returned from the server...
// Would this be on the call stack until the data returns?

다행히도, 그렇지 않습니다! 이런 기능은 사실 JavaScript 자체의 일부가 아니라, Web APIs를 통해 제공됩니다.


Web APIs

Web APIs는 브라우저가 제공하는 기능들과 상호작용할 수 있는 인터페이스 모음입니다. 우리가 JavaScript로 개발할 때 자주 사용하는 Document Object Model, fetch, setTimeout과 같은 기능 외에도 더 많은 기능들이 여기에 포함됩니다.

4

브라우저는 수많은 기능을 활용하는 강력한 플랫폼입니다. 그중 일부는 실용적인 애플리케이션을 만드는 데 필수적입니다. 예를 들어 콘텐츠를 화면에 렌더링하기 위한 렌더링 엔진(Rendering Engine)이나 네트워크 요청을 위한 네트워킹 스택(Networking Stack) 같은 것들이죠.

심지어 디바이스의 센서, 카메라, 위치 정보 등 저수준의 기능에도 접근할 수 있습니다.

5

Web APIs는 본질적으로 JavaScript 런타임과 브라우저의 기능 사이의 다리 역할을 하며, 우리가 JavaScript 자체의 기능을 넘어서는 정보에 접근하고 기능들을 사용할 수 있게 해줍니다.


좋네요. 근데 이게 논블로킹 작업과 무슨 관계가 있는 걸까요?
일부 Web APIs는 비동기 작업을 시작하고, 오래 걸리는 작업을 브라우저로 오프로드할 수 있게 해줍니다!

이런 API가 제공하는 메서드를 호출하는 것은 사실상 오래 걸리는 작업을 브라우저 환경으로 넘기고, 해당 작업이 완료되었을 때 이를 처리할 핸들러를 등록하는 과정입니다.

6

비동기 작업이 시작한 후에(결과를 기다리지 않고), 실행 컨텍스트는 Call Stack에서 빠르게 pop될 수 있습니다. 이게 논 블로킹입니다!

비동기 기능을 제공하는 Web APIs는 콜백 기반 또는 프로미스 기반 방식을 사용합니다.

7

첫 번째로 콜백 기반 방식을 살펴보겠습니다.


Callback 기반 API

Geolocation API를 예로 들어보겠습니다. 웹사이트에서 사용자의 위치 정보에 접근하고 싶다고 해봅시다.

이를 위해 우리는 getCurrentPosition메서드를 사용할 수 있는데, 이 메서드는 두 개의 callback 함수를 인자로 받습니다. 사용자의 위치를 성공적으로 받았을 때 사용되는 successCallback과, 문제가 발생했을 때를 위한 선택적 errorCallback입니다.

8

이 함수를 호출하면 새로 생성된 실행 컨텍스트가 Call Stack에 push됩니다. 이는 단순히 해당 콜백들을 Web API에 등록하는 과정일 뿐이며, Web API는 이 작업을 브라우저로 오프로드합니다. (즉, 실제 작업은 브라우저가 처리합니다)

그 다음 함수는 콜 스택에서 pop됩니다. 이제 그 다음 처리는 브라우저의 책임이 됩니다.

9

백그라운드에서 브라우저는 사용자에게 웹사이트의 위치 정보 접근을 허용할지 묻는 팝업을 띄웁니다.

우리는 사용자가 언제 이 팝업과 상호작용할지 알 수 없습니다. 다른 일에 정신이 팔렸을 수도 있고, 아니면 팝업이 뜬 걸 못 봤을 수도 있죠.

하지만 문제될 건 없습니다! 이 모든 일이 백그라운드에서 실행되고 있기 때문에, Call Stack은 다른 작업들을 받아서 실행할 수 있는 상태로 남아있습니다. 우리 웹사이트는 반응성을 유지하며 계속 상호작용할 수 있습니다.

10

마침내 사용자가 우리 웹사이트의 위치 정보 접근을 허용했습니다. 이제 API는 브라우저로부터 데이터를 받고, successCallback을 사용해 결과를 처리합니다.

11

그런데 successCallback은 콜 스택에 바로 push될 수 없습니다. 그렇게 하면 이미 실행 중인 작업이 중단되거나, 예측하기 어려운 동작이나 잠재적인 충돌로 이어질 수 있기 때문입니다.

JavaScript 엔진은 한 번에 하나의 작업만 처리할 수 있으며, 이를 통해 예측 가능하고 체계적인 실행 환경을 보장합니다.


Tast Queue

대신에 successCallback은 태스크 큐(Task Queue)에 추가 됩니다(이 때문에 태스크 큐는 콜백 큐(Callback Queue) 라고도 불립니다). Task Queue는 미래의 어느 시점에 실행되기를 기다리는 Web API 콜백들과 이벤트 핸들러를 보관하는 큐입니다.

12

좋습니다, 이제 successCallback이 태스트 큐에 있네요... 그런데 이게 언제 실행될까요?


Event Loop

드디어 Event Loop에 대해 알아볼 차례입니다! Event Loop의 역할은 Call Stack이 비어있는지 계속 확인하는 것입니다.

Call Stack이 비어있을 때마다(즉, 현재 실행중인 작업이 없는 경우) Event LoopTask Queue에서 가장 먼저 대기 중인 작업(콜백)을 가져와 Call Stack으로 옮기고, 콜백을 실행합니다.

13

Event LoopCall Stack이 비어있는지 끊임 없이 확인하고, 비어 있다면 Task Queue에서 가장 먼저 대기중인 작업을 찾아 실행을 위해 Call Stack으로 옮깁니다.


또 다른 대표적인 콜백 기반 Web API로는 setTimeout이 있습니다. setTimeout을 호출하면 함수 호출이 Call Stack에 push되는데, 이 함수는 지정된 지연 시간으로 타이머를 설정하는 역할만 수행합니다. 실제 타이머의 관리 자체는 브라우저가 백그라운드에서 처리합니다.

14

타이머가 만료되면, 그 타이머의 callbackTask Queue에 추가됩니다. 여기서 중요한 점은, 지연 시간은 callbackCall Stack이 아닌 Task Queue에 넣어지는 시점을 의미합니다.

이 말은 setTimeout에 전달한 지연 시간보다, 실제 실행까지 걸리는 시간은 더 길어질 수도 있다는 뜻입니다. 만약 Call Stack이 아직 다른 작업을 처리하느라 바쁜 경우, 콜백은 Task Queue에서 대기했다가 나중에 실행됩니다.


지금까지, 우리는 콜백 기반 API들이 어떻게 처리되는지 살펴봤습니다. 하지만 대부분의 최신 Web API는 프로미스 기반 방식을 사용하며, 눈치챘겠지만 이들은 동작 방식이 조금 다릅니다.

여기서부터는 Promise에 대한 기본 지식이 있다고 가정하겠습니다. 복습이 필요하시다면 제 Promise 영상을 참고해주세요!


Microtask Queue

대부분의 (최신) Web APIs는 프로미스를 반환하며, 콜백 대신 프로미스 핸들러 체이닝이나 await을 사용해서 반환된 데이터를 처리할 수 있게 해줍니다.

fetch("...")
  .then(res => ...)
  .catch(err => ...)

프로미스 핸들러에서 데이터를 처리하기 때문에, 우리는 Microtask Queue를 사용합니다.

Microtask Queue는 런타임 내의 또 다른 큐로, Task Queue보다 우선순위가 높은 큐입니다. 이 큐는 다음과 같은 작업들을 위해 특별히 사용됩니다.

  • Promise 핸들러 콜백 (then(callback), catch(callback), finally(callback))
  • await 뒤에 오는 async 함수 본문의 실행
  • MutationObserver 콜백
  • queueMicrotask콜백

Call Stack이 비면, Event LoopTask Queue로 넘어가기 전에 먼저 Microtask Queue의 모든 마이크로태스크를 먼저 처리합니다.

15

Task Queue에서 하나의 작업을 완료한 후 Call Stack이 비면, Event Loop는 다음 작업으로 넘어가기 전에 Microtask Queue의 모든 마이크로태스크를 처리하면서 사실상 "다시 시작"합니다.

이를 통해 방금 완료된 작업과 관련된 마이크로태스크들이 즉시 처리되어, 프로그램의 반응성과 일관성을 유지할 수 있습니다.

마이크로태스크는 또 다른 마이크로태스크를 스케줄링할 수도 있습니다! 따라서 무한 마이크로태스크 루프를 만들면, Task Queue를 무한정 지연시키고 프로그램의 나머지 부분을 멈추게 할 수도 있으므로 주의해야 합니다.
16
이런 시나리오는 Task Queue에서는 발생할 수 없습니다(!). Event LoopTask Queue의 작업들을 하나씩 처리한 다음, Microtask Queue를 확인하는 것으로 "다시 시작"합니다.


대표적인 프로미스 기반 API에는 fetch가 있습니다. fetch를 호출하면 실행 컨텍스트가 Call Stack에 추가됩니다.

fetch를 호출하면 메모리에 기본 상태가 "pending"Promise Object가 생성되고, 네트워크 요청을 시작한 후 fetch 함수 호출은 Call Stack에서 제거됩니다.

17

이제 엔진은 체이닝된 then 핸들러를 만나는데, 이는 PromiseFulfillReactions에 저장되는 PromiseReaction 레코드를 만듭니다.

18

다음으로 console.logCall Stack에 push되고, 콘솔에 End of script를 출력합니다. 이 때, 네트워크 요청은 아직 진행중입니다.

19

서버가 마침내 데이터를 반환하면 [[PromiseStatus]]"fulfilled"로 설정되고, [[PromiseResult]]Response 객체로 설정됩니다. Promiseresolve되면서 PromiseReactionMicrotask Queue에 push(추가)됩니다.

20

Call Stack이 비면, Event LoopMicrotask Queue 에서 Call Stack으로 핸들러 콜백을 옮겨 실행시키고, Response 객체를 출력한 뒤 결국 Call Stack에서 제거됩니다.


모든 Web APIs가 비동기로 처리될까요?

아닙니다. 비동기 작업을 시작하는 API들만 그렇습니다. 예를 들어 document.getElementById()localStorage.setItem() 같은 다른 메서드들은 동기적으로 처리됩니다.

0개의 댓글