본문으로 건너뛰기
김신건의 로그

[Javascript] Microtask Queue

· 수정 · 📖 약 2분 · 1,029자/단어 #javascript #async #runtime #event-loop
Microtask Queue, microtask queue, 마이크로태스크 큐, microtask, 마이크로태스크, JavaScript Microtask Queue, JS microtask queue, queueMicrotask

정의

Microtask Queue (마이크로태스크 큐)이벤트 루프 가 매 틱마다 완전히 비울 때까지 처리하는 우선순위 큐다. 매크로태스크 큐보다 항상 먼저 처리된다.

ECMAScript 명세는 이를 “PromiseJobs” 라는 Job 큐로 정의한다 (§9.5). WHATWG HTML 명세는 별도의 “microtask queue” 로 통합해 관리한다 (§8.1.4).

언제 쓰나

목적도구
Promise .then / .catch / .finally자동으로 마이크로태스크
await 이후 코드 재개자동으로 마이크로태스크
현재 동기 코드 직후, 다음 매크로 전에 실행queueMicrotask(fn)
렌더링 전에 DOM 상태 일괄 갱신queueMicrotask
렌더링 이후 실행 필요requestAnimationFrame 또는 setTimeout

이벤트 루프 틱 안에서의 순서

flowchart TD
    A["이벤트 루프 틱 시작"] --> B{"매크로태스크 큐에\n작업 있음?"}
    B -->|"Yes"| C["태스크 1개 꺼내 실행"]
    B -->|"No"| G["대기"]
    C --> D{"마이크로태스크 큐\n비었나?"}
    D -->|"No"| E["마이크로태스크 전부 실행\n(새로 추가되는 것 포함)"]
    E --> D
    D -->|"Yes"| F["렌더링 필요하면 Paint"]
    F --> A

핵심: 매크로태스크 1개 실행 후, 마이크로태스크 큐가 완전히 빌 때까지 계속 실행한 뒤 렌더링으로 넘어간다.

핵심 특성

측면동작
처리량한 틱에 큐가 빌 때까지 전부
우선순위매크로태스크보다 항상 먼저
추가 허용마이크로태스크 처리 중 등록된 것도 같은 틱 에 처리
렌더링 블로킹마이크로태스크 비우는 동안 화면 업데이트 없음

들어가는 콜백

출처예시
Promise.then, .catch, .finally 콜백
async/awaitawait 이후 함수 재개
직접 등록queueMicrotask(fn)
DOM 변경 관찰MutationObserver 콜백
Node.jsqueueMicrotask, process.nextTick (더 높은 우선순위)

매크로태스크와의 실행 순서

console.log('start');

setTimeout(() => console.log('macro'), 0);           // 매크로태스크
Promise.resolve().then(() => console.log('micro'));   // 마이크로태스크
queueMicrotask(() => console.log('microtask'));       // 마이크로태스크

console.log('end');

// 출력: start, end, micro, microtask, macro

실행 순서:

  1. 동기 코드 (start, end) 즉시 실행
  2. 마이크로태스크 큐 비우기 (micro, microtask)
  3. 매크로태스크 큐에서 1개 (macro)

queueMicrotask: 직접 등록

ES2020 에 표준화. Promise 객체 생성 없이 마이크로태스크를 직접 등록한다.

queueMicrotask(() => {
    console.log('동기 코드가 끝난 직후, 렌더링 전에 실행');
});

Promise.resolve().then(fn) 과 실행 타이밍이 동일하지만:

  • 의도가 명확 (스케줄링이 목적임을 코드에서 표현)
  • Promise 인스턴스 생성 비용 없음
  • 에러가 unhandledRejection 으로 튀지 않음 (Promise.then 와 다름)

언제 쓰나

// 라이브러리 내부: "이 작업 끝나면 옵저버에게 알리고 싶은데,
// 동기로 바로 호출하면 호출자 콜 스택을 오염시킴"
function notifyObservers(data) {
    queueMicrotask(() => {
        for (const observer of observers) {
            observer.update(data);
        }
    });
}

await 도 마이크로태스크

await 는 함수를 일시정지하고, 재개(resume) 를 마이크로태스크 큐에 등록한다.

async function f() {
    console.log('1: async 함수 시작');
    await Promise.resolve();
    console.log('3: await 이후 (마이크로태스크로 재개)');
}

console.log('start');
f();
console.log('2: f() 호출 직후 (동기)');

// 출력: start, 1: async 함수 시작, 2: f() 호출 직후 (동기), 3: await 이후 (마이크로태스크로 재개)

await Promise.resolve() 가 이미 정착된 Promise 를 기다려도, 재개는 반드시 다음 마이크로태스크 에 일어난다.

복수 await 의 마이크로태스크 소비

async function slow() {
    await null;   // 마이크로태스크 1개 소비
    await null;   // 마이크로태스크 1개 소비
    return 'done';
}

// async function 을 여러 개 await 할 때 순서
async function main() {
    const p1 = slow();
    const p2 = slow();

    // p1 과 p2 는 교대로 마이크로태스크 큐에 재개를 등록
    const [r1, r2] = await Promise.all([p1, p2]);
}

실전 예시

DOM 배치 갱신 (배치 업데이트 패턴)

let pending = false;
const updates = [];

