SUIN

[JS] Event Loop 란? 본문

JavaScript

[JS] Event Loop 란?

choi suin 2023. 4. 4. 17:26
728x90

웹 브라우저의 동작 원리

- 웹페이지를 서버에 요청(Request)하고 서버의 응답(Response)을 받아 브라우저에 표시

브라우저는 서버로부터 HTML, CSS, Javascript, 이미지 파일 등을 응답받는다. HTML, CSS 파일은 렌더링 엔진의 HTML 파서와 CSS 파서에 의해 파싱(Parsing)되어 DOM, CSSOM 트리로 변환되고 렌더 트리로 결합된다. 이렇게 생성된 렌더 트리를 기반으로 브라우저는 웹페이지를 표시한다.

자바스크립트는 자바스크립트 엔진이 처리한다.

HTML 파서는 <script> 태그를 만나면 자바스크립트 코드를 실행하기 위해 DOM 생성 프로세스를 중지하고 자바스크립트 엔진으로 제어 권한을 넘긴다

제어 권한을 넘겨 받은 자바스크립트 엔진은 script 태그 내의 자바스크립트 코드 또는 script 태그의 src 어트리뷰트에 정의된 자바스크립트 파일을 로드하고 파싱하여 실행한다.

자바스크립트의 실행이 완료되면 다시 HTML 파서로 제어 권한을 넘겨서 브라우저가 중지했던 시점부터 DOM 생성을 재개한다.

실행 환경(Runtime)

브라우저는 단순히 엔진 하나만으로 구성되어 있지 않다.  DOM, AJAX, setTimeout 등의 브라우저에서 제공하는 Web API라고 하는 것들이 있다. 또한 이러한 Web API의 호출을 통제하기 위한 Event Queue와 Event Loop도 존재한다.

JS Engine

자바스크립트 엔진은 Memory Heap 과 Call Stack으로 구성 되어 있다.(가장 유명한 것이 구글의 V8 Engine)

Memory Heap : 메모리 할당이 일어나는 곳(우리가 프로그램에 선언한 변수, 함수 등)

Call Stack : 코드가 실행될 때 쌓이는 곳. stack 형태로 쌓임.

  • Stack(스택) : 자료구조 중 하나,후입선출(LIFO, Last In First Out)의 룰을 따른다.

 

하나의 쓰레드 = 하나의 콜 스택 = 한번에 하나의 작업

Call Stack은 하나이기 때문에 한번에 코드 한줄을 실행하며 이때문에 js는 단일 스레드 (sigle thread) 언어라고 한다.

 Call Stack 동작 방식 

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

웹 브라우저에서 제공하는 Web API

  • Web API는 자바스크립트 엔진이 아닌 웹 브라우저에서 제공하는 api
  • DOM, Ajax, Timeout등을 제공

Callback Queue

  • 비동기적으로 실행된 콜백함수가 보관 되는 영역

비동기함수가 실행된다면, Web API가 호출되며 Callback Queue 대기 후 무조건 스택이 비어있을때 큐가 하나씩 실행 하나씩 스택으로 올려보낸다.

Event Loop

 이벤트 루프의 역할 

이벤트 루프는 콜 스택과 콜백 큐를 감시하는 역할로 콜백 큐에 함수가 존재하고 콜 스택이 비었다면 콜백 큐에서 콜백을 꺼내 콜 스택에 넣어주는 역할 (반복적인 행동을 틱(tick)이라고 한다.)

 이벤트 루프의 정의 

이벤트 루프는 태스크가 들어오길 기다렸다가 태스크가 들어오면 이를 처리하고, 처리할 태스크가 없는 경우엔 잠드는, 끊임없이 돌아가는 자바스크립트 내 루프입니다

자바스크립트 엔진이 돌아가는 알고리즘을 일반화

  1. 처리해야 할 태스크가 있는 경우: 먼저 들어온 태스크부터 순차적으로 처리함
  2. 처리해야 할 태스크가 없는 경우: 잠들어 있다가 새로운 태스크가 추가되면 다시 1로 돌아간다.

Queue의 두가지 종류 (Macrotask Queue , Microtask Queue)

이벤트 루프의 비동기 작업 처리 우선순위는 Microtask Queue  Animation Frames  Task Queue (Macrotask Queue) 

Task Queue(Macrotask Queue)

- 우리가 기존에 알고 있던 태스크 큐(Task Queue)는 매크로태스크 큐(Macrotask Queue)라고도 부른다.

- 태스크 큐는 setTimeout(), setInterval(), setImmediate()와 같은 task를 넘겨받는다.

매크로태스크 큐에 들어가는 함수

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame (mousemove…)
  • I/O
  • UI 렌더링

