Skip to main content

Command Palette

Search for a command to run...

자바스크립트의 Call Stack, Microtask Queue, Macrotask Queue 완벽 정리

Published
•4 min read•View as Markdown

“왜 Promise.then()이 setTimeout()보다 먼저 실행될까?”
“Call Stack, Queue, Event Loop은 서로 어떤 관계일까?”

이 글에서는 자바스크립트의 이벤트 루프(Event Loop) 를 구성하는 핵심 요소
Call Stack, Microtask Queue, Macrotask Queue의 구조와 실행 순서를
실제 코드 예시로 완벽히 정리해봅니다.


🧩 1️⃣ 자바스크립트의 실행 모델

자바스크립트는 단일 스레드(Single Thread) 언어입니다.
즉, 한 번에 한 줄의 코드만 실행할 수 있습니다.

하지만 브라우저나 Node.js 환경은
비동기 코드를 지원하기 위해 이벤트 루프(Event Loop) 구조를 사용합니다.

이 이벤트 루프는 세 가지 주요 구성요소로 작동합니다 👇

구성 요소설명
Call Stack현재 실행 중인 함수들의 “실행 컨텍스트”를 쌓아두는 스택 구조
Macrotask Queue다음 이벤트 루프 사이클에서 실행할 “큰 단위 작업”들의 대기열
Microtask Queue현재 매크로태스크가 끝난 직후 즉시 실행해야 할 “짧은 작업”들의 대기열

⚙️ 2️⃣ 각 구성요소의 역할 요약

🔹 Call Stack

지금 실행 중인 함수들의 호출 흐름을 추적하는 실행 컨텍스트 스택

함수 호출 시 → push
함수 실행 완료 시 → pop

예시:

function a() {
  b();
}
function b() {
  c();
}
function c() {
  console.log("Hello");
}
a();

실행 중 Call Stack 상태:

[c()]
[b()]
[a()]

🔹 Macrotask Queue

“큰 단위의 비동기 작업”을 저장하는 큐.
이벤트 루프 한 사이클당 매크로태스크 하나만 꺼내 실행합니다.

대표 예시:

  • <script> 전체 실행

  • setTimeout, setInterval

  • I/O, UI Event

  • postMessage, MessageChannel

  • setImmediate (Node.js)


🔹 Microtask Queue

매크로태스크가 끝난 직후 즉시 실행해야 하는 “짧은 비동기 작업”들의 큐.
모든 마이크로태스크가 비워질 때까지 연속 실행됩니다.

대표 예시:

  • Promise.then, Promise.catch, Promise.finally

  • queueMicrotask

  • MutationObserver

  • process.nextTick (Node.js)


🔄 3️⃣ 이벤트 루프의 동작 순서

1. 하나의 매크로태스크 실행
2. 그 안에서 생성된 모든 마이크로태스크 실행
3. 렌더링 단계(브라우저)
4. 다음 매크로태스크 실행

다시 말해,

“Call Stack이 비면 → Microtask Queue를 모두 처리 → 다음 Macrotask로 이동”
이 순서가 이벤트 루프의 핵심 원리입니다.


🧩 4️⃣ 예제 코드로 이해하기

console.log("1");

setTimeout(() => console.log("2 (macrotask)"), 0);

Promise.resolve()
  .then(() => console.log("3 (microtask)"))
  .then(() => console.log("4 (microtask)"));

console.log("5");

실행 결과:

1
5
3 (microtask)
4 (microtask)
2 (macrotask)

실행 순서 분석

단계Call StackMicrotask QueueMacrotask Queue출력
Aconsole.log("1") 실행--1
BsetTimeout 실행-콜백 등록
CPromise.then 실행콜백 등록콜백 등록
Dconsole.log("5") 실행then 콜백 대기setTimeout 대기5
E스택이 비어 Microtask 실행모두 실행 (3,4)setTimeout 대기3, 4
FMicrotask 큐 비워짐 → Macrotask 실행-setTimeout 콜백 실행2

⚙️ 5️⃣ Call Stack과 큐의 관계

Call Stack은 “지금 실행 중인 함수들”을 추적하고,
Macrotask/Microtask Queue는 “다음에 실행할 함수들”을 보관합니다.

이벤트 루프는 Call Stack이 비워지는 순간을 감지해
→ Microtask Queue부터 모두 실행
→ 이후 Macrotask Queue에서 다음 함수를 꺼냅니다.


🧠 6️⃣ setTimeout은 Stack에 들어갈까?

setTimeout(() => console.log("A"), 0);
  • setTimeout 호출 자체는 Stack에 잠깐 올라갑니다.
    (함수 호출이므로 push → 내부 타이머 등록 → pop)

  • 타이머가 만료되면 콜백은 Macrotask Queue에 들어감.

  • 그 후 이벤트 루프가 큐에서 꺼내 Stack에 올려 실행합니다.

즉,

setTimeout 함수 본체는 Stack에 들어왔다가 바로 빠지고,
그 콜백은 나중에 Stack에 올라가 실행된다.


🧩 7️⃣ 전체 구조 시각화

       ┌────────────────────┐
       │     Call Stack     │ ← 현재 실행 중인 코드
       └────────┬───────────┘
                │
                ▼
┌────────────────────────────┐
│      Microtask Queue       │ ← Promise.then, queueMicrotask
└────────────────────────────┘
                │
                ▼
