HTML DOM API/Using microtasks in JavaScript with queueMicrotask()/In depth: Microtasks and the JavaScript runtime environment

김동현·2026년 4월 7일

요청하신 문서 번역을 계속해서 도와드릴게요! 이번에도 빠뜨리거나 요약하는 내용 없이, 편안하고 자연스러운 구어체로 전부 번역해 두었습니다. 구조와 링크 모두 동일하게 마크다운으로 구성했어요.


자바스크립트 실행 컨텍스트 (JavaScript execution contexts)

참고 (Note):
여기 있는 세부 내용들은 일반적인 자바스크립트 프로그래머들에게는 보통 크게 중요하지 않아요. 이 정보는 마이크로태스크가 왜 유용하고 어떻게 작동하는지에 대한 기본 지식으로 제공되는 거랍니다. 만약 별로 관심이 없다면 그냥 넘어가셨다가 나중에 필요할 때 다시 와서 읽으셔도 괜찮아요.

자바스크립트 코드 조각이 실행될 때, 그 코드는 실행 컨텍스트(execution context) 안에서 실행돼요. 새로운 실행 컨텍스트를 만들어내는 코드의 종류에는 세 가지가 있습니다:

  • 전역(global) 컨텍스트는 코드의 메인 본문을 실행하기 위해 만들어지는 실행 컨텍스트예요. 즉, 자바스크립트 함수 밖에 있는 모든 코드를 말하죠.
  • 각 함수는 자신만의 실행 컨텍스트 안에서 실행돼요. 이걸 종종 "지역(local) 컨텍스트"라고 부른답니다.
  • 권장하지 않는 방식이긴 하지만, eval() 함수를 사용해도 새로운 실행 컨텍스트가 만들어져요.

본질적으로 각 컨텍스트는 코드 내의 스코프(scope) 레벨이라고 볼 수 있어요. 이런 코드 세그먼트 중 하나가 실행되기 시작하면, 그 코드를 실행할 새로운 컨텍스트가 구성돼요. 그리고 코드 실행이 끝나면 그 컨텍스트는 파괴된답니다. 아래 자바스크립트 프로그램을 한번 볼까요:

const outputElem = document.getElementById("output");

const userLanguages = {
  Mike: "en",
  Teresa: "es",
};

function greetUser(user) {
  function localGreeting(user) {
    let greeting;
    const language = userLanguages[user];

    switch (language) {
      case "es":
        greeting = `¡Hola, ${user}!`;
        break;
      case "en":
      default:
        greeting = `Hello, ${user}!`;
        break;
    }
    return greeting;
  }
  outputElem.innerText += `${localGreeting(user)}\n`;
}

greetUser("Mike");
greetUser("Teresa");
greetUser("Veronica");

이 짧은 프로그램에는 세 개의 실행 컨텍스트가 포함되어 있어요. 그중 일부는 프로그램이 실행되는 동안 여러 번 생성되었다가 파괴되죠. 각 컨텍스트가 생성될 때마다 실행 컨텍스트 스택(execution context stack) 에 배치(push)돼요. 그리고 실행이 끝나면 컨텍스트 스택에서 제거된답니다.

  • 프로그램이 시작되면 전역 컨텍스트가 생성돼요.
    • greetUser("Mike")에 도달하면 greetUser() 함수를 위한 컨텍스트가 생성되고, 이 실행 컨텍스트가 스택에 푸시돼요.
      • greetUser()localGreeting()을 호출하면, 그 함수를 실행하기 위한 또 다른 컨텍스트가 생성돼요. 이 함수가 값을 반환하고 나면, localGreeting()의 컨텍스트는 실행 스택에서 제거되고 파괴됩니다. 그러면 프로그램은 스택에서 그다음으로 찾은 컨텍스트인 greetUser()로 돌아가서 멈췄던 부분부터 다시 실행을 이어가요.
      • greetUser() 함수가 값을 반환하면 이 컨텍스트도 스택에서 제거되고 파괴됩니다.
    • greetUser("Teresa")에 도달하면, 이를 위한 컨텍스트가 생성되어 스택에 푸시돼요.
      • greetUser()localGreeting()을 호출하면 이 함수를 실행할 새로운 컨텍스트가 생성돼요. 함수가 반환되면 localGreeting()의 컨텍스트는 스택에서 제거, 파괴되고 greetUser()는 멈췄던 곳부터 실행을 계속합니다.
      • greetUser() 함수가 반환되고 해당 컨텍스트는 스택에서 제거 및 파괴돼요.
    • greetUser("Veronica")에 도달하면, 이를 위한 컨텍스트가 생성되어 스택에 푸시돼요.
      • greetUser()localGreeting()을 호출하면 이를 실행할 컨텍스트가 새롭게 생성돼요. 반환 후 localGreeting() 컨텍스트는 스택에서 제거되고 파괴됩니다.
      • greetUser() 함수가 반환되고 컨텍스트가 스택에서 제거 및 파괴돼요.
  • 메인 프로그램이 종료되면 메인 프로그램의 실행 컨텍스트가 실행 스택에서 제거돼요. 이제 스택에 남은 컨텍스트가 없기 때문에 프로그램 실행이 완전히 끝나게 됩니다.

