1. 자바스크립트는 싱글 스레드 언어다
먼저 전제부터 정확히 잡자.
자바스크립트는 싱글 스레드(Single Thread) 기반으로 동작한다.
즉, 한 번에 하나의 작업만 실행할 수 있다.
console.log("1");
console.log("2");
console.log("3");
출력:
1
2
3
단순하죠?
그런데 다음 코드를 보면 이야기가 달라진다.
console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");
출력:
1
3
2
왜 0초인데 “2”가 나중에 실행될까?
그 답이 바로 이벤트 루프(Event Loop) 에 있다.
2. 콜 스택(Call Stack)이란?
콜 스택은 자바스크립트 엔진이 현재 실행 중인 코드를 관리하는 작업 목록(Stack) 이다.
예시 👇
function first() {
second();
console.log("first 실행");
}
function second() {
console.log("second 실행");
}
first();
콜 스택 흐름:
[1] first() 호출 → 스택에 push
[2] second() 호출 → push
[3] second() 실행 완료 → pop
[4] first() 실행 완료 → pop
✅ 스택은 LIFO (Last In, First Out) 구조
✅ 즉, “마지막에 들어온 함수가 가장 먼저 나간다”
3. 이벤트 루프(Event Loop)의 역할
이벤트 루프는 말 그대로 “계속 도는 관찰자”다.
“콜 스택이 비어 있으면, 대기 중인 비동기 콜백을 실행시킨다.”
이벤트 루프는 콜 스택(Call Stack) 과 태스크 큐(Task Queue) 를 끊임없이 감시하며,
비동기 작업을 순서대로 처리한다.
4. 비동기 동작의 흐름
아래 코드를 보자 👇
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
결과:
A
D
C
B
이걸 해석해보면 이렇게 된다 👇
단계 설명
| 1 | console.log("A") → 바로 실행 |
| 2 | setTimeout() → 콜백이 Task Queue로 이동 |
| 3 | Promise.then() → Microtask Queue로 이동 |
| 4 | console.log("D") → 바로 실행 |
| 5 | 콜 스택이 비면 → Microtask Queue (C) 먼저 실행 |
| 6 | 다음으로 Task Queue (B) 실행 |
즉, Promise.then()은 setTimeout보다 먼저 실행된다.
5. Task Queue vs Microtask Queue
구분 예시 실행 시점
| Task Queue (매크로태스크) | setTimeout, setInterval, setImmediate, DOM 이벤트 | 콜 스택이 비면 실행 |
| Microtask Queue | Promise.then, process.nextTick, MutationObserver | 각 Task 실행 후 즉시 실행 |
👉 Microtask Queue가 항상 Task Queue보다 우선순위가 높다.
6. 실행 순서 시각화
다음 코드를 예로 들어 보자 👇
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
실행 순서 흐름:
[Call Stack]
console.log("1")
setTimeout(...)
Promise.then(...)
console.log("4")
↓
[Microtask Queue]
-> Promise.then()
[Task Queue]
-> setTimeout()
이벤트 루프가 콜 스택을 비운 뒤,
먼저 Microtask(3)를 실행하고
그다음 Task(2)를 처리한다.
결과:
1
4
3
2
7. 동기와 비동기의 협력
자바스크립트는 이 구조 덕분에
동기적 안정성과 비동기적 유연성을 동시에 유지한다.
예를 들어, React의 렌더링 사이클이나
Vue의 nextTick(), Node.js의 I/O 처리도 전부 이 원리 위에 있다.
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");
결과:
start
end
promise
timeout
“Promise는 즉시 실행되지만, .then()은 다음 이벤트 루프 사이클에서 실행된다.”
8. async/await 내부 구조
async/await은 결국 Promise의 문법적 설탕(Syntactic Sugar) 이다.
async function run() {
console.log("1");
await Promise.resolve();
console.log("2");
}
run();
console.log("3");
결과:
1
3
2
왜?
await은 함수의 실행을 잠시 멈추고,
Promise가 완료된 뒤 다음 Microtask 사이클에서 나머지 코드를 실행하기 때문이다.
9. 실무 예제 — 비동기 순서 제어
console.log("데이터 요청 시작");
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(() => console.log("데이터 응답 도착"))
.then(() => console.log("DOM 업데이트"));
console.log("UI 렌더링 중...");
실행 순서:
데이터 요청 시작
UI 렌더링 중...
데이터 응답 도착
DOM 업데이트
즉, 네트워크 요청(fetch)은 바로 실행되지만,
응답 처리 콜백은 나중에 (이벤트 루프를 통해) 실행된다.
10. 브라우저가 실제로 일하는 순서
브라우저의 내부 이벤트 순환 구조는 이렇게 된다 👇
1. 콜 스택이 비었는지 확인
2. Microtask Queue 처리 (Promise 등)
3. 렌더링 업데이트 (Repaint/Reflow)
4. Task Queue 처리 (setTimeout 등)
5. 다시 1단계로 이동
💡 그래서 “렌더링이 늦는 이유”도 사실은 이벤트 루프가 UI 갱신 타이밍을 기다리기 때문이야.
11. setTimeout(0)의 진짜 의미
setTimeout(fn, 0)은 즉시 실행이 아니다.
“다음 이벤트 루프 사이클에 실행하라”는 뜻이다.
즉, 브라우저는 “0초 뒤”가 아니라 “현재 스택이 비면” 처리한다.
그래서 다음 두 코드는 다른 결과를 낸다 👇
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
결과:
promise
timeout
12. 실전: 이벤트 루프 시각화 예시
console.log("1️⃣ 시작");
setTimeout(() => console.log("2️⃣ setTimeout"), 0);
Promise.resolve()
.then(() => console.log("3️⃣ Promise"))
.then(() => console.log("4️⃣ 또 다른 Promise"));
console.log("5️⃣ 끝");
실행 순서 정리:
1️⃣ 시작
5️⃣ 끝
3️⃣ Promise
4️⃣ 또 다른 Promise
2️⃣ setTimeout
✅ Promise.then 체인은 모두 Microtask Queue
✅ setTimeout은 Task Queue
13. Node.js 환경에서는?
Node.js도 이벤트 루프를 가지고 있지만,
브라우저와 약간 다르다.
Node는 다음 순서로 실행된다 👇
1. timers (setTimeout, setInterval)
2. pending callbacks
3. idle, prepare
4. poll (I/O 이벤트)
5. check (setImmediate)
6. close callbacks
하지만 근본적인 개념(콜 스택 + 큐 + 루프)은 동일하다.
14. 정리
개념 설명
| 콜 스택 | 실행 중인 함수 목록 |
| 이벤트 루프 | 스택이 비면 대기 중인 콜백을 실행 |
| Task Queue | setTimeout, setInterval 등 |
| Microtask Queue | Promise, MutationObserver |
| async/await | Promise 기반 문법적 설탕 |
| 실행 우선순위 | Microtask → Task |
15. 마무리
이제 자바스크립트 코드가 언제, 어떤 순서로 실행되는지 정확히 이해했다.
이 개념을 확실히 잡아두면,
React의 렌더링 순서, 비동기 데이터 패칭, useEffect의 타이밍까지 완벽히 설명할 수 있다.
“이벤트 루프를 이해하면, 자바스크립트의 모든 ‘이상한 동작’이 논리적으로 보인다.”
'frontend > javascript' 카테고리의 다른 글
| 🟨 2-18. 클로저(Closure) 완전 정복 — 함수형 자바스크립트의 핵심 개념과 실무 응용 (0) | 2025.11.07 |
|---|---|
| 🟨 2-17. this 완전 정복 — 함수, 객체, 클래스에서의 this 바인딩 규칙과 혼동 포인트 (0) | 2025.11.07 |
| 🟨 2-15. 이벤트 위임 실전 활용 — 성능과 유지보수를 모두 잡는 동적 이벤트 처리법 (1) | 2025.11.07 |
| 🟨 2-14. 이벤트 버블링과 캡처링 완벽 이해 — 브라우저 이벤트 흐름의 핵심 메커니즘 (0) | 2025.11.07 |
| 🟨 2-13. 자바스크립트 DOM 완전 이해 — HTML 문서를 제어하는 핵심 원리와 실전 예제 (0) | 2025.11.07 |