┌────────────────────────────┐
│      Macrotask Queue       │ ← setTimeout, I/O, UI Event
└────────────────────────────┘
                │
                ▼
        Event Loop 순환

💡 8️⃣ 정리 요약

구분실행 시점예시우선순위
Call Stack현재 실행 중함수 호출, return최고
Microtask Queue매크로태스크 직후Promise.then, queueMicrotask높음
Macrotask Queue한 루프마다 1개씩setTimeout, I/O, <script>낮음

🧩 9️⃣ 핵심 문장으로 기억하기

“Call Stack은 지금 실행 중인 코드,
Microtask Queue는 지금 끝난 매크로태스크 직후 바로 실행할 코드,
Macrotask Queue는 다음 이벤트 루프 사이클에 실행할 코드다.”


🧭 10️⃣ 결론

  • <script> 실행 자체도 하나의 매크로태스크이다.

  • Promise.then() 은 항상 setTimeout() 보다 먼저 실행된다.

  • Call Stack이 비면 → Microtask 큐를 모두 비우고 → 다음 Macrotask 실행

  • 이벤트 루프는 이 세 요소의 협력으로 비동기를 구현한다.


✍️ 참고 요약 코드

setTimeout(() => console.log("Macrotask"), 0);
queueMicrotask(() => console.log("Microtask"));
console.log("Sync");

// 실행 결과:
// Sync
// Microtask
// Macrotask

✅ 마무리

이제 “이벤트 루프가 어떻게 비동기 코드를 처리하는지”를
단순 암기가 아닌 정확한 실행 순서로 이해할 수 있습니다.

✨ 기억하세요:
Call Stack은 ‘현재 실행 중’,
Microtask Queue는 ‘지금 바로 실행해야 할’,
Macrotask Queue는 ‘다음 턴에 실행할’ 작업의 집합
입니다.


🧾 작성 참고

이 글은 ChatGPT의 도움을 받아 내용을 정리하였습니다.

More from this blog

LangGraph의 astream_events: 에이전트 실행을 실시간으로 들여다보기

💡 한 줄 요약: astream_events()는 LangGraph 그래프 실행 중 발생하는 모든 이벤트(LLM 토큰, 툴 호출, 노드 시작/종료 등)를 실시간 스트림으로 받을 수 있는 비동기 API입니다. 들어가며 AI 에이전트가 응답을 생성하는 데 25초가 걸린다고 상상해보세요. 사용자는 평균 8초 후에 페이지를 이탈합니다. 하지만 스트리밍을 도입

Jun 20, 20266 min read

LLM은 어떻게 글을 읽고 쓰는가 — 입력부터 출력까지

대화형 AI에게 질문을 던지면, 마치 사람처럼 문장을 이해하고 답을 써 내려가는 것처럼 보인다.하지만 그 안에서 벌어지는 일은 생각보다 단순하고, 또 생각보다 기계적이다.이 글에서는 두 가지 질문에 답해 본다.LLM은 어떻게 글을 만들어내는가,그리고 우리가 입력한 프롬프트는 모델에 어떤 모습으로 들어가는가. 1부. LLM은 어떻게 글을 만들어내는가 핵심은

Jun 10, 20264 min read

useEffect 지옥이란 무엇이며, 어떻게 안전하게 사용하는가

React를 어느 정도 사용하다 보면 한 번쯤은“useEffect 지옥에 빠졌다”는 말을 듣거나 직접 느껴본 적이 있을 것이다. useEffect가 계속 늘어나고 의존성 배열은 점점 길어지고 왜 실행되는지 이해하기 어려워지며 eslint-disable-next-line 이 늘어나는 상태 하지만 많은 사람들이 오해한다.useEffect 지옥은 useEffect가 많아서 생기는 문제가 아니다. 이 글에서는 ‘useEffect 지옥’의 ...

Jan 12, 20263 min read

Zod란 무엇인가: TypeScript에서 런타임 검증과 타입 안정성을 동시에 해결하는 방법

TypeScript 프로젝트를 하다 보면 반드시 마주치는 문제가 있다.바로 “타입은 있는데, 데이터는 믿을 수 없다”는 것이다. TypeScript는 컴파일 타임에는 강력하지만,런타임에 들어오는 데이터에 대해서는 아무런 보장을 해주지 않는다. API 요청 데이터 사용자 입력(Form) 환경 변수 (process.env) 외부 JSON / 설정 파일 이 모든 것은 타입 시스템 바깥에서 들어온다. 이 문제를 해결하기 위해 등장한 라이브러...

Jan 10, 20263 min read

Flatpak을 이해하기 위한 배경 지식

배포판, 라이브러리 충돌, 그리고 OSTree까지 리눅스 데스크톱에서 Flatpak이 표준처럼 자리 잡은 데에는 분명한 이유가 있다.그 이유를 이해하려면 단순히 “Flatpak은 패키징 시스템이다”를 넘어서,리눅스 배포판 구조, 라이브러리 버전 충돌, OSTree라는 기술까지 함께 이해해야 한다. 이 글에서는 다음 질문에 답해본다. 리눅스에서 말하는 배포판이란 무엇인가? 라이브러리 버전 충돌은 왜 발생하는가? Flatpak은 이 문제를 어...

Jan 8, 20265 min read

Dev note

32 posts