Event Loop (이벤트 루프)

후훗♫·2020년 1월 12일
79

이번 글은 Event Loop (이벤트 루프) 에 대해 정리해보려고 한다.

Event Loop?

출처 How JavaScript works: an overview of the engine, the runtime, and the call stack

Event Loop는 MDN 문서로 검색하면 "큐의 다음 메시지를 처리합니다" 라고 나온다.
이것만 봐서는 모르겠다.... 하나씩 정리해보자.

JS Engine

자바스크립트 엔진은 Memory HeapCall Stack 으로 구성되어 있다.(그림 왼쪽!)
가장 유명한 것이 구글의 V8 Engine이다.
자바스크립트는 단일 스레드 (sigle thread) 프로그래밍 언어인데,
이 의미는 Call Stack이 하나 라는 이야기이다.
(멀티가 되지 않고, 하나씩 하나씩 처리한다는 의미!)

  • Memory Heap : 메모리 할당이 일어나는 곳
    (ex, 우리가 프로그램에 선언한 변수, 함수 등이 담겨져 있음)
  • Call Stack : 코드가 실행될 때 쌓이는 곳. stack 형태로 쌓임.
    • Stack(스택) : 자료구조 중 하나, 선입후출(LIFO, Last In First Out)의 룰을 따른다.

Web API

그림의 오른쪽에 있는 Wep API는 JS Engine의 밖에 그려져 있다.
즉, 자바스크립트 엔진이 아니다.
Web API브라우저에서 제공하는 API 로, DOM, Ajax, Timeout 등이 있다.
Call Stack에서 실행된 비동기 함수는 Web API를 호출하고,
Web API는 콜백함수를 Callback Queue에 밀어 넣는다.

Callback Queue

비동기적으로 실행된 콜백함수가 보관 되는 영역이다.
예를 들어 setTimeout에서 타이머 완료 후 실행되는 함수(1st 인자),
addEventListener에서 click 이벤트가 발생했을 때 실행되는 함수(2nd 인자) 등이 보관된다.

  • Queue(큐) : 자료 구조 중 하나, 선입선출(FIFO, Frist In Frist OUT)의 룰을 따른다.

Event Loop

Event Loop는 Call Stack과 Callback Queue의 상태를 체크하여,
Call Stack이 빈 상태가 되면, Callback Queue의 첫번째 콜백을 Call Stack으로 밀어넣는다.
이러한 반복적인 행동을 틱(tick) 이라 부른다.

정리하면,

  • V8 엔진에서 코드가 실행되면, Call Stack에 쌓인다.
  • Stack의 선입후출의 룰에 따라 제일 마지막에 들어온 함수가 먼저 실행되며,
    Stack에 쌓여진 함수가 모두 실행된다.
    • 비동기함수가 실행된다면, Web API가 호출된다.
    • Web API는 비동기함수의 콜백함수를 Callback Queue에 밀어넣는다.
    • Event Loop는 Call Stack이 빈 상태가 되면
      Callback Queue에 있는 첫번째 콜백을 Call Stack으로 이동시킨다.
      (이러한 반복적인 행동을 틱(tick)이라 한다.)

자바스크립트를 단일 스레드 프로그래밍 언어라 한번에 하나씩 밖에 실행할 수 없다.
그러나 Web API, Callback Queue, Event Loop 덕분에 멀티 스레드 처럼 보여진다.
👍

Microtask Queue???

출처 자바스크립트 비동기 처리 과정과 RxJS Scheduler

겨우 Event Loop를 조금.. 이해한 것 같은데...
Event Loop가 설명된 또 다른 그림에서 내가 모르는 개념이 보인다...하..

먼저 코드로 간단히 알아보자.

console.log('script start'); 

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

콘솔창에는 어떤 순서로 실행될까?

script start
script end
promise1
promise2
setTimeout

script start, script end 순으로 처리되는건 당연히 알겠는데..
왜 Promise, setTimeout 순으로 실행되는 걸까?

여기서 Microtask Queue의 개념이 나온다.

Event Loop는 우선적으로 Microtask Queue를 확인한다.
만약 Microtask Queue에 콜백이 있다면, 이를 먼저 Call Stack에 담는다.
그리고 Microtask Queue에 더이상 처리해야할 콜백이 없다면,
Task Queue에 확인 후 처리한다.

Promise의 then()의 콜백 은 Task Queue가 아닌 Microtask Queue에 담긴다.
따라서 위 코드에서는 우선순위가 높은 Microtask Queue부터 처리되므로,
Promise의 then() 콜백이 다 실행되고, setTimeout 콜백이 실행되는 거다.

(맨 위 그림에서 Callbakc Queue가 너무 단순하게 처리되어있었다...)

Animation Frames????

