Published on

태스크큐 & 마이크로태스크큐

이벤트 루프

태스크 큐

태스크큐란, 실행해야 할 태스크의 집합을 의미한다. 이벤트루프는 태스크큐를 한개 이상 가지고 있고 이름과는 다르게 자료구조에서의 큐(queue)가 아니고 set의 형태를 가지고 있다. 선택된 큐 중에 실행 가능한 가장 오래된 태스크를 가져와야하기 때문이다. 태스크 큐에서의 '실행해야 할 태스크'라는 것은 비동기 함수의 콜백함수나, 이벤트 핸들러 등을 의미한다. 이벤트 루프는 현재 실행 중인 작업이 완료되면 태스크 큐에서 다음 작업을 꺼내어 실행한다.

마이크로 태스크 큐

이벤트 루프는 하나의 마이크로 태스크 큐를 갖는다. 태스크 큐보다 먼저 실행되며, 대표적으로 Promise가 있다. 마이크로 태스크 큐가 빌 때까지 기존 태스크 큐의 실행은 뒤로 미뤄 진다. (태스크큐보다 우선순위가 높다) 마이크로태스크는 현재 실행 중인 태스크가 종료되면 즉시 실행된다. 현재 태스크가 마이크로태스크 큐에 있는 모든 작업을 처리한 후 다음 태스크로 넘어간다.

  • 태스크 큐: setTimeout, setInterval, setImmediate
  • 마이크로 태스크 큐: process.nextTick, Promise, queueMicroTask, MutationObserver
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <body>
    <ul>
      <li>동기코드: <button id="sync">0</button></li>
      <li>태스크: <button id="task">0</button></li>
      <li>마이크로 태스크: <button id="microtask">0</button></li>
    </ul>

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

    const allRun = document.getElementById('button');

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

    task.addEventListener('click', function () {
      for (let i = 0; i <= 100000; i++) {
        setTimeout(() => {
          task.innerHTML = i;
        }, 0);
      }
    });

    microtask.addEventListener('click', function () {
      for (let i = 0; i <= 100000; i++) {
        queueMicrotask(() => {
          microtask.innerHTML = i;
        });
      }
    });

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

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

        queueMicrotask(() => {
          microtask.innerHTML = i;
        });
      }
    });
  </script>
</html>
  • 동기코드는 0 ~ 100,000까지 숫자가 올라가기 전까지 렌더링이 일어나지 않다가 for문이 끝나야 한번에 렌더링이 된다.
  • 태스크큐는 setTimeout 콜백이 큐에 들어가기 전까지 잠깐의 대기 시간을 갖다가 1 ~ 100,000까지 다 끝난 후에 순차적으로 렌더링된다.
  • 마이크로태스크큐는 100,000까지 다 끝낸 후에 렌더링이 된다.

콜백함수나 이벤트 핸들러를 일시 저장한다는 점은 태스크큐와 동일하지만 마이크로태스크큐는 태스크큐보다 우선순위가 높다. 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에서 대기하고 있는 함수를 가져와 실행한다. 이후 마이크로태스크 큐가 비면 태스크 큐에서 대기하고 있는 함수를 가져와서 실행한다.

중요한 차이점 우선순위: 마이크로태스크가 태스크큐보다 우선순위가 높기 때문에 마이크로태스크가 모두 처리된 이후에 태스크가 실행됨.

재귀적 호출: 마이크로태스크에서 다른 마이크로태스크를 호출하면 즉시 실행됨. 반면 태스크큐에서 다른 태스크를 호출하면 현재 태스크가 완료된 이후 실행됨.

  • 마이크로태스크에서 다른 마이크로태스크 호출 (즉시 실행)
console.log('Start');

Promise.resolve().then(() => {
  console.log('First microtask');

  // 새로운 마이크로태스크를 호출하고, 이는 즉시 실행됨
  Promise.resolve().then(() => {
    console.log('Nested microtask');
  });

  console.log('End of first microtask');
});

console.log('End');
// ------
// 결과
// Start
// End
// First microtask
// End of first microtask
// Nested microtask

위 코드에서 첫 번째 마이크로태스크에서 다른 마이크로태스크를 호출했을 때, 호출된 마이크로태스크는 현재 마이크로태스크가 종료되자마자 즉시 실행.

  • 태스크 큐에서 다른 태스크 호출 (완료 이후 실행)
console.log('Start');

// 태스크 큐에 등록된 콜백 함수
setTimeout(() => {
  console.log('Task in the task queue');
}, 0);

console.log('End');
//------
// 결과
// Start
// End
// Task in the task queue

위 코드에서 setTimeout으로 등록된 콜백은 현재 코드의 실행이 완료된 이후에 태스크 큐에 들어가고, 현재 마이크로태스크나 태스크 큐에 있는 다른 작업이 끝난 이후에 실행됨.