본문 바로가기
Modern React Deep Dive/01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트

1.5.3 태스크 큐와 마이크로 태스크 큐

by Whiimsy 2024. 5. 14.

1.5.3 태스크 큐와 마이크로 태스크 큐

이벤트 루프는 하나의 마이크로 태스크 큐를 갖고 있는데, 기존의 태스크 큐와는 다른 태스크를 처리함. 여기에 들어가는 마이크로 태스크에는 대표적으로 Promise가 있음. 이 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 가짐. 즉, setTimeoutsetIntervalPromise보다 늦게 실행됨. 명세에 따르면, 마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐의 실행은 뒤로 미뤄짐.

function foo() {
  console.log('foo')
}

function bar() {
  console.log('bar')
}

function baz() {
  console.log('baz')
}

setTimeout(foo, 0)

Promise.resolve().then(bar).then(baz)

 

위 코드를 실행하면 bar, baz, foo 순으로 실행됨. 확실히 Promise가 우선권이 있음.

각 태스크에 들어가는 대표적인 작업은 다음과 같음.

  • 태스크 큐: setTimeout, setInterval, setImmediate
  • 마이크로 태스크 큐: process.nextTick, Promises, queueMicroTask, MutationObserver

그렇다면 렌더링은 언제 실행됨? 태스크? 마이크로 태스크 큐? 태스크 큐를 실행하기에 앞서 먼저 마이크로 태스크 큐를 실행하고, 이 마이크로 태스크 큐를 실행한 뒤 렌더링이 일어남. 각 마이크로 태스크 큐 작업이 끝날 때마다 한 번씩 렌더링할 기회를 얻게 됨.

<html>
  <body>
    <ul>
      <li>동기 코드: <button id="sync">0</button></li>
      <li>태스크: <button id="macrotask">0</button></li>
      <li>마이크로 태스크: <button id="microtask">0</button></li>
    </ul>

    <button id="macro_micro">모두 동시 실행</button>
  </body>
  <script>
    const button = document.getElementById('run')
    const sync = document.getElementById('sync')
    const macrotask = document.getElementById('macrotask')
    const microtask = document.getElementById('microtask')

    const macro_micro = document.getElementById('macro_micro')

    // 동기 코드를 버튼에 1부터 렌더링
    sync.addEventListener('click', function() {
      for (let i = 0; i <= 100000; i++) {
        sync.innerHTML = i
      }
    })

    // setTimeout으로 태스크 큐에 작업을 넣어서 1부터 렌더링
    macrotask.addEventListener('click', function() {
      for (let i = 0; i <= 100000; i++) {
        setTimeout(() => {
          macrotask.innerHTML = i
        }, 0)
      }
    })

    // queueMicrotask로 마이크로 태스크 큐에 넣어서 1부터 렌더링
    microtask.addEventListener('click', function() {
      for (let i = 0; i <= 100000; i++) {
        queueMicrotask(() => {
          microtask.innerHTML = i
        }, 0)
      }
    })

    macro_micro.addEventListener('click', function() {
      for (let i = 0; i <= 100000; i++) {
        sync.innerHTML = i

        setTimeout(() => {
          macrotask.innerHTML = i
        }, 0)

        queueMicrotask(() => {
          microtask.innerHTML = i
        }, 0)
      }
    })
  </script>
</html>

 

위 코드의 결과를 정리하면 다음과 같음.

  • 동기 코드는 해당 연산, 즉 100,000까지 숫자가 올라가기 전까지는 렌더링이 일어나지 않다가 for 문이 끝나야 비로소 렌더링 기회를 얻으며 100,000이라는 숫자가 한 번에 나타남.
  • 태스크 큐(setTimeout)는 모든 setTimeout 콜백이 큐에 들어가기 전까지 잠깐의 대기 시간을 갖다가 1부터 100,000까지 순차적으로 렌더링 됨.
  • 마이크로 태스크 큐(queueMicrotask)는 동기 코드와 마찬가지로 렌더링이 전혀 일어나지 않다가 100,000까지 다 끝난 이후에야 한 번에 렌더링이 일어남.
  • 모든 것을 동시에 실행했을 경우 동기 코드와 마이크로 태스크 큐만 한 번에 100,000까지 올라가고, 태스크 큐만 앞선 예제처럼 순차적으로 렌더링 됨.

이러한 작업 순서는 브라우저에 다음 리페인트 전에 콜백 함수 호출을 가능하게 하는 requestAnimationFrame으로도 확인할 수 있음.

console.log('a')

setTimeout(() => {
  console.log('b')
}

Promise.resolve().then(() => {
  console.log('c')
})

window.requestAnimationFrame(() => {
  console.log('d')
})

 

위 코드를 실행하면 a, c, d, b 순서로 출력됨. 즉, 브라우저에 렌더링하는 작업은 마이크로 태스크 큐와 태스크 큐 사이에서 일어남.

결론적으로 동기 코드는 물론이고 마이크로 태스크 또한 마찬가지로 렌더링에 영향을 미칠 수 있음. 따라서 만약 특정 렌더링이 자바스크립트 내 무거운 작업과 연관이 있다면 이를 어떤 식으로 분리해서 사용자에게 좋은 애플리케이션 경험을 제공해 줄지 고민해 보아야 함.