노트

마이크로태스크

Microtask

프론트엔드#js · 연결된 개념 9개

쉽게 말하면

마이크로태스크는 바리스타가 지금 잔을 다 만들고 다음 주문을 받기 전에, '아 이것도요' 하고 붙은 짧은 일을 전부 먼저 끝내는 것과 같아요. Promise의 then 콜백이 이렇게 지금 코드 바로 뒤에 돌아요.

비유의 한계 '이것도요'가 끝없이 이어지면 다음 손님은 영영 못 와요. 마이크로태스크가 자기 자신을 계속 등록하면 렌더링도 끼어들지 못해 화면이 멈추고, setTimeout이면 사이사이 화면이 그려져요.

현재 실행 중인 코드가 끝난 직후, 다음 태스크로 넘어가기 전에 몰아서 처리되는 작은 작업. 이벤트 루프는 태스크 하나를 끝낼 때마다 마이크로태스크 큐가 빌 때까지 전부 실행한다.

  • 들어가는 것: Promise의 then·catch·finally 콜백, await 다음에 이어지는 코드(continuation), queueMicrotask(), MutationObserver 콜백
  • 같은 태스크 안에서 setTimeout을 먼저 등록했어도 마이크로태스크가 항상 먼저 실행된다
  • 처리 중에 새로 추가된 마이크로태스크도 같은 차례에 처리한다. 그래서 마이크로태스크가 자기 자신을 계속 등록하면 루프를 빠져나가지 못하고 화면이 멈춘다. 같은 재귀라도 setTimeout으로 등록하면 사이사이 렌더링이 끼어들어서 화면은 살아 있다
function loopTimeout() { setTimeout(loopTimeout) }          // 느리지만 화면은 그려짐
function loopMicro()   { queueMicrotask(loopMicro) }         // 탭이 멈춤

굶김을 피하려면

이렇게 마이크로태스크가 큐를 독차지해 렌더링·입력·타이머가 밀리는 현상을 마이크로태스크 굶김(Microtask Starvation)이라고 한다. 흔한 함정은 await다. 이미 끝난 Promise를 await해도 이어지는 코드는 마이크로태스크라서, 루프 안에서 await Promise.resolve()를 반복하면 비동기처럼 보여도 브라우저에 한 번도 양보하지 않는다.

큰 작업은 청크로 나누고, 청크 사이에 태스크로 양보한다. 메인 스레드를 50ms 넘게 붙잡는 작업(롱 태스크)을 줄이는 기본 방법이기도 하다.

const yieldToMain = () =>
  globalThis.scheduler?.yield?.() ?? new Promise(r => setTimeout(r, 0))
 
for (let i = 0; i < items.length; i += 100) {
  items.slice(i, i + 100).forEach(process)
  await yieldToMain()   // 여기서 렌더링·입력 처리가 끼어든다
}
  • setTimeout(fn, 0)은 콜백을 별도 태스크로 미룬다. 다만 중첩이 5단계를 넘으면 명세상 최소 4ms 지연이 붙는다
  • scheduler.yield()는 양보한 뒤 이어지는 코드를 같은 우선순위의 다른 태스크보다 앞에 다시 세운다. 2026 기준 Baseline이 아니라 위처럼 대체 코드를 둔다
  • 화면 갱신과 맞물린 작업은 requestAnimationFrame(다음 리페인트 직전), 급하지 않은 작업은 requestIdleCallback(유휴 시간, 꼭 해야 하면 timeout 지정)에 맡긴다
  • 계산 자체가 무거우면 메인 스레드 밖 Web Worker로 옮긴다

then 체인의 순서

체인의 다음 then은 앞 콜백이 끝나야 큐에 들어간다. 그래서 두 체인을 나란히 만들면 단계별로 번갈아 실행된다.

Promise.resolve().then(() => log('1-1')).then(() => log('1-2'))
Promise.resolve().then(() => log('2-1')).then(() => log('2-2'))
// 1-1, 2-1, 1-2, 2-2

React가 일부 상태 업데이트를 모아 처리하거나(자동 배칭) 상태 관리 라이브러리가 알림을 미루는 데에도 이 "현재 코드 직후" 타이밍이 쓰인다.

참고: MDN — Using microtasks in JavaScript with queueMicrotask()

명세: HTML Standard — timer initialisation steps

출처: web.dev — Optimize long tasks · MDN — Scheduler.yield() · MDN — requestAnimationFrame() · MDN — requestIdleCallback()

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 동기·비동기와 블로킹·논블로킹

    동기·비동기는 결과를 언제 어떻게 받느냐, 블로킹·논블로킹은 기다리는 동안 호출한 스레드가 멈추느냐의 문제다.

  • asyncio 태스크 예외가 조용히 사라지는 문제

    asyncio.create_task()로 띄운 태스크 안에서 예외가 나면, 그 태스크를 누군가 await하기 전까지 예외는 어디에도 전파되지 않는다. 백그라운드 루프가 아무 로그 없이 멈추고, 나머지 프로그램은 멀쩡해 보인다.

  • 우선순위 큐

    원소마다 우선순위가 있고, 들어온 순서와 상관없이 우선순위가 가장 높은 것부터 꺼내는 추상 자료형(Abstract Data Type, ADT). 같은 우선순위끼리의 순서는 보장하지 않는다. 응급실 대기 순서가 좋은 비유다.

  • 마이크로 프론트엔드

    마이크로 프론트엔드는 하나의 큰 프론트엔드 앱을 기능별 조각으로 나눠, 팀마다 따로 개발·배포하고 런타임에 하나의 화면으로 조립하는 아키텍처다. 백엔드의 마이크로서비스(microservices) 발상을 프론트엔드로 가져온 것이다.

  • 스텁 큐

    서드파티 SDK 본체가 아직 로드되지 않았을 때, 작은 스텁(stub)이 들어온 호출을 배열에 쌓아 두고 본체가 도착하면 쌓인 호출을 순서대로 처리하는 방식. Google Tag Manager(GTM)의 dataLayer와 gtag.js의 gtag()가 이 구조이고, Meta Pixel의 fbq() 설치 코드도 본체 전 호출을 배열에 쌓는다. 쌓아 두는 배열은 먼저 들어온 것부터 꺼내는 큐로 쓰인다.

보기 옵션