산넘어 산이다....ㅠㅠㅠㅠ
Animation Frames이란 개념도 있다.
requestAnimationFrame API가 실행되면 콜백이 Animation Frames으로 담긴다.
(setTimeout이 실행되면 타이머 완료 후 콜백이 Task Queue에 담기는 것 처럼...)

그럼 Microtask Queue, Task Queue, Animation Frames의 우선순위는 어찌될까?
코드로 확인해보자.

console.log("script start");

setTimeout(function() {
  console.log("setTimeout");
}, 0);

Promise.resolve().then(function() {
  console.log("promise1");
}).then(function() {
  console.log("promise2");
});

requestAnimationFrame(function() {
    console.log("requestAnimationFrame");
})
console.log("script end");

콘솔 창에 위의 코드를 실행시키면,

script start
script end
promise1
promise2
requestAnimationFrame
setTimeout
  • Microtask Queue > Animation Frames > Task Queue 순으로 실행된다.
    (크롬 기준이다! 브라우저마다 다를 수 있다.)

다시 정리해보면,

  • 코드가 실행되면 Call Stack에 쌓이고, Stack에서는 선입후출 룰 대로 실행된다.
    • 비동기 함수가 실행된다면, Web API가 호출된다.
    • Web API는 비동기함수의 콜백함수를 Callback Queue에 밀어넣는다.
      • Promise는 Microtask Queue로, Timeout은 Task Queue로,
        RequestAnimationFrame은 Animation Frame으로 콜백함수를 밀어넣는다.
    • Event Loop는 Call Stack이 빈 상태가 되면 콜백을 Call Stack으로 이동시킨다.
      • 콜백 이동 우선순위는 Microtask Queue > Animation Frames > Task Queue 이다.

실제로 실행해보면, seTimeout과 requestAnimationFrame의 순서가 바뀌는 결과도 있었다..
분명 우선순위는 Animation Frames이 Task Queue보다 높다고 하는데 왜 그럴까?ㅠㅠ
구글링을 통해 내가 추측한 바로는... (정말 추측이다. 정확히 모르겠다.. 알려주세요..ㅠㅠ)

브라우저는 좋은 사용자 경험을 위해 초당 60번 화면을 레더링 한다고 한다.
이를 위해서는 16ms안에 1번 화면을 그려야 한다.

만약 Microtask Queue안에 있던 콜백은 다 Call Stack으로 이동되었고,
Animation Frames과 Task Queue에 모두 콜백이 있다고 가정 하자..
만약 Task Queue의 콜백이 먼저 Call Stack으로 이동되어도
16ms 안에 1번 렌더링할 수 있다면, Task Queue의 콜백이 먼저 이동되는거 아닐까?

Microtask Queue의 콜백을 많이 만들어서,
일정 시간동안 Animatation Frame과 Task Queue의 콜백이 그대로 쌓여있을 경우
Microtask Queue의 콜백이 모두 Call Stack으로 이동되었을 때,
( = Microtask Queue가 빈 상태가 될 경우)
Animation Frames에 쌓여있던 Callback이 모두 다 Stack으로 이동되어야
Task Queue에 쌓여있던 콜백이 이동한다.

즉, Animation Frames가 Task Queue보다 우선순위가 높게 작용했다.
[자세한건 7번째 링크 - 비동기 스케줄링과 Frame의 LifeCycle 확인해보세요.]


Event Queue?? Job Queue??

마지막으로 Event Queue, Job Queue라는 개념도 나온다.
Event Queue는 Task Queue와 동일한 역할을,
Job Queue는 Microtask Queue와 동일한 역할을 가지고 있다.
(자세한 내용은 하기 링크를 참고!!)

[출처][JavaScript main thread. Dissected.](https://frarizzi.science/journal/web-engineering/javascript-main-thread-dissected)

Render Queue???

진짜 마지막으로...(ㅋㅋㅋㅋ) Render Queue라는 개념도 있다..
(자세한건 출처링크에서 확인!!)
[출처][Browser Rendering Queue in-depth](https://frarizzi.science/journal/web-engineering/browser-rendering-queue-in-depth)

  • Render Queue는 브라우저에서 사용자에게 래스터 이미지를 보여주기 위해
    HTML, CSS, Javascirpt 코드를 변환하는 과정을 의미한다.
    (rendering path 혹은 critical rendering path 라고도 한다.)
  • DOM Tree -> CSS Tree -> CSSOM -> Render Tree -> Layouting
    -> Layer Tree -> Paint -> GPU Syne -> Composition 순으로 이뤄지는데,
    Composition은 스크린에 그려진 Final Frame이다.

여기서 Animation Frames와 Render Queue를 같이 설명된 자료는 찾을 수 없었다...ㅠㅠ

역시 내 추측이지만...ㅠㅠ
Render Queue에 만들어진 Composition도 Event Loop에 의해 우선순위가 결정되고,
Animation Frames에 에 담긴 Callback도 Event Loop에 의해 우선순위가 결정된다.
Render Queue와 Animation Frames은 서로 같은 개념 아닐까?


참고

profile
꾸준히, 끄적끄적 해볼게요 :)

6개의 댓글

comment-user-thumbnail
2020년 4월 5일

좋은 글 이네요!
보기쉽게 이해하기 쉽게 정리 된 글 인것 같습니다!

답글 달기
comment-user-thumbnail
2020년 5월 5일

저같은 초짜가 봐도 읽힐 정도로 기초적인 개념들도 잘 설명해주셔서 감사해요. 너무 유익한 글이네용!ㅎㅎ

답글 달기
comment-user-thumbnail
2020년 9월 18일

잘 읽고 갑니다

답글 달기
comment-user-thumbnail
2022년 4월 23일

좋은 글 감사합니다 :)
본문 중 rAF 콜백보다 task queue에 담긴 콜백이 먼저 실행되는 경우가 있다고 하셨는데요. 제가 이해한 방식으로 해당 동작이 가능한 이유를 설명드리고 싶어서요! 혹시나 제가 잘못 알고 있는 부분이 있다면 말씀해주세요🙏