이런 방식으로 실행 컨텍스트를 사용하면 각 프로그램과 함수가 자신만의 변수 세트와 기타 객체들을 가질 수 있게 돼요. 또한 각 컨텍스트는 다음에 실행해야 할 코드 라인과 해당 컨텍스트 운영에 필수적인 기타 정보들을 추적해요. 컨텍스트와 컨텍스트 스택을 이렇게 활용함으로써 지역 및 전역 변수, 함수 호출과 반환 등 프로그램이 동작하는 데 필요한 수많은 핵심 요소들을 관리할 수 있답니다.

재귀 함수(자기 자신을 호출하는 함수로 여러 깊이나 재귀 단계를 거칠 수 있음)에 대해 특별히 기억해 둘 점이 있어요. 함수를 재귀적으로 호출할 때마다 새로운 실행 컨텍스트가 만들어집니다. 덕분에 자바스크립트 런타임은 재귀의 깊이와 그 재귀를 통한 결과 반환을 잘 추적할 수 있지만, 반대로 말하면 함수가 재귀를 돌 때마다 새로운 컨텍스트를 만들기 위해 더 많은 메모리가 필요하다는 뜻이기도 해요.

달려라, 자바스크립트, 달려 (Run, JavaScript, run)

자바스크립트 코드를 실행하기 위해, 런타임 엔진은 코드를 실행할 에이전트(agents) 들의 집합을 유지하고 관리해요. 각 에이전트는 실행 컨텍스트들의 집합, 실행 컨텍스트 스택, 메인 스레드, 워커(worker)를 처리하기 위해 생성될 수 있는 추가 스레드들의 집합, 태스크 큐, 그리고 마이크로태스크 큐로 이루어져 있어요. (일부 브라우저에서 여러 에이전트가 공유하는 메인 스레드를 제외하면) 에이전트의 각 구성 요소는 해당 에이전트 고유의 것이랍니다.

이제 런타임이 어떻게 기능하는지 좀 더 자세히 들여다볼게요.

이벤트 루프 (Event loops)

각 에이전트는 계속해서 반복 처리되는 이벤트 루프(event loop)에 의해 구동돼요. 매 반복(iteration)마다 이벤트 루프는 대기 중인 자바스크립트 태스크를 최대 하나 실행하고, 그다음 대기 중인 마이크로태스크들을 모두 실행한 뒤, 다시 루프를 돌기 전에 필요한 렌더링과 페인팅 작업을 수행해요.

여러분의 웹사이트나 앱 코드는 웹 브라우저의 사용자 인터페이스와 동일한 스레드(thread) 에서 실행되며, 같은 이벤트 루프를 공유해요. 이것이 바로 메인 스레드(main thread) 예요. 메인 스레드는 여러분 사이트의 메인 코드 본문을 실행하는 것 외에도, 사용자의 이벤트나 다른 이벤트들을 받아 전달하고 웹 콘텐츠를 렌더링하고 페인팅하는 등 수많은 작업을 처리한답니다.

