이벤트 루프, 마이크로 태스크 큐 in JS

동동·2021년 8월 31일
0

이벤트 루프는 무엇인가요?

  • 이벤트 루프는 싱글 스레드 기반의 자바스크립트에서 비동기 연산을 가능하게 해주는 브라우저의 메커니즘입니다.

  • JS엔진은 싱글 스레드이기 때문에 단일 호출 스택을 사용하며, 요청이 들어올 때마다 해당 요청을 순차적으로 호출 스택에 담아 처리합니다. 하지만 실제 JS가 구동되는 환경(브라우저, Node.js)에서는 여러 개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 JS엔진과 상호 연동하기 위해 사용하는 장치가 바로 '이벤트 루프'입니다.

  • 웹 페이지에는 JS를 실행하고 렌더링을 수행하는 메인 스레드가 있습니다. 메인 스레드는 브라우저가 사용자 이벤트를 처리하고 렌더링하는 곳입니다. 기본적으로 브라우저는 단일 스레드를 사용하여 페이지의 모든 JS를 실행하고 레이아웃, 리플로우 및 가비지 수집을 수행합니다. 메인 스레드 외에 다른 스레드에서 네트워킹, 인코딩, 디코딩 등을 수행하지만, 그런 스레드도 페이지 관련 작업이 끝나면 메인 스레드에 돌아와 정보를 줘야 합니다. 따라서 오랫동안 실행되는 JS 함수는 메인 스레드를 블락하여 응답하지 않는 페이지와 잘못된 사용자 환경으로 이어질 수 있습니다. 이 모든 걸 이벤트 루프가 주관합니다.

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

  • JS에서 비동기 연산은 주로 Web API 로 구성되어 있습니다. setTimeout, setInterval 등의 Web API는 실행되면은 비동기 연산의 실행을 브라우저에게 요청한 뒤 콜 스택에서 사라집니다. 브라우저는 다른 스레드에서 비동기 연산이 완료된 후, 콜백함수를 위한 새로운 테스크를 태스크 큐에 추가합니다.

  • 이벤트 루프는 '현재 실행중인 태스크가 없을 때'(주로 호출 스택이 비워졌을 때) 태스크 큐의 첫 번째 태스크를 꺼내와 실행합니다.

  • JS엔진은 호출 스택이 모두 비워질 때까지 코드를 실행합니다. 하나의 함수가 실행되면 이 함수의 실행이 끝날 때까지는 다른 어떤 작업도 끼어들지 못합니다. 현재 호출 스택에 쌓여있는 모든 함수들이 실행을 마치고 스택에서 제거되기 전까지는 다른 어떠한 함수도 실행될 수 없습니다.

마이크로 태스크 큐는 무엇인가요?

기존의 태스크 큐를 매크로 태스크 큐라고 칭하겠습니다.

  • 마이크로 태스크는 매크로 태스크보다 우선순위가 더 높은 태스크입니다. 따라서 마이크로 태스크에 쌓인 마이크로 태스크가 매크로 태스크 큐에 쌓인 매크로 태스크보다 더 높은 우선순위로 실행됩니다.

  • 각 매크로 태스크 실행 직후, 다른 매크로 태스크 실행이나 렌더링 등보다 앞서서 모든 마이크로 태스크가 실행됩니다

  • JS 엔진의 호출 스택이 비워지고 나면 이벤트 루프는 마이크로 태스크 큐에 마이크로 태스크가 있는지를 먼저 확인합니다. 마이크로 태스크 큐에 작업이 있다면 마이크로 태스크 큐가 빌 때까지 모든 마이크로 태스크를 순차적으로 실행합니다. 마이크로 태스크 실행은 다른 매크로 태스크 실행 및 렌더링 등보다 앞서서 실행됩니다.

  • 따라서 마이크로 태스크는 주로 현재 실행중인 태스크 직후에 발생해야하는 것들을 스케쥴할 때 사용됩니다.

  • 마이크로태스크 실행 중에 추가되는 마이크로태스크는 마이크로태스크큐에 추가되어 다음 매크로 태스트가 실행되기 전에 실행됩니다. 따라서 마이크로 태스크에서 또 다른 마이크로 태스크를 계속해서 추가한다면 무한히 실행되는 마이크로 태스크를 만들 수 있으므로, 주의하여야 합니다.

사용 예시

  • 프로미스가 확정되면 프로미스는 then(catch/finally) 메서드를 통해 등록된 성공 핸들러 또는 실패 핸들러를 마이크로 태스크 큐에 추가합니다. 이미 확정된 프로미스라면 then 메서드를 호출하여 성공 핸들러와 실패 핸들러를 추가하는 즉시, 핸들러를 곧장 마이크로 태스크 큐에 추가합니다. 이것은 프로미스가 이미 확정되었라도 프로미스의 핸들러가 비동기인 것을 보장합니다.

  • queueMicrotask()를 호출해서 마이크로 태스크 큐에 작업을 추가할 수 있습니다.

  • DOM의 변화를 감지할 수 있게 해주는 MutationObserver도 마이크로 태스크를 사용합니다.

요약: 이벤트 루프 알고리즘

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

(자세한 사항은 명세서에서 확인할 수 있습니다).

Q. 브라우저의 구성요소에 따르면 렌더링은 렌더링 엔진이 수행하고, JS 실행은 JS 엔진이 수행하는데 왜 JS엔진이 무거운 작업(무한루프와 같은)을 실행하면 렌더링이 블락이 되는 것일까?

  • 메인 스레드는 브라우저가 사용자 이벤트를 처리하고 렌더링하는 곳입니다. 기본적으로 브라우저는 단일 스레드를 사용하여 페이지의 모든 JS를 실행하고 레이아웃, 리플로우 및 가비지 수집을 수행합니다. 따라서 오랫동안 실행되는 JS 함수는 스레드를 블락하여 응답하지 않는 페이지와 잘못된 사용자 환경으로 이어질 수 있습니다

  • Service Worker와 같은 Web Worker를 의도적으로 사용하지 않는 한 JavaScript는 메인 스레드에서 실행되므로 스크립트가 이벤트 처리 또는 렌더링을 지연시키기 쉽습니다. 메인 스레드에 필요한 작업이 적을수록 스레드가 사용자 이벤트에 응답하고 렌더링하며 일반적으로 사용자에게 응답할 수 있습니다.

https://youtu.be/cCOL7MC4Pl0

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#event_loop

https://meetup.toast.com/posts/89

profile
작은 실패, 빠른 피드백, 다시 시도

0개의 댓글