Microtask Queue

- 마이크로태스크 큐(Microtask Queue)는 Promise나 async/await와 같은 비동기 호출을 넘겨받는다.

- 자바스크립트 엔진은 매크로태스크 하나를 처리할 때마다 또 다른 매크로태스크나 렌더링 작업을 하기 전에 마이크로태스크 큐에 쌓인 마이크로태스크 전부를 처리한다.

- 마이크로태스크 전체가 처리되는 동안에는 UI 변화나 네트워크 이벤트 핸들링이 일어나지 않는다.

마이크로태스크 큐에 들어가는 함수들

  • process.nextTick
  • Promise
  • Object.observe
  • MutationObserver

 

setTimeout(() => alert("timeout")); //  1.스택         2.매크로태스크 큐        8.실행

Promise.resolve()
  .then(() => alert("promise")); // 3.스택             4마이크로태스크 큐        7.실행

alert("code");  // 5.스택                              6.실행
  1. code – 일반적인 동기 호출이므로 가장 먼저 매크로태스크 큐에 들어간 후 실행.
  2. promise – .then은 마이크로태스크 큐에 들어가 처리되기 때문에, 현재 코드(alert("code"))가 실행되고 난 후에 실행.
  3. timeout – setTimeout에서 설정한 시간이 끝난 후 콜백 함수를 실행하는 것은 매크로태스크이기 때문에 가장 마지막에 출력.

그런데 개발을 하다 보면 직접 만든 함수를 현재 코드 실행이 끝난 후, 새로운 이벤트 핸들러가 처리되기 전이면서 렌더링이 실행되기 전에 비동기적으로 실행해야 하는 경우가 생기곤 한다. 이럴 때 queueMicrotask를 사용해 커스텀 함수를 스케줄링하면 된다.

<script>
  function count() {
    for (let i = 0; i < 1e6; i++) {
      i++;
      progress.innerHTML = i;
    }
  }
  count();
</script>
<script>
  let i = 0;

  function count() {

    // 무거운 작업을 쪼갠 후 이를 수행
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e7) {
      setTimeout(count); 
      //또는 
      queueMicrotask(count)
    }

  }

  count();
</script>

 

 


정리

이벤트 루프의 역할

이벤트 루프는 콜 스택과 콜백 큐를 감시하는 역할로 콜백 큐에 함수가 존재하고 콜 스택이 비었다면 콜백 큐에서 콜백을 꺼내 콜 스택에 넣어주는 역할 (반복적인 행동을 틱(tick)이라고 한다.)

이벤트 루프의 정의

이벤트 루프는 태스크가 들어오길 기다렸다가 태스크가 들어오면 이를 처리하고, 처리할 태스크가 없는 경우엔 잠드는, 끊임없이 돌아가는 자바스크립트 내 루프입니다

이벤트 루프 알고리즘

  1. 매크로태스크 큐에서 가장 오래된 태스크를 꺼내 실행합니다(예: 스크립트를 실행).
  2. 마이크로태스크 큐가 빌 때 모든 마이크로태스크를 실행합니다. (태스크는 오래된 순서대로 처리됩니다.)
  3. 렌더링할 것이 있으면 처리합니다.
  4. 매크로태스크 큐가 비어있으면 새로운 매크로태스크가 나타날 때까지 기다립니다.
  5. 1번으로 돌아갑니다

.

새로운 매크로태스크를 스케줄링하는 방법

  • 지연시간이 0인 setTimeout(f) 사용하기

이 방법을 사용하면 계산이 복잡한 큰 태스크 하나를 여러 개로 쪼갤 수 있습니다. 태스크를 여러 개로 쪼개면 태스크 중간중간 사용자 이벤트에 반응할 수 있고, 작업 진척 상태를 화면에 표시해줄 수도 있습니다.

지연시간이 0인 setTimeout은 이벤트가 완전히 처리되고 난 후(버블링이 끝난 후)에 특정 작업을 수행하도록 스케줄링할 때도 사용됩니다.

새로운 마이크로태스크를 스케줄링하는 방법

  • queueMicrotask(f) 사용하기
  • 이외에도 프라미스 핸들러는 마이크로태스크 큐에 들어가 처리됩니다.

마이크로태스크 전체가 처리되는 동안에는 UI 변화나 네트워크 이벤트 핸들링이 일어나지 않습니다.

렌더링이나 네트워크 요청 등의 작업들은 마이크로태스크 전부가 처리되고 난 직후 처리됩니다.

이런 처리 순서 덕분에 queueMicrotask를 사용해 함수를 비동기적으로 처리할 때 애플리케이션 상태의 일관성이 보장됩니다.