태스크 큐 — setTimeout 콜백이 줄 서는 곳

setTimeout(fn, 0) 이 0ms 뒤에 돌지 않는 이유. 타이머가 끝난 콜백과 클릭 이벤트는 태스크 큐에 줄을 서고, 콜 스택이 빌 때 하나씩 꺼내진다.

Seobway · · 7분

0ms 라고 했는데 왜 나중에 도나

setTimeout 에 0 을 줬다. "0ms 뒤에 실행" 이니 바로 돌 것 같다.

setTimeout(() => console.log("타이머"), 0);

console.log("동기 1");
console.log("동기 2");

// 출력: 동기 1 → 동기 2 → 타이머

아래 두 줄보다 위에 적었고 0ms 인데도 맨 마지막이다.
setTimeout 의 숫자는 "이만큼 뒤에 실행" 이 아니라 "이만큼 뒤에 줄을 세워라" 이기 때문이다.
그 줄이 태스크 큐다.


어떻게 동작하는가

자바스크립트는 한 번에 하나만 실행한다. 지금 실행 중인 것은 콜 스택에
쌓여 있다. 콜 스택이 일하는 중에는 아무도 끼어들 수 없다.

그러면 "1초 뒤에 이거 해 줘" 나 "클릭되면 이거 해 줘" 는 어디서 기다리나?

setTimeout(cb, 100) 한 줄은 세 구간을 지난다.

  1. 맡긴다 — setTimeout 은 cb 를 브라우저의 타이머에 맡기고 곧바로 끝난다.
    자바스크립트는 기다리지 않고 다음 줄로 간다.
  2. 줄 선다 — 100ms 가 지나면 브라우저가 cb 를 태스크 큐에 넣는다. 아직 실행이 아니다.
  3. 꺼내 실행한다 — 이벤트 루프가 콜 스택이 비었을 때
    큐 맨 앞에서 하나를 꺼내 콜 스택에 올린다. 그때 비로소 실행된다.

그래서 숫자는 최소 대기 시간이다. 100ms 가 지나 줄을 섰어도 콜 스택이 바쁘면 계속 기다린다.

인터랙티브 다이어그램: 타이머 두 개가 줄 서고 꺼내지는 과정 (단계별로 재생할 수 있다)

  1. setTimeout(A, 100) — A 를 브라우저 타이머에 맡기고 setTimeout 은 바로 끝난다.
  2. setTimeout(B, 0) — B 도 맡긴다. 아직 아무것도 실행되지 않았다.
  3. 동기 코드 console.log 가 먼저 돈다. 콜 스택이 쓰이는 동안 큐는 기다린다.
  4. 0ms 가 지난 B 가 태스크 큐에 줄을 선다. 줄을 섰을 뿐 아직 실행 전이다.
  5. 콜 스택이 비었다. 이벤트 루프가 큐 맨 앞의 B 하나를 꺼내 올린다.
  6. B 가 끝나 빠진다. 100ms 가 지나 A 도 줄을 선다.
  7. 스택이 비어 있으니 A 를 꺼내 실행한다.

한 번에 하나씩 꺼낸다

이벤트 루프는 태스크를 한 번에 하나만 꺼낸다. 하나를 끝까지 돌리고, 콜 스택이 빈 뒤에야
다음 것을 본다. 태스크와 태스크 사이에 브라우저는 화면을 다시 그릴 기회를 얻는다.

그래서 태스크 하나가 오래 걸리면 그동안 다음 태스크도, 화면 갱신도 전부 멈춘다.
버튼을 눌러도 반응이 없는 "먹통" 이 이것이다.


직접 확인

먼저 출력 순서를 예측해 보고 돌려라. 두 번째 실험은 0ms 타이머가 실제로는 몇 ms 뒤에
도는지 잰다.

// 실험 1 — 순서
setTimeout(() => console.log("C (100ms)"), 100);
setTimeout(() => console.log("A (0ms, 먼저 등록)"), 0);
setTimeout(() => console.log("B (0ms, 나중 등록)"), 0);
console.log("동기 코드");

// 실험 2 — 0ms 인데 얼마나 기다리나
const start = performance.now();
setTimeout(() => {
  const waited = Math.round(performance.now() - start);
  console.log(`0ms 타이머가 실제로 기다린 시간: ${waited}ms`);
}, 0);

// 콜 스택을 300ms 동안 붙잡는다 — 이 동안 큐는 한 줄도 줄지 않는다
const until = Date.now() + 300;
while (Date.now() < until) {}
console.log("동기 루프 끝 (300ms 점거)");

같은 0ms 인 A 와 B 는 등록한 순서대로 나온다. 줄이니까. 그리고 0ms 타이머는
300ms 가량 기다린다. 그동안 콜 스택이 비지 않았기 때문이다.


흔한 실수


한 줄 정리

setTimeout·DOM 이벤트의 콜백은 바로 실행되지 않고 태스크 큐에 줄을 선다.
이벤트 루프는 콜 스택이 비었을 때 맨 앞 하나씩 꺼내 실행한다. 그래서 타이머 숫자는
"정확한 시각" 이 아니라 "최소 대기 시간" 이다.

참고

  1. [1] JavaScript execution model — MDN Web Docs
  2. [2] Window: setTimeout() — MDN Web Docs
  3. [3] Event loops — task queue (HTML Standard, WHATWG)

관련 글