🟨 2-22. 이벤트 루프와 비동기 흐름의 시각적 이해 — 브라우저의 내부 작동 구조
1. 기본 구조 이해: 이벤트 루프의 세 친구
자바스크립트 런타임은 기본적으로 세 가지 핵심 요소로 돌아간다 👇
구성 요소 역할
| 콜 스택(Call Stack) | 실행 중인 코드의 “현재 위치” |
| 태스크 큐(Task Queue) | 타이머, DOM 이벤트, fetch 콜백 등 대기 중인 비동기 작업 |
| 마이크로태스크 큐(Microtask Queue) | Promise, MutationObserver 같은 “짧은 비동기” 대기 공간 |
그리고 이 셋을 끊임없이 돌며 조율하는 것이 바로 이벤트 루프(Event Loop) 다.
“이벤트 루프는 브라우저의 심장처럼 계속 뛰며,
콜 스택이 비면 큐에 쌓인 작업들을 순서대로 처리한다.”
2. 동기 vs 비동기 흐름 다시보기
console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");
실행 순서:
1
3
2
✅ setTimeout()은 태스크 큐(Task Queue)에 콜백을 등록한다.
✅ 콜 스택이 모두 비워진 다음, 이벤트 루프가 큐에 있는 함수를 꺼내 실행한다.
3. 마이크로태스크(Microtask)의 등장
Promise나 async/await은 “조금 더 빠른 비동기”인 마이크로태스크 큐에 쌓인다.
console.log("시작");
setTimeout(() => console.log("setTimeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
console.log("끝");
실행 순서:
시작
끝
Promise
setTimeout
✅ Promise는 Microtask Queue
✅ setTimeout은 Task Queue
Microtask Queue는 Task Queue보다 우선순위가 높다.
4. 내부 실행 순서 시각화
[1] 코드 실행 시작
[2] 콜 스택에 main() 등록
[3] console.log("시작") 실행
[4] setTimeout 등록 → Task Queue로 이동
[5] Promise.then 등록 → Microtask Queue로 이동
[6] console.log("끝") 실행
[7] 콜 스택 비워짐
[8] Microtask Queue 실행 (Promise)
[9] Task Queue 실행 (setTimeout)
즉, 이벤트 루프는 이렇게 작동한다 👇
콜 스택이 비었는가?
├─ 예 → Microtask Queue 비어있는가?
│ ├─ 아니오 → Microtask 실행
│ └─ 예 → Task Queue에서 하나 꺼내 실행
└─ 아니오 → 대기
5. 렌더링(Rendering) 타이밍
브라우저는 “렌더링(화면 갱신)”도 이벤트 루프 안에서 수행한다.
다만 렌더링은 Microtask가 모두 끝난 뒤에 이루어진다.
[Loop Cycle]
1. Microtask Queue 실행
2. Rendering 수행
3. Task Queue 실행
그래서 이런 코드에서는 Promise가 먼저 실행되고,
렌더링이 잠시 뒤에 일어난다.
Promise.resolve().then(() => {
document.body.style.background = "skyblue";
});
✅ Microtask Queue가 모두 비워져야 브라우저가 “화면을 다시 그림”
✅ 그래서 비동기 DOM 조작 시 약간의 지연이 생길 수 있다.
6. 마이크로태스크가 많으면?
Microtask는 Task보다 우선이기 때문에,
Promise가 너무 많으면 렌더링이 지연된다.
let count = 0;
function heavyTask() {
Promise.resolve().then(() => {
if (++count < 10000) heavyTask();
});
}
heavyTask();
console.log("끝");
✅ 이 경우 브라우저가 렌더링할 시간을 얻지 못함
✅ Microtask는 렌더링보다 항상 먼저 실행되기 때문
👉 이런 문제를 방지하려면 setTimeout()으로 한 템포 늦추는 것도 방법이다.
7. async/await 내부 구조
await은 내부적으로 Promise를 사용하기 때문에
Microtask Queue에 등록된다.
async function example() {
console.log("1");
await Promise.resolve();
console.log("2");
}
example();
console.log("3");
출력 순서:
1
3
2
✅ await 뒤의 코드는 다음 루프 사이클의 Microtask에서 실행됨
8. 예제: 모든 큐의 흐름 한눈에 보기
console.log("1");
setTimeout(() => console.log("2 (Task Queue)"), 0);
Promise.resolve()
.then(() => console.log("3 (Microtask Queue 1)"))
.then(() => console.log("4 (Microtask Queue 2)"));
console.log("5");
결과:
1
5
3 (Microtask Queue 1)
4 (Microtask Queue 2)
2 (Task Queue)
실행 순서 흐름 👇
main() 시작
→ 콜 스택에 main()
├ console.log(1)
├ setTimeout 등록 → Task Queue
├ Promise.then 등록 → Microtask Queue
├ console.log(5)
→ main() 종료
→ Microtask Queue 실행
→ Task Queue 실행
9. 렌더링과 비동기의 협력
자바스크립트는 싱글 스레드지만,
렌더링 엔진(화면 갱신)은 이벤트 루프와 협력하여 병렬처럼 느껴지게 만든다.
즉, JS가 비동기로 작업을 큐에 넘겨놓고,
브라우저는 그 사이에 렌더링 프레임(16ms 주기) 을 수행한다.
이 구조 덕분에
사용자는 “버벅이지 않는 화면”을 볼 수 있게 된다.
10. 실무 예제 — 애니메이션 루프
function loop() {
console.log("프레임 실행");
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
✅ requestAnimationFrame()은 Task Queue가 아닌 “렌더링 단계”에 실행된다.
✅ 이 덕분에 애니메이션이 부드럽고 GPU와 동기화된다.
11. 이벤트 루프와 Node.js의 차이
Node.js에서도 이벤트 루프가 존재하지만,
브라우저보다 단계가 세분화되어 있다.
Node 이벤트 루프 단계 👇
- timers (setTimeout, setInterval)
- pending callbacks
- idle, prepare
- poll (I/O 이벤트 처리)
- check (setImmediate)
- close callbacks
하지만 기본 원리(스택, 큐, 루프)는 동일하다.
12. 디버깅 팁 — 순서 예측하기
이 코드를 실행하기 전에
결과를 스스로 예측해보자 👇
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve()
.then(() => console.log("promise 1"))
.then(() => console.log("promise 2"));
console.log("end");
정답:
start
end
promise 1
promise 2
timeout
✅ “Microtask → Rendering → Task” 순서만 기억하면
이벤트 루프의 대부분 코드는 예측 가능하다.
13. 이벤트 루프 전체 흐름 요약
1. 콜 스택이 비어 있는지 확인
2. Microtask Queue 실행 (Promise, async/await 등)
3. 렌더링 업데이트 수행
4. Task Queue 실행 (setTimeout, I/O 등)
5. 다음 루프로 이동
💡 이벤트 루프는 “시간 단위의 순환”이 아니라,
스택의 상태를 감시하는 반복 시스템이다.
14. 시각적 구조 (개념도)
┌─────────────┐
│ Call Stack │ ← 실행 중 코드
└──────┬──────┘
│
▼
┌─────────────┐
│ Microtasks │ ← Promise.then(), await
└──────┬──────┘
│
▼
┌─────────────┐
│ Rendering │ ← DOM 업데이트, paint
└──────┬──────┘
│
▼
┌─────────────┐
│ Task Queue │ ← setTimeout, fetch, events
└─────────────┘
이 모든 단계를 이벤트 루프가 계속 돌며 감시한다.
15. 마무리
이제 자바스크립트 코드가
시간의 흐름 속에서 어떻게 실행되는지 완전히 이해했을 거야.
콜 스택, 마이크로태스크, 태스크 큐, 렌더링, 그리고 이벤트 루프는
언뜻 분리된 개념 같지만, 실제로는 브라우저라는 하나의 생태계 안에서
정교하게 협력하고 있다.
“비동기를 이해한다는 건,
자바스크립트의 시간 개념을 이해하는 것이다.”