해뜨기전에자자

Eventloop: macrotask and microtask 본문

개발/javascript

Eventloop: macrotask and microtask

조앙'ㅁ' 2020. 9. 7. 01:43

ECMAScript에는 이벤트 루프가 없다.

Nodejs 뿐만 아니라 브라우저 javascript 실행 흐름은 Eventloop를 기반으로 한다.

Eventloop의 작동 방식을 이해하는 것은 최적화 및 올바른 아키텍처에 중요하다. 그러나 event loop의 개념은 HTML 스펙에 정의되어있다. https://html.spec.whatwg.org/multipage/webappapis.html#event-loops

Nodejs와 브라우저에서의 javascript, eventloop 의 관계

eventloop는 javascript의 스펙이 아니다. javascript는 Heap, stack만 존재하고 실제로 eventloop는 javascript 실행환경에 있다. 왼쪽은 브라우저 환경에서의 이벤트루프이고, 오른쪽 그림은 nodejs 환경에서의 eventloop이다. javascript와 이벤트루프가 분리되어있다는 것은 아래와 같은 도식으로도 확인할 수 있다. 브라우저를 기준으로 설명해보면 webAPIs, 로딩, setTimeout의 콜백들은 eventloop가 실행시킨다.

단일 호출 스택과 Run-to-Completion

  • Javascript는 단일 호출 스택을 가지고 있고, 하나의 자바스크립트 코드(Task)가 모두 실행될때까지 다음 작업은 실행되지 않는다.
  • Javascript는 script/handler/event activates 을 실행시키는 것 외에 대부분의 시간 동안 아무것도 하지 않는다.
  • 실행 단위들은 task라고 부른다. task들은 macrotask queue (v8 term)에 들어간다.

Macrotask - Task Queue

  • <script src="..."> 가 로딩 되었을 때 실행시키는 것
  • 유저가 마우스를 움직였을때, mousemove 이벤트를 dispatch 하고, handler를 실행시키는 것
  • setTimeout에 스케줄 된 시간이 다 되었을 때, callback을 실행시키는 것
  • .. etc

개발 시에는 두가지 고려해야 할 점이 있다.

  • 엔진이 task를 실행시키는 동안 rendering 이 일어나지 않는다. task가 오래 실행 되는지 상관하지 않고, task가 완료된 후에 DOM의 변경 사항이 painted된다.
  • task가 시간이 너무 오래 걸릴 경우, browser 가 다른 task를 실행하거나 user event를 처리하지 못하고 얼마간 시간이 지나 "페이지가 응답하지 않습니다" 같은 알람을 받게 될 수 있다.

EventLoop

  • A fundamental abstract concept of the JavaScript programming model
  • 이벤트루프의 구현은 아래와 같이 표현될 수 있다. microTask가 도입되기 전 기본적인 컨셉은 아래와 같다.
while (true) {
    const task = eventLoop.nextTask();
    if (task) {
        task.execute();
    }
    if (eventLoop.needsRendering())
        eventLoop.render();
}

Microtask - Job Queue

ES6/ ES2015 부터 도입된 Promise는 Microtask로 JobQueue에 들어간다. 한 루프에서 하나의 microTask를 실행하지만, macroTask는 해당 큐가 비어질때까지 계속 실행된다. 이 과정에 끝나야 DOM의 변경사항이 painted가 되기 때문에 promise에서 cpu를 과도하게 점유시키고 끝나지 않는 경우 브라우저가 멈추는 현상이 일어날 수 있으므로 주의해야한다. 아래 슈도 코드를 보자.

Eventloop pseudo code

while (true) {
    const task = eventLoop.nextTask();
    if (task) {
        task.execute();
    }

    for(let microTask of queueMicrotask)
            microTask.execute();

    if (eventLoop.needsRendering())
        eventLoop.render();
}

Practice

결과를 예상해보자

while (true) {
    const task = eventLoop.nextTask();
    if (task) {
        task.execute();
    }

    for(let microTask of queueMicrotask)
            microTask.execute();

    if (eventLoop.needsRendering())
        eventLoop.render();
}
  • 결과
  • script start script end promise1 promise2 setTimeout1
  • 퀴즈: 왜 promise1이 setTimeout1보다 먼저 나올까?
  • 이유는 script start, script end 의 스크립트 부분이 global task 에 속해 MacroTask가 되기 때문이다. 틀린 사람은 "단일 호출 스택과 Run-to-Completion"의 Task 종류(=macrotask)에 대해 다시 살펴보자.
  • 단순히 microTask, macroTask의 개념에서 봤을때 브라우저 등 실행환경에 따라 이 순서는 바뀌지 않는다. 그러나 다른 이벤트 (ex scroll, resize, rAF 등)의 경우 실행 환경에 따라 순서의 차이가 있을 수 있다. 자세한 실험 결과는 A dive into event loop specification 에서 확인할 수 있다.

Ref