이벤트 루프: JavaScript가 비동기 코드를 실행하는 방법

발행: (2026년 5월 23일 PM 12:23 GMT+9)
8 분 소요
원문: Dev.to

Source: Dev.to

JavaScript는 single‑thread 언어입니다. 즉 실행 스레드가 하나뿐이기 때문에 비동기 작업을 조정하기 위해 Event Loop 라는 메커니즘에 의존합니다. 일부 비동기 작업은 Web API와 같은 외부 리소스에 의해 병렬로 수행되고, 다른 작업은 현재 동기 코드가 끝날 때까지 미뤄집니다.

Event Loop이 어떻게 동작하는지 이해하려면 JavaScript가 사용하는 다음 구조들을 간단히 살펴보겠습니다.

  • Call Stack
  • Web APIs
  • Task queue
  • Microtask queue
  • Event loop

Call Stack

메모리에서 LIFO(Last In, First Out) 원칙을 따르는 자료구조로, 코드 실행을 조직하고 처리하는 데 사용됩니다. firstFn()처럼 함수를 호출하면 해당 호출이 Call Stack에 쌓입니다. firstFnsecondFn을 호출하면 secondFnfirstFn 위에 쌓이고, 가장 위에 있는 함수가 먼저 실행·제거됩니다.

또한 함수 안에 선언되지 않은 전역 코드도 Call Stack에 쌓입니다. JavaScript 파일이 읽히면 엔진은 Global Execution Context(전역 실행 컨텍스트)를 생성하고, 이것이 Call Stack의 가장 아래에 먼저 쌓입니다.

Web APIs

JavaScript는 데이터와 로직만 처리할 수 있는 프로그래밍 언어이며, 마우스 클릭, 네트워크, 타이머 등 실제 환경과의 상호작용은 브라우저가 담당합니다. 브라우저가 제공하는 기능 집합이 바로 Web APIs이며, 이를 통해 JS 코드는 외부 리소스를 사용할 수 있습니다. Web API를 통해 실행되는 모든 작업은 JavaScript 스레드와 별도의 스레드에서 수행됩니다.

Web API는 브라우저 환경에서만 존재합니다. Node.js에서는 동일한 역할을 libuv가 담당합니다. libuv는 C로 구현된 라이브러리로 비동기 입출력을 가능하게 합니다.

Node.js는 Web API와는 별도로 nextTick Queue를 가지고 있습니다. 이는 Microtask Queue보다도 높은 우선순위를 갖는 전용 큐이며, Node 런타임 자체가 관리합니다.

Task Queue (Callback Queue, Macrotask Queue)

FIFO(First In, First Out) 원칙을 따르는 큐로, Web API가 외부에서 처리한 결과의 콜백을 받아 저장합니다. 브라우저의 Web API가 작업을 마치면 콜백이 Task Queue에 들어가고, Event Loop가 Call Stack이 비었을 때 이 콜백을 꺼내 실행합니다.

대표적인 Web API

  • fetch()
  • setTimeout()
  • setInterval()
  • addEventListener()

Microtask Queue (Job Queue)

Callback Queue와 구조는 동일하지만 사용 방식과 Event Loop에서의 처리 순서가 다릅니다.

  • Microtask는 JavaScript 엔진 내부에서 이미 결과가 준비된 콜백을 받습니다. 외부 작업이 필요 없으며, 현재 동기 코드가 끝난 뒤 바로 실행됩니다.
  • 이 큐는 Callback Queue보다 높은 우선순위를 가집니다. Event Loop는 Callback Queue에서 아이템을 꺼내기 전에 Microtask Queue를 완전히 비워야 합니다. Callback Queue에서 아이템을 Call Stack으로 옮긴 뒤에도 다시 Microtask Queue를 먼저 확인합니다.

Microtask Queue는 Job Queue라고도 불립니다.

Microtask Queue에 넣을 수 있는 비동기 처리 방법

  • Promise.then()
  • Promise.catch()
  • Promise.finally()
  • async/await
  • queueMicrotask()

Event Loop

Event Loop는 무한 루프로 Call Stack, Microtask Queue, Callback Queue를 조정합니다. Call Stack이 비어 있으면 다음과 같은 순서로 콜백을 이동시킵니다.

  1. Microtask Queue를 모두 비운다.
  2. 그 다음 Callback Queue에서 하나를 꺼내 Call Stack에 넣는다.
  3. 다시 1번으로 돌아가 흐름을 반복한다.

전체 흐름을 이해하기 위한 예시 코드

function soma(a, b) {
    return a + b;
}

console.log('Síncrono 1');

console.log(soma(5, 5));

setTimeout(() => {
    console.log("Assíncrono 2")
}, 0);

Promise.resolve("Assíncrono 1").then((item) => {
    console.log(item)
});

console.log('Síncrono 2')

Call Stack이 쌓이는 순서

  • 코드가 읽히면 Global Execution Context가 생성되어 Call Stack 바닥에 쌓인다.

  • console.log('Síncrono 1')이 스택에 올라가 실행되고 바로 제거된다.

  • console.log(soma(5, 5))가 스택에 올라가고, 내부에서 soma(5, 5)가 다시 스택에 쌓인다. soma가 실행·제거된 뒤 console.log(10)이 실행·제거된다.

  • setTimeout(callback, 0)이 스택에 올라가 콜백을 Web API에 등록하고 바로 제거된다.

    Web API 스레드에서 타이머가 동작하면서 동시에 메인 흐름은 계속 진행된다. 타이머가 끝나면 콜백이 Task Queue에 들어가고, Event Loop은 Microtask Queue가 완전히 비워진 뒤에 이 콜백을 확인한다.

  • Promise.resolve("Assíncrono 1")이 스택에 올라가 콜백을 Microtask Queue에 등록하고 바로 제거된다.

  • console.log('Síncrono 2')가 스택에 올라가 실행·제거된다.

  • 모든 코드가 읽히면 Global Execution Context가 스택에서 제거된다.

Event Loop 동작

  1. Call Stack이 비어 있으므로 Event Loop은 Microtask Queue를 비운다. 첫 번째 마이크로태스크(console.log('Assíncrono 1'))가 Call Stack으로 이동해 실행되고 제거된다.
  2. Microtask Queue도 비었으니 이제 Task Queue에서 첫 번째 아이템(console.log('Assíncrono 2'))을 Call Stack으로 옮겨 실행한다.

비록 setTimeoutPromise.resolve보다 먼저 선언되었지만, setTimeout 콜백은 타이머가 만료된 뒤 Task Queue에 들어가기 때문에, Microtask Queue가 먼저 비워지는 Event Loop의 우선순위 규칙 때문에 Promise 콜백이 먼저 실행됩니다.

우리의 흐름은 여기서 끝납니다. 만약 Microtask Queue에 새로운 콜백이 추가된다면, Event Loop은 다시 Task Queue를 확인하기 전에 그 콜백을 먼저 Call Stack으로 옮겨 실행합니다.


추가 자료

  • JavaScript Execution Model
  • Using microtasks in JavaScript with queueMicrotask
  • In depth: Microtasks and the JavaScript Runtime Environment
  • 사진: Aleksandr Popov, Unsplash
0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.