macro와 micro, 그리고 이 모든걸 지켜보는 event loop

YEONGHUN KO·2023년 11월 30일
0

WEB

목록 보기
8/14
post-thumbnail


script에서 비동기 코드를 쓰면 web broswer api가 보관해놓았다가 적절한 타이밍이 되었을때 JS engine stack으로 넘겨준다.

비동기 코드는 두가지 queue로 나뉜다.

micro / macro

그리고 아래와 같은 순서대로 이벤트 루프가 돌면서 task를 처리한다.

그리고 위의 그림처럼 script내부에서 동기적 코드 => micro tasks queue (모든 작업이 실행될때까지 이벤트 루프는 넘어가지 않음) => 브라우저 화면 랜더링 => macro task에서 한개의 작업이 js 엔진의 call stack으로 넘어감 => 동기적 코드 실행 => micro tasks queue => ... 반복

micro task queue

이름 그대로 세밀한 작업, 가벼운 작업들이 여기에 쌓인다. Promise나 mutationObserver가 micro queue로 넘어간다. 그리고 이벤트 루프가 여기에 방문하면 모든 작업을 JS쪽으로 보낸다.

왜냐면 최대한 같은 환경에서 모든 micro task를 실행해야하기 때문이다.

it guarantees that the application environment is basically the same (no mouse coordinate changes, no new network data, etc) between microtasks. [js info]

같은 환경에서 실행한다는게 무슨뜻인지 이해는 가지 않는다. 그러나 , 추측해볼때, 만약에 micro task를 하나씩 처리한다면은, task사이에 일관적이지 않은 무언가가 발생할 것이다.

그리고 micro task는 자잘한 작업이므로 찔끔찔끔 처리하기 보다 한 번에 다 처리하는게 깔끔하고 유저를 blocking하지 않아서 그런게 아닐까 싶다.

The primary reason for executing micro tasks before macro tasks is to prioritize tasks that are often more critical for maintaining a smooth and responsive user experience.

Micro tasks, often associated with Promises, allow for more fine-grained control and responsiveness in handling asynchronous operations. This design choice helps ensure that promises and related tasks are resolved as quickly as possible. [chat gpt]

macro task queue

이름 그대로 조금 heavy한 작업이 여기에 쌓인다. 그리고 heavy하기 때문에 queue안에 있는 것을 모두 처리하지 않고 한 번에 하나씩 처리한다. 이벤트 루프가 여기에 방문했을때 한 번에 하나만 js engine call stack 으로 보낸다.

주로 settimeout , setinterval을 여기서 수행한다.

그럼 이제 코드로 얘기해보자.

실제 실행 순서를 보기

동기 코드(script)와 마이크로

우선 동기적 코드와 마이크로를 실행해보자 아래처럼 말이다.

<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
</head>

<body>
  <div>
    <div>동기코드 : <span id="synchronous">0</span></div>
    <div>매크로 : <span id="macro">0</span></div>
    <div>마이크로 : <span id="micro">0</span></div>
    <button id="run">실행</button>
    <button id="reset">초기화</button>
  </div>
 
</body>

</html>

    const synchronous = document.querySelector('#synchronous');
    const macro = document.querySelector('#macro');
    const micro = document.querySelector('#micro');
    document.querySelector('#reset').addEventListener('click', reset);
    document.querySelector('#run').addEventListener('click', run);

    function heavyTask(elem, iter) {
      for (let i = 1; i <= iter; i++) {
        elem.textContent = i;
      }
    }

    function run() {
      
      // synchronous task
      heavyTask(synchronous, 30000);
      console.log('synchronous task end');
      
      // asynchronous micro task
      queueMicrotask(() => {
        heavyTask(micro, 40000);
        console.log('micro task end');
      });
    }

    function reset() {
      synchronous.textContent = 0;
      macro.textContent = 0;
      micro.textContent = 0;
    }

우선 각각의 함수를 보자면은 heavyTask는 element를 받아서 textContent를 계속 업데이트 하는 함수이다.

먼저, 동기코드의 숫자(synchronous) content 를 3만번 업데이트하고 마이크로의 숫자(micro) content를 4만번 업데이트한다.

여기서, queueMicrotask 는 promise 객체를 사용하지 않더라도 micro task queue 보낼 수 있는 built-in api이다.

바로 결과를 살펴보자.

동기와 비동기가 함께 랜더링 되는 것을 볼 수 있다. 순서를 자세하게 말하자면.

heavyTask(synchronous, 30000)(synchronous의 content에 담길 i가 30000이 됨) => console.log('synchronous task end') ==> heavyTask(micro, 30000)(micro의 content에 담길 i가 40000이 됨) => console.log('micro task end') => synchronous, micro가 랜더링 됨.

micro와 macro

function run() {  
  // asynchronous macro task
  setTimeout(() => {
    heavyTask(macro, 50000);
    console.log('macro task end');
  });
  
  // asynchronous micro task
  queueMicrotask(() => {
    heavyTask(micro, 40000);
    console.log('micro task end');
  });
}

12

역시 마이크로가 랜더링이 먼저되고 매크로가 나중에 랜더링 된다. 그럼 매크로, 마이크로 , 동기 셋다 실행하면??

동기코드와 macro, micro

function run() {  
  // asynchronous macro task
  setTimeout(() => {
    heavyTask(macro, 50000);
    console.log('macro task end');
  });
  
  // synchronous task
  heavyTask(synchronous, 30000);
  console.log('synchronous task end');
  
  // asynchronous micro task
  queueMicrotask(() => {
    heavyTask(micro, 40000);
    console.log('micro task end');
  });
}

4

동기코드 + 마이크로가 함께 랜더링 되고 그다음에 매크로가 랜더링 된다. 만약 heavyTask의 작업 진행과정이 하나하나 화면에 표시하게 하려면 어떻게 해야할까??

한 번에 하나씩! 이라는 문장을 잘 음미해보자.... 그렇다. macro task queue를 이용하면 됨.

그럼 setTimeOut을 쌓이게끔 하면 되겠지?? 쌓이게끔하려면 반복문을 활용하면 될듯.

function run() {
  // asynchronous macro task but successively
  for (let i = 1; i <= 1000; i++) {
    setTimeout(() => heavyTask(macro, i), 0);
  }
 
  // synchronous task
  heavyTask(synchronous, 30000);
  console.log('synchronous task end');
  
  // asynchronous micro task
  queueMicrotask(() => {
    heavyTask(micro, 40000);
    console.log('micro task end');
  });
}

그럼 어떻게 될까??

동기 코드와 macro , micro 모두 실행해보았다.

역시 동기와 micro먼저 랜더링 되고 이후 setTimeOut들이 반복문에 의해 web browser로 보내진다. 그리고 web broswer에 등록된 setTimeOut가 macro task로 작업을 보내면 이벤트 루프가 한 개씩 call stack으로 보내면서 랜더링이 순차적으로 되는 것!

요 기법으로 progress bar를 이용해 특정 task의 진척도를 시각화 할 수 있을 것 같다.

출처 : https://gobae.tistory.com/134

profile
'과연 이게 최선일까?' 끊임없이 생각하기

0개의 댓글