STUDY NOTE · 개념 정리
자바스크립트 이벤트 루프 개념 정리: 콜 스택, 태스크 큐, 마이크로태스크
Frontend
자바스크립트 엔진은 싱글 스레드라 한 번에 하나의 코드만 실행한다. 이벤트 루프는 콜 스택이 비었을 때 대기 중인 작업을 정해진 순서로 꺼내 실행하는 장치로, 싱글 스레드로도 비동기 처리를 가능하게 한다.
1. 구성 요소
| 구성 요소 | 역할 |
| 콜 스택 (Call Stack) | 현재 실행 중인 함수들이 쌓이는 곳. 한 번에 하나만 실행 |
| 힙 (Heap) | 객체가 저장되는 메모리 영역 |
| Web API (브라우저) | setTimeout, fetch, DOM 이벤트 등을 엔진 밖에서 처리하고, 끝나면 콜백을 큐에 넣음 |
| 태스크 큐 (매크로태스크) | setTimeout, setInterval, 이벤트 핸들러, I/O 콜백 대기열 |
| 마이크로태스크 큐 | Promise 콜백(then/catch/finally), await 이후 코드, queueMicrotask, MutationObserver 대기열 |
| 이벤트 루프 | 콜 스택이 비면 큐에서 작업을 꺼내 콜 스택으로 보냄 |
타이머 시간을 세거나 네트워크 응답을 기다리는 일은 엔진이 아니라 브라우저(Node에서는 libuv)가 한다. 엔진은 결과 콜백만 받아 실행한다.
2. 이벤트 루프 한 바퀴
- 태스크 큐에서 하나를 꺼내 실행 (처음엔 스크립트 전체가 하나의 태스크)
- 콜 스택이 비면 마이크로태스크 큐를 빌 때까지 전부 실행 (실행 중 새로 추가된 것도 포함)
- 렌더링이 필요하면: requestAnimationFrame 콜백 → 스타일 계산 → 레이아웃 → 페인트
- 1로 돌아감
3. 태스크 vs 마이크로태스크
| 구분 | 태스크 (매크로태스크) | 마이크로태스크 |
| 예 | setTimeout, setInterval, 이벤트 핸들러, MessageChannel | Promise.then, await 이후, queueMicrotask |
| 한 바퀴에 실행 | 하나 | 큐가 빌 때까지 전부 |
| 우선순위 | 낮음 | 높음 (현재 태스크 직후) |
| 사이에 렌더링 | 태스크와 태스크 사이에 가능 | 마이크로태스크 사이에는 불가 |
4. 실행 순서 예제
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
queueMicrotask(() => console.log("4"));
console.log("5");
출력은 1 5 3 4 2다.
| 시점 | 실행 | 이유 |
| 현재 태스크 | 1, 5 | 동기 코드는 즉시 실행 |
| 마이크로태스크 | 3, 4 | 현재 태스크가 끝나면 전부 실행 (들어간 순서대로) |
| 다음 태스크 | 2 | setTimeout 콜백은 태스크 큐 |
5. async / await
await 앞까지는 동기로 실행되고, await 이후의 코드는 마이크로태스크로 예약된다.
async function f() {
console.log("A");
await null;
console.log("B");
}
f();
console.log("C");
// 출력: A C B
6. requestAnimationFrame과 타이머
| 구분 | setTimeout(fn, 16) | requestAnimationFrame(fn) |
| 실행 시점 | 최소 16ms 이후 아무 태스크 차례 | 다음 화면을 그리기 직전 |
| 화면 주기와 동기화 | 안 됨 (끊김 발생) | 됨 |
| 백그라운드 탭 | 최소 간격이 늘어남 | 멈춤 |
| 용도 | 지연 실행 | 애니메이션, 화면 변화 측정 |
setTimeout(fn, 0)도 0ms 뒤가 아니라 "현재 태스크와 마이크로태스크가 모두 끝난 뒤 다음 태스크 차례"다. 중첩 타이머가 5단계를 넘으면 브라우저가 최소 4ms로 늘린다.
7. 브라우저와 Node.js의 차이
| 구분 | 브라우저 | Node.js |
| 비동기 처리 주체 | Web API | libuv |
| 태스크 단계 | 태스크 큐 하나로 보면 됨 | timers → pending → poll → check → close 단계로 나뉨 |
| 추가 큐 | - | process.nextTick 큐 (Promise 마이크로태스크보다 먼저) |
| 고유 API | requestAnimationFrame | setImmediate (check 단계) |
8. 실제로 어떻게 적용되나
- 화면이 멈추는 이유 이해: 렌더링은 태스크 사이에서만 일어난다. 한 태스크가 1초 걸리면 그동안 화면과 클릭이 모두 멈춘다. 무거운 작업은 여러 태스크로 쪼개거나(
setTimeout,scheduler.yield()) Web Worker로 옮긴다. - 마이크로태스크로 쪼개면 안 되는 이유:
await Promise.resolve()로 쪼개면 마이크로태스크라 큐가 빌 때까지 이어서 실행되므로 렌더링 기회가 생기지 않는다. 쪼갤 때는 태스크를 써야 한다. - 애니메이션과 측정: 화면 변화에 맞춰야 하는 애니메이션은 requestAnimationFrame을 쓴다. 반응속도 테스트처럼 "화면이 바뀐 시점"을 재는 경우에도 상태를 바꾼 시각이 아니라 rAF 시각을 기준으로 잡아야 실제로 보인 시점에 가깝다.
- 정확한 시간 측정: setTimeout은 "최소 이 시간 뒤"라는 뜻이라 오차가 있다. 경과 시간이 필요하면
performance.now()로 직접 계산한다.
9. 면접 질문으로 정리
- Q. 자바스크립트가 싱글 스레드인데 비동기가 가능한 이유는? 타이머·네트워크 같은 작업은 브라우저(Web API)나 libuv가 처리하고, 완료되면 콜백을 큐에 넣는다. 이벤트 루프가 콜 스택이 빌 때마다 큐의 콜백을 꺼내 실행하기 때문이다.
- Q. 태스크와 마이크로태스크의 차이는? 태스크는 setTimeout·이벤트 등으로 한 바퀴에 하나씩 실행되고, 마이크로태스크는 Promise 콜백 등으로 현재 태스크가 끝난 직후 큐가 빌 때까지 전부 실행된다. 그래서 Promise 콜백이 setTimeout(0)보다 먼저 실행된다.
- Q. setTimeout(fn, 0)은 바로 실행되나? 아니다. 현재 실행 중인 코드와 모든 마이크로태스크가 끝난 뒤 태스크 큐 순서가 와야 실행된다.
- Q. requestAnimationFrame을 쓰는 이유는? 브라우저의 화면 갱신 직전에 실행되도록 보장되어 애니메이션이 끊기지 않고, 백그라운드에서는 멈춰 자원을 아낀다.