다시 말해 이벤트 루프는 사용자와의 상호작용과 관련해서 브라우저 안에서 일어나는 모든 일들을 구동해요. 하지만 지금 우리가 다루고 있는 맥락에서 더 중요한 사실은, 이벤트 루프가 해당 스레드 내에서 실행되는 모든 코드 조각의 스케줄링과 실행을 책임진다는 거예요.

이벤트 루프에는 세 가지 종류가 있어요:

윈도우 이벤트 루프 (Window event loop)
: 윈도우 이벤트 루프는 비슷한 출처(origin)를 공유하는 모든 창(windows)을 구동하는 루프예요 (단, 아래 설명처럼 몇 가지 추가적인 한계가 있습니다).

워커 이벤트 루프 (Worker event loop)
: 워커 이벤트 루프는 워커를 구동하는 루프예요. 기본 웹 워커(web workers), 공유 워커(shared workers), 서비스 워커(service workers)를 포함한 모든 형태의 워커가 여기에 속해요. 워커는 "메인" 코드와 분리된 하나 이상의 에이전트에 보관되는데, 브라우저는 특정 유형의 모든 워커에 대해 단일 이벤트 루프를 사용할 수도 있고 여러 개의 이벤트 루프를 사용해 워커들을 나누어 처리할 수도 있어요.

워크렛 이벤트 루프 (Worklet event loop)
: 워크렛(worklet) 이벤트 루프는 특정 에이전트의 워크렛 코드를 실행하는 에이전트를 구동하는 데 쓰이는 루프예요. Worklet 타입과 AudioWorklet 타입의 워크렛이 여기에 포함됩니다.

같은 출처(origin)에서 로드된 여러 창(windows)은 같은 이벤트 루프에서 실행될 수 있어요. 각 창은 이벤트 루프의 대기열(queue)에 태스크를 밀어 넣어서 프로세서를 번갈아 가며 하나씩 사용하게 되죠. 명심할 점은, 웹 분야에서 쓰는 "창(window)"이라는 단어는 실제 브라우저 창뿐만 아니라 탭이나 프레임을 포함하여 "웹 콘텐츠가 실행되는 브라우저 수준의 컨테이너"를 의미한다는 거예요.

같은 출처를 가진 창들끼리 이렇게 이벤트 루프를 공유할 수 있는 특수한 상황들이 있어요. 예를 들면:

  • 한 창이 다른 창을 열었을 때, 두 창은 이벤트 루프를 공유할 가능성이 높아요.
  • 만약 창이 사실 <iframe> 안의 컨테이너인 경우, 자신을 포함하고 있는 부모 창과 이벤트 루프를 공유할 가능성이 높아요.
  • 멀티 프로세스를 구현한 웹 브라우저에서 우연히 같은 프로세스를 공유하게 된 창들의 경우.

정확한 세부 동작은 브라우저마다 구현 방식에 따라 다를 수 있어요.

태스크와 마이크로태스크 (Tasks vs. microtasks)

태스크(task) 란 스크립트의 초기 실행, 비동기적인 이벤트 발생 등 표준 메커니즘에 의해 실행되도록 스케줄링된 모든 것을 말해요. 이벤트를 사용하는 것 외에도, setTimeout()이나](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout)이나) setInterval()을](https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval)을) 사용해서 태스크를 큐에 넣을 수 있답니다.

태스크 큐와 마이크로태스크 큐의 차이는 간단하지만 아주 중요해요:

  • 이벤트 루프의 새로운 반복(iteration)이 시작되면, 런타임은 태스크 큐에서 다음 태스크를 꺼내 실행해요. 이 반복이 시작된 이후에 큐에 새롭게 추가된 태스크들은 다음 반복이 될 때까지 실행되지 않아요.
  • 반면에 하나의 태스크가 종료되고 실행 컨텍스트 스택이 텅 비게 되면, 마이크로태스크 큐에 있는 모든 마이크로태스크가 차례대로 실행돼요. 여기서 결정적인 차이점은, 마이크로태스크를 실행하는 도중에 새로운 마이크로태스크가 스케줄링되더라도 큐가 완전히 빌 때까지 마이크로태스크 실행을 계속한다는 거예요. 즉, 마이크로태스크는 새로운 마이크로태스크를 큐에 계속 넣을 수 있고, 이렇게 새로 추가된 마이크로태스크들은 다음 태스크가 시작되기 전, 그리고 현재 이벤트 루프 반복이 끝나기 전에 모두 실행됩니다.