rAF 콜백이 task queue에 담기는 콜백에 우선순위를 가지는 것은 아닌 것으로 알고 있습니다. 정확히는 둘 간의 우선순위는 정해져 있지 않습니다. 다만 rAF가 우선순위를 가지는 대상은 브라우저의 렌더링 스텝입니다. (리페인트 이전 호출 보장).
rAF는 기본적으로 디스플레이 주사율에 맞춰 호출되고(60fps라면 1초에 60번, 대략 16.7ms마다), 그 이상으로 호출되는 일은 없습니다. 이는 즉 rAF의 호출 시기가 환경에 따라 변할 수 있다는 말이 됩니다.
이를 미루어 보았을 때, 다음과 같은 결과를 생각할 수 있습니다. 메인 쓰레드가 새로운 프레임을 그리기위해 rAF 호출과 렌더링 과정을 실행할 때쯤에, 태스크 큐의 콜백이 먼저 호출되어 큐가 비어있을 수도 있고, 아직 큐에 남아 있는 상황일 수도 있겠죠. 태스크 큐에 담기는 콜백들은 이벤트 루프(틱) 한 번에 하나의 콜백만 스택으로 옮겨지므로 여분의 콜백이 아직 남아있을 수 있는 상황인 겁니다. 그러한 콜백들은 rAF 호출 및 렌더링(리플로우, 리페인트, 컴포지션 중 필요 과정만)을 마치고 나서야 스택으로 옮겨져 실행될 겁니다.
마이크로태스크의 경우에는 하나의 틱에서 큐가 아예 비워질때까지 모든 콜백들을 실행하므로, 그 작업들이 모두 끝나서야 새로운 프레임을 그리기 위한 작업인 렌더링과 rAF를 호출하게 될 겁니다. 예시로, promise 콜백을 재귀적으로 호출하게 되면 화면이 멈추고, setTimeout을 재귀적으로 호출하게 되면 화면이 문제없이 렌더링됩니다. (당장의 스택이 비워지면서 렌더링 과정을 수행 가능한지에 대한 여부가 서로 다릅니다.)

1개의 답글
comment-user-thumbnail
2023년 10월 17일

rAF 는 다음 렌더링이 발생하기 전에 호출할 콜백을 등록하는 API인데요 이 rFA는 이벤트 루프가 Render Queue에 방문할 때만 새로운 렌더링 이전에 실행됩니다.
이벤트 루프가 RQ MTQ TQ 와 콜스택을 돌면서 방문하는데요,
콜스택에 등록된 함수 호출이 있으면 먼저 비우게 됩니다. 이때 이벤트 루프는 루프 도는것을 멈추고 콜스택에 머무릅니다. 그러다가 콜스택이 비었을 때 RQ -> MTQ -> TQ 순으로 방문하게 되죠.
하지만 RQ는 매번 방문하지 않습니다.
브라우저는 사람의 눈에 자연스럽게 렌더를 하기 위해서 렌더 간격을 60fps정도로 구현하도록 설계되어있습니다.
그리고 60fps를 맞춰서 렌더를 그리기 위해서는 약 16.7ms마다 렌더 큐에 방문하면 되는데요,
이벤트 루프가 루프를 한번 도는데에는 1ms도 걸리지 않습니다.
그래서 이벤트 루프는 루프를 한바퀴 돌 때마다 RQ에 방문하지는 않습니다. 일정 시간 간격으로 재면서 그 시간이 지났을 때에만 방문하고 아직 시간이 남았으면 RQ는 패스하는것입니다.
그러므로 MTQ와 TQ는 루프를 한번 돌때마다 방문하지만 RQ는 매번 방문하지 않고 약 16.7ms마다 방문한다고 볼 수 있죠.

답글 달기