Event Loop

강정우·2023년 3월 15일
0

JavaScript

목록 보기
35/53
post-thumbnail
  • 우선 JS는 알다시피 단일 스레드 기반언어이다. 즉, 동시에 1가지 일만 처리할 수 있다.
    이는 연산이 오래걸리는 task를 만났을 때 로드가 매우 지연되는 치명적인 경험을 제공하고 이를 방지하고자 "비동기" 개념이 나왔다.

JS 엔진 이벤트 루프 작동과정

  • Heap: 메모리 할당이 발생하는 곳
  • Call Stack : 실행된 코드의 환경을 저장하는 자료구조, 함수 호출 시 Call Stack에 push 됩니다. (Call Stack에 대한 자세한 설명은 여기)
  • Web APIs: DOM, AJAX, setTimeout 등 브라우저가 제공하는 API

JS engine(event loop)이 brower에 돌아가는 과정

  • 브라우저는 Web APIs, Event Table, Callback Queue, Event Loop 등으로 구성되며 JS 코드가 실행될 때 브라우저와의 동작은 아래 그림으로 표현할 수 있다.

  • Web APIs: JS 엔진이 아니며 브라우저 API로 Call Stack에서 실행된 비동기 함수는 Web API(Browser API)인 timer를 호출하고, Web API는 콜백함수를 Callback Queue에 밀어 넣는다.

  • Event Table: 비동기적으로 실행된 콜백함수가 보관 되는 영역이다. Queue의 자료 구조를 따른다.
    특정 event(timeout, click, mouse move 등등)가 발생했을 때 어떤 callback 함수가 호출되야 하는지를 알고 있는 자료구조이다.
    위 코드에서 호출된 timer가 종료되면 event가 발생하게 되는데 이때 exec callback 함수가 실행되어야 한다는 것을 Event Table이 알고 있다.

  • Callback Queue: 이벤트 발생 시 실행해야 할 callback 함수가 Callback Queue에 추가된다.

  • Event Loop: Event Loop의 역할은 간단하다.

    1. Call Stack과 Callback Queue를 감시한다.
    2. Call Stack이 비어있을 경우, Callback queue에서 함수를 꺼내 Call Stack에 추가 한다.

Microtask Queue

  • microtask 는 일반 task queue보다 순위를 우선적으로 가져간다.
    즉, Microtask Queue가 전부 실행된 후 task queue가 실행된다.

  • 주로 프라미스 잡(promise job)이 해당된다. Promise api

  • 예) Promise, async/await, process.nextTick, Object.observe, MutationObserver

Animation Queue

  • requestAnimationFrame 과 같이 브라우저 렌더링과 관련된 task를 넘겨받는 Queue이다.

  • requestAnimationFrame API가 실행되면 콜백이 Animation Frames으로 담긴다.

  • 우선순위는 Microtask Queue > Animation Frames > Task Queue 순으로 실행된다.

이때 중요한 점은 Microtask Queue나 Animation Frames를 방문할 때는, 큐 안에 있는 모든 작업들을 수행하지만, Task Queue를 방문할 때는 한 번에 하나의 작업만 call stack으로 전달하고 다른 Queue를 순회한다.

use-case

1. cpu 소모가 많은 task 쪼개기

  • 연산이 많이 필요로 하는 곳에 event loop(async)를 사용하지 않는다면 "지연", "멈춤" 현상이 발생한다. 그래서 이러한 상황을 예방하기위해 task를 쪼갤 수 있다. 다음 예제 코드를 봐보자
let i = 0;
let start = Date.now();

function count() {
  // CPU 소모가 많은 무거운 작업을 수행
  for (let j = 0; j < 1e9; j++) {
    i++;
  }
  alert("처리에 걸린 시간: " + (Date.now() - start) + "ms");
}

count();
  • 위 코드는 실행되는 동안 브라우저 멈추고
let i = 0;
let start = Date.now();

function count() {
  // 무거운 작업을 쪼갠 후 이를 수행 (*)
  do {
    i++;
  } while (i % 1e6 != 0);
  
  // 여기에 onclick 이벤트 라든지
  
  if (i == 1e9) {
    alert("처리에 걸린 시간: " + (Date.now() - start) + "ms");
  } else {
    setTimeout(count); // 새로운 호출을 스케줄링 (**)
  }
}

count();
  • 위 코드는 실행해도 브라우저 나머지 기능이 이상없이 동작하는데 그 이유는 (*)로 표시한 do-while 반복에서 count 태스크 일부가 처리되고, 카운팅이 다 끝나지 않았다면 (**)로 표시한 줄에서 카운팅 태스크가 다시 스케줄링 되기 때문이다.

  • 부분 카운팅 실행 중간 중간에 '환기’를 해 줘서 이벤트 루프가 돌아갈 수 있게 해주면, 사용자 이벤트에 반응하면서 무거운 태스크 처리가 가능해진다.

  • 하지만 두 코드의 시간차에 매우 큰데 이를 줄일려면 setTimeout을 앞으로 보내면 된다. 그래서 숫자를 세기 전에 스케쥴링을 하며 숫자를 세며 대기 시간을 소모할 수 있어 조금이라도 빠른 코드가 된다.

2. 프로그레스 바

  • 원래 브라우저 특성 상 현재 작업중인 task가 끝나야 DOM에 변경된 부분을 redering 해주는데
  • 이 setTimout을 사용하여 task를 여러개로 쪼개면 sub task 중간마다 상태변화를 볼 수 있다.
<div id="progress"></div>

<script>
  let i = 0;
  function count() {
    // 무거운 작업을 쪼갠 후 이를 수행
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);
    if (i < 1e7) {
      setTimeout(count);
    }
  }

  count();
</script>

3. 이벤트 처리 끝난 이후에 작업

  • 이벤트 핸들러를 만들다 보면 이벤트 버블링이 끝나 모든 DOM 트리 레벨에서 이벤트가 핸들링 될 때까지 특정 액션을 연기시켜야 하는 경우가 생기곤 한다.
    이럴 때 연기시킬 액션 관련 코드를 지연 시간이 0인 setTimeout으로 감싸면 원하는 동작을 구현할 수 있다.
menu.onclick = function() {
  ...

  // 클릭한 메뉴 내 항목 정보가 담긴 커스텀 이벤트 생성
  let customEvent = new CustomEvent("menu-open", {
    bubbles: true
  });

  // 비동기로 커스텀 이벤트를 디스패칭
  setTimeout(() => menu.dispatchEvent(customEvent));
};
  • 이렇게 되면 click 이벤트가 완전히 끝난 다음에 dispatch를 진행하여 오류 나지 않는다.

reference

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글