문제점 (Problems)

여러분의 코드는 브라우저의 사용자 인터페이스와 동일한 스레드에서 같은 이벤트 루프를 공유하며 실행돼요. 그렇기 때문에 만약 코드가 블로킹(blocking)되거나 무한 루프에 빠져버리면 브라우저 자체도 멈춰버리게 됩니다. 버그 때문이든 아니면 코드가 너무 복잡하고 무거운 작업을 수행하고 있어서든, 성능이 조금만 느려져도 사용자는 브라우저가 버벅거린다고 느끼게 돼요.

오늘날에는 여러 프로그램과 그 안의 여러 코드 객체들이 동시에 작업을 시도하고, 동시에 브라우저도 프로세서 시간을 필요로 하잖아요? 사이트를 렌더링해서 그리고 자신의 UI를 업데이트하며 사용자 이벤트를 처리하는 등의 작업까지 더해지면 너무나 쉽게 병목현상이 생기고 전체가 막혀버리곤 한답니다.

해결책 (Solutions)

메인 스크립트가 새로운 스레드에서 다른 스크립트들을 실행할 수 있게 해주는 웹 워커(web workers)를 사용하면 이 문제를 완화할 수 있어요. 잘 설계된 웹사이트나 앱은 복잡하거나 오래 걸리는 작업에 워커를 사용해서, 메인 스레드가 웹 페이지를 업데이트하고 레이아웃을 잡고 렌더링하는 본연의 일 외에는 최대한 일을 적게 하도록 만들죠.

프로미스(promises) 같은 비동기 자바스크립트(asynchronous JavaScript) 기법을 사용하면 이 문제를 더욱 완화할 수 있어요. 요청 결과를 기다리는 동안에도 메인 코드가 계속해서 실행될 수 있게 해주니까요. 하지만 라이브러리나 프레임워크를 구성하는 코드처럼 좀 더 근본적인 레벨에서 실행되는 코드는, 단일 요청이나 태스크의 결과와 독립적으로 작동하면서 메인 스레드 상의 "안전한 시간"에 코드가 실행되도록 스케줄링할 방법이 필요할 수 있습니다.

마이크로태스크는 이 문제에 대한 또 다른 훌륭한 해결책이에요. 다음 이벤트 루프 반복이 시작될 때까지 기다려야 하는 태스크와 달리, 다음 반복이 시작되기 직전에 코드가 실행되도록 예약할 수 있어서 훨씬 더 미세한 수준의 접근과 제어를 제공하거든요.

마이크로태스크 큐는 이전부터 존재했지만, 역사적으로는 프로미스 같은 것들을 구동하기 위해 내부적으로만 사용되었어요. 하지만 웹 개발자들에게 직접 노출된 queueMicrotask()가](https://developer.mozilla.org/en-US/docs/Web/API/Window/queueMicrotask)가) 추가되면서, 자바스크립트 실행 컨텍스트 스택에 남은 컨텍스트가 없을 때 코드가 안전하게 실행되도록 예약해야 하는 곳이라면 어디서든 사용할 수 있는 통합된 마이크로태스크 큐가 탄생하게 되었죠. 여러 인스턴스와 모든 브라우저, 자바스크립트 런타임에 걸쳐 표준화된 큐 메커니즘이 있다는 건, 이러한 마이크로태스크들이 항상 같은 순서로 안정적으로 작동한다는 걸 의미해요. 덕분에 찾아내기 힘든 골치 아픈 버그들을 피할 수 있게 되었습니다.

참고 자료 (See also)

profile
프론트에_가까운_풀스택_개발자

0개의 댓글