function scheduleUpdate(data) {
    updates.push(data);
    if (!pending) {
        pending = true;
        queueMicrotask(flushUpdates);   // 다음 마이크로태스크에 일괄 처리
    }
}

function flushUpdates() {
    pending = false;
    const batch = updates.splice(0);
    // batch 를 한 번에 DOM 에 반영
    render(batch);
}

// 동기 코드에서 여러 번 호출해도 렌더링은 1번
scheduleUpdate({ id: 1 });
scheduleUpdate({ id: 2 });
scheduleUpdate({ id: 3 });
// flushUpdates 는 세 호출 후 단 1번 실행

반응성 시스템의 effect 큐잉

React, Vue, Solid 같은 프레임워크 내부에서 state 변경을 마이크로태스크로 배치 처리하는 패턴.

const effectQueue = new Set();
let isFlushing = false;

function scheduleEffect(fn) {
    effectQueue.add(fn);
    if (!isFlushing) {
        isFlushing = true;
        queueMicrotask(() => {
            effectQueue.forEach(effect => effect());
            effectQueue.clear();
            isFlushing = false;
        });
    }
}

옵저버 패턴에서 동기 안전성 보장

class EventEmitter {
    emit(event, data) {
        const handlers = this.listeners.get(event) ?? [];
        // 동기로 호출하면 emit 호출자의 상태가 아직 업데이트 안 됐을 수 있음
        queueMicrotask(() => {
            handlers.forEach(h => h(data));
        });
    }
}

함정

마이크로태스크 무한 등록 (페이지 freeze)

function loop() {
    Promise.resolve().then(loop);
}
loop();
// setTimeout 콜백, 렌더링, 사용자 이벤트 모두 영원히 실행 불가

마이크로태스크 큐는 “비어있을 때까지” 처리한다. 처리 중 새로 등록한 마이크로태스크도 같은 틱 안에 처리되므로, 무한 등록하면 큐가 영원히 비지 않는다.

CAUTION

Promise.resolve().then(loop) 무한 재귀는 setTimeout(loop, 0) 과 달리 렌더링과 I/O 를 완전히 차단한다. 잘게 쪼개야 하는 작업은 매크로태스크 (setTimeout) 로, 즉시 양보해야 하는 작업은 마이크로태스크 로 구분하자.

catch 하지 않은 마이크로태스크 에러

queueMicrotask(() => {
    throw new Error('microtask 에러');
});
// 브라우저에서는 window.onerror / unhandledrejection 으로 전파
// Promise 와 달리 try/catch 로 감쌀 수 없음

// ✅ 내부에서 처리
queueMicrotask(() => {
    try {
        riskyOp();
    } catch (err) {
        handleError(err);
    }
});

Promise.then vs queueMicrotask 에러 버블링

// ❌ Promise.then 에서 던진 에러: unhandledRejection 이벤트
Promise.resolve().then(() => { throw new Error('oops'); });

// ✅ queueMicrotask 에서 던진 에러: 일반 uncaught 에러 (window.onerror)
queueMicrotask(() => { throw new Error('oops'); });

Node.js: process.nextTick 의 위치

Node.js 는 마이크로태스크보다 더 높은 우선순위process.nextTick 큐를 따로 관리한다.

setImmediate(() => console.log('setImmediate'));       // 매크로태스크
setTimeout(() => console.log('setTimeout'), 0);        // 매크로태스크
Promise.resolve().then(() => console.log('promise')); // 마이크로태스크
process.nextTick(() => console.log('nextTick'));       // nextTick (최우선)

// 출력: nextTick, promise, setTimeout 또는 setImmediate (순서 다를 수 있음)

CAUTION

process.nextTick 도 재귀 등록 시 I/O 와 타이머를 영원히 차단한다. 외부에 노출되는 라이브러리 API 는 queueMicrotask 또는 setImmediate 가 더 안전하다.

관련 위키

이 글의 용어 (5개)
[Javascript] 이벤트 루프javascript
정의 이벤트 루프 (Event Loop) 는 싱글 스레드 JavaScript 엔진이 비동기 작업을 처리하기 위해 돌리는 무한 루프다. 콜 스택이 비어있을 때마다 마이크로태스크 큐…
[Javascript] async/awaitjavascript
정의 / 는 기반 비동기 코드를 마치 동기 코드처럼 쓸 수 있게 해주는 ES2017 의 문법 설탕. 본질은 Promise 그 자체, 문법만 다르다. 전체 동작 메커니즘은 글 참조…
[Javascript] Promisejavascript
정의 Promise 는 미래의 어떤 시점에 결정될 값을 나타내는 객체다. ECMAScript 2015 (ES6) 에 도입되어 기반 비동기의 가독성과 에러 처리 문제를 해결했다. …
[JavaScript] requestAnimationFrame: 브라우저 애니메이션 루프javascript
정의 (rAF) 은 브라우저에게 다음 리페인트 직전에 콜백을 실행해달라고 요청합니다. 60Hz 모니터 기준 약 16.67ms 주기. 브라우저 렌더 파이프라인 브라우저는 한 프레임…
[Javascript] setTimeoutjavascript
정의 은 호스트 환경 (브라우저 또는 Node.js) 이 제공하는 타이머 API. 지정한 밀리초가 지난 뒤 을 의 매크로태스크 큐에 등록한다. 자세한 흐름과 함정은 글 참고. 이…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기