자바스크립트를 실행하는 하나의 스레드가 할 일을 큐에서 하나씩 꺼내 처리하는 반복 구조. JS 엔진 자체에는 타이머도 네트워크도 없고, 브라우저(또는 Node.js)가 이 루프를 돌리면서 콜백을 엔진에 넘겨준다.
언어 쪽에서 본 모습
- JS는 콜 스택(Call Stack) 하나로 동기 코드를 실행한다(스택). 함수 하나가 시작되면 끝날 때까지 다른 코드가 끼어들지 못한다(run-to-completion)
- 그래서 무거운 동기 루프가 1초 걸리면, 10ms 뒤로 예약한
setTimeout도 1초 넘게 기다린다.setTimeout이 보장하는 것은 "지정한 시간 뒤에 큐에 넣는다"까지이고, 실행 시점은 보장하지 않는다 - 비동기 작업의 결과는 콜백 형태로 큐에 쌓였다가 스택이 비면 실행된다. Promise와
await은 일반 태스크 큐가 아닌 마이크로태스크(Microtask) 큐를 쓴다
브라우저 쪽에서 본 모습
루프 한 바퀴는 대략 이렇다.
while (true) {
const task = taskQueue.shift() // 1. 태스크 하나
run(task)
while (microtaskQueue.length) // 2. 마이크로태스크 전부
run(microtaskQueue.shift())
if (isRenderingOpportunity()) { // 3. 그릴 차례일 때만
runAnimationFrameCallbacks() // rAF 콜백
render() // 스타일·레이아웃·페인트
}
}- 태스크(Task, 매크로태스크):
setTimeout·setInterval, 클릭 같은 DOM 이벤트,postMessage, 네트워크 콜백. 스펙상으로는 출처별로 여러 큐가 있고 브라우저가 어느 큐에서 꺼낼지 고른다. 한 바퀴에 하나만 처리한다 - 마이크로태스크: 큐가 하나뿐이고, 태스크가 끝날 때마다 빌 때까지 전부 처리한다
- 렌더링: 매 바퀴가 아니라 화면을 갱신할 때(60Hz면 약 16.7ms마다)만 온다. 그 직전에 requestAnimationFrame 콜백이 실행되고 이어서 렌더링 파이프라인이 돈다
console.log('1')
setTimeout(() => console.log('timeout'), 0)
Promise.resolve().then(() => console.log('promise'))
console.log('2')
// 1, 2, promise, timeout주의
- "마이크로 → rAF(requestAnimationFrame) → 매크로" 순서로 우선순위가 고정돼 있다는 설명이 흔한데 틀렸다. rAF는 렌더링 차례가 왔을 때만 실행되므로
setTimeout(fn, 0)과의 선후는 프레임 타이밍에 따라 달라진다. 확실한 것은 "마이크로태스크가 다음 태스크보다 먼저 전부 처리된다"뿐이다 - 태스크 하나가 오래 걸리면(Long Task, 50ms 초과) 그동안 클릭에 반응도, 화면 갱신도 못 한다. INP(Interaction to Next Paint)가 나빠지는 가장 흔한 원인이다. React의 동시성 렌더링은 렌더 작업을 잘게 쪼개 이 문제를 줄인다
출처: HTML Standard — Event loops · MDN — JavaScript execution model: Job queue and event loop