본문으로 건너뛰기

Task와 Microtask Queue

JavaScript 코드는 작성한 순서대로만 실행되는 것처럼 보이지만, 브라우저에서는 Task와 Microtask Queue를 이용해 실행 순서를 관리합니다.

특히 setTimeout과 Promise의 실행 순서를 이해하려면 Task와 Microtask의 차이를 알아야 합니다.

1. Task란?​

Task는 브라우저가 한 번에 처리하는 하나의 작업 단위입니다.

대표적인 Task에는 다음과 같은 것들이 있습니다.

  • setTimeout의 콜백 실행
  • 사용자 클릭 이벤트
  • 네트워크 응답 처리
  • 초기 JavaScript 실행

예를 들어 다음 코드에서 setTimeout의 콜백은 바로 실행되는 것이 아니라 Task로 등록됩니다.

setTimeout(() => {
console.log("B");
}, 0);

0ms라고 해서 즉시 실행되는 것은 아닙니다.

현재 실행 중인 JavaScript가 끝난 뒤, 브라우저가 해당 콜백을 Task로 실행할 수 있는 시점이 되었을 때 실행됩니다.


2. Microtask란?​

Microtask는 현재 실행 중인 Task가 끝난 직후 처리되는 더 우선순위가 높은 작은 작업 단위입니다.

대표적으로 다음이 Microtask를 생성합니다.

  • Promise.then()
  • Promise.catch()
  • Promise.finally()
  • queueMicrotask()
  • MutationObserver

예를 들어,

Promise.resolve().then(() => {
console.log("C");
});

then()의 콜백은 Microtask Queue에 들어갑니다.


3. 실행 순서를 직접 확인해보자​

다음 코드를 살펴보겠습니다.

console.log("A");

setTimeout(() => {
console.log("B");
}, 0);

Promise.resolve().then(() => {
console.log("C");
});

console.log("D");

결과는 다음과 같습니다.

A
D
C
B

왜 이렇게 실행될까요?

① console.log("A")​

현재 JavaScript가 실행되고 있으므로 바로 실행됩니다.

A

② setTimeout​

setTimeout의 콜백은 Task로 등록됩니다.

Task
└── console.log("B")

하지만 지금 실행 중인 JavaScript가 아직 끝나지 않았기 때문에 실행되지 않습니다.

③ Promise.then()​

then()의 콜백은 Microtask Queue에 들어갑니다.

Microtask Queue
└── console.log("C")

④ console.log("D")​

현재 실행 중인 JavaScript가 계속 실행되므로 바로 실행됩니다.

A
D

이제 현재 Task가 끝났습니다.


4. Task가 끝나면 Microtask를 먼저 처리한다​

현재 Task가 끝나면 브라우저는 Microtask Queue를 비울 때까지 Microtask를 처리합니다.

현재 상태는 다음과 같습니다.

Task
└── console.log("B")

Microtask Queue
└── console.log("C")

Microtask가 먼저 실행됩니다.

C

Microtask Queue가 비워진 후에야 다음 Task를 실행합니다.

B

따라서 최종 결과는 다음과 같습니다.

A
D
C
B

5. Microtask가 더 우선적으로 실행되는 이유​

브라우저의 실행 흐름을 단순화하면 다음과 같습니다.

Task 실행
↓
Task 종료
↓
Microtask Queue 전부 처리
↓
다음 Task

여기서 Microtask Queue를 전부 처리한다는 것이 중요합니다.

예를 들어 Microtask 안에서 또 다른 Microtask를 만들 수도 있습니다.

queueMicrotask(() => {
console.log("A");

queueMicrotask(() => {
console.log("B");
});
});

queueMicrotask(() => {
console.log("C");
});

실행 결과는 다음과 같습니다.

A
C
B

처음에는 다음과 같이 들어갑니다.

Microtask Queue
┌── A
└── C

A를 실행하는 과정에서 새로운 B가 추가됩니다.

Microtask Queue
└── B

따라서 기존에 있던 C가 먼저 실행되고, 그다음 B가 실행됩니다.

즉, Microtask Queue가 비어야 다음 단계로 넘어갈 수 있습니다.

이때 등장하는 개념이 Microtask Checkpoint입니다.

Microtask Checkpoint는 별도의 큐나 작업이 아니라,

현재 실행 중인 작업이 끝난 뒤 Microtask Queue를 확인하고, 큐가 빌 때까지 Microtask를 처리하는 과정

이라고 이해하면 됩니다.

Task
↓
Task 종료
↓
Microtask Checkpoint
↓
Microtask
↓
Microtask
↓
Microtask ...
↓
Queue가 비어짐
↓
다음 Task

6. Microtask를 무한히 생성하면 어떻게 될까?​

Microtask는 실행되는 동안 새로운 Microtask를 추가할 수 있습니다.

function loop() {
queueMicrotask(loop);
}

loop();

이 코드는 다음과 같은 상황을 만듭니다.

Microtask 실행
↓
새로운 Microtask 추가
↓
Microtask 실행
↓
새로운 Microtask 추가
↓
Microtask 실행
↓
새로운 Microtask 추가
↓
...

앞에서 살펴본 것처럼 브라우저는 Microtask Queue가 빌 때까지 Microtask를 처리합니다.

따라서 이런 코드처럼 Microtask가 계속 추가되면 Queue가 비워지지 않습니다.

그 결과 다음 Task가 실행되지 못하고, 브라우저가 정상적으로 렌더링할 기회도 얻지 못할 수 있습니다.

Task
↓
Microtask
↓
Microtask 추가
↓
Microtask
↓
Microtask 추가
↓
Microtask
↓
...
↓
다음 Task 실행 ❌
렌더링 ❌

이처럼 Microtask를 과도하게 생성하면 다른 Task의 실행을 지연시키고 화면 업데이트까지 막을 수 있습니다.

따라서 Microtask는 일반적으로 짧게 처리하고, 오래 걸리는 작업이나 반복적인 작업을 Microtask로 계속 연결하지 않는 것이 중요합니다.


7. Task와 Microtask의 핵심 차이​

구분TaskMicrotask
대표적인 예setTimeout, 클릭 이벤트Promise.then, queueMicrotask
실행 시점Task로 선택되었을 때현재 Task가 끝난 직후
우선 처리Microtask보다 낮음Task보다 먼저 처리
처리 방식하나의 Task 실행Queue가 빌 때까지 계속 처리
주의점실행 시점은 정확한 시간과 일치하지 않을 수 있음너무 많이 생성하면 다음 Task와 렌더링을 지연시킬 수 있음