Event loop에 대해

Bitnara Lee·2021년 12월 20일
0

브라우저가 자바스크립트 코드를 실행시키는 원리에 대해 구체적으로 알아보고, 얻은 내용을 바탕으로 보다 나은 웹 퍼포먼스를 가진 서비스를 개발하고자 한다.


알아야 할 중요 개념

  • 프로세스(Process)
    실행 중인 하나의 애플리케이션
    운영체제가 프로그램의 실행을 위해 프로그램에 메모리를 할당하는 단위
  • 스레드(Thread)
    한 가지 작업을 실행하기 위해 순차적으로 실행한 코드를 실처럼 이어 놓았다고 해서 한가닥의 실이라고 명칭, 하나의 스레드는 코드가 실행되는 하나의 흐름
    한 프로세스 내에 스레드가 두 개라면 코드가 실행되는 흐름이 두 개 생긴다는 의미
  • 힙(Heap)
    객체가 저장되는 메모리 공간
    객체는 원시 값과 달리 크기가 정해져 있지 않으므로 할당해야 할 메모리 공간의 크기를 런타임에 결정(동적 할당)한다. -> 객체가 저장되는 메모리 공간인 힙은 구조화 되어 있지 않다는 특징을 가짐
    콜 스택의 실행요소인 컨텍스트(코드의 흐름(문맥)이나 함수 동작에 필요한 환경 정보가 담긴 객체)는 힙에 저장된 객체를 참조
  • 콜 스택(Call stack)
    소스코드평가 과정에서 생성된 실행 컨텍스트가 추가되고 제거되는 스택 자료구조
    함수를 호출하면 함수 실행 컨텍스트가 순차적으로 콜 스택에 푸시된다.
    선입후출의 룰을 따라 가장 마지막에 들어온 함수(최상위 실행 컨텍스트)가 가장 먼저 실행되고, 이 함수가 종료되어 콜 스택에서 제거되기 전까지는 다른 태스크가 실행되지 않는다.
  • 태스크 큐(Task queue)
    콜 스택에 푸시되기 전, setTimeour, setInterval 등과 같은 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역, 선입선출(FIFO, First In First OUT)의 룰을 따른다.

✨ Event loop

🌟 브라우저와 자바스크립트 엔진이 협력하여 비동기 함수를 처리

  • V8과 같은 자바스크립트 엔진은 단일 호출 스택을 사용하고, 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 처리할 뿐, 비동기 요청을 처리할 수 없다.
  • 비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저 또는 Node.js가 담당한다.
    (ex: setTimeout의 콜백 함수의 평가와 실행은 자바스크립트 엔진이 담당하지만 타이머 설정과 콜백 함수를 태스크 큐에 등록하는 작업은 브라우저 또는 Node.js가 담당)
  • 이를 위해 브라우저 환경은 태스크 큐(task queue)이벤트 루프(event loop)를 제공

(브라우저에 내장된) 자바스크립트 엔진은 싱글 스레드로 동작한다. 즉, 한번에 하나의 태스크만 처리한다.
but 브라우저는 멀티 스레드로 동작한다.

-> 따라서 브라우저는 자바스크립트의 비동기 처리를 돕기 위해 Web API (DOM API, 타이머 함수, HTTP 요청(Ajax) 등)를 제공할 수 있다.

🌟 Event loop, 브라우저의 메인 스레드 동작 타이밍을 관리하는 관리자

싱글 스레드에서 시간이 오래 걸리는 하나의 작업을 하고 있다면 다른 작업은 지연시키기 때문에(이를 blocking이라 한다) 결국 브라우저의 퍼포먼스에도 영향이 간다.
또한, 작업 간 전환 속도를 빠르게 하여 한 번에 하나의 작업씩 수행하지만 마치 동시에 수행하는 것처럼 동작해야 한다. ( 동시성과 병렬성 )
-> 따라서 어떤 작업을 우선으로 동작시킬 것인지 결정하고 그 타이밍을 관리하는 것이 중요.
-> 그 관리자 역할을 하는 것이 이벤트 루프이다.

이벤트 루프(event loop):

콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인
콜 스택이 비어있고, 태스크 큐에 대기 중인 함수가 있다면 순차적(FIFO, First In First Out)으로
태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킨다.
-> 이러한 반복을 tick이라고 함

Run-to-completion : 어떠한 함수가 나머지 함수들이 진행되기 전에 진행이 완료됨

🌟 Event loop이 동작하는 과정


(그림 출처 - Jake Archibald: 루프 속 - JSConf.Asia)

T : task queue
rAF : requestAnimationFrame
S : Style (렌더 트리 생성)
L : Layout
P : Paint

중앙을 기준으로 왼쪽 path가 task queue, 오른쪽 path가 브라우저 렌더링이라고 생각하면 된다. (microtask는 그림 어디에서나 실행될 수 있다고 한다.)
아직 생소한 개념들에 대해서는 밑에서 더 이야기해보겠다.

evnet loop의 동작하게 되는 환경과 그 과정을 구체적으로 정리하자면 이렇다.

  • 서버로부터 리소스를 응답받은 브라우저가 html 태그를 파싱하고 실행시키다가 script 태그를 만나면 html파싱을 멈추고 script를 파싱한다. -> script 코드를 읽기 시작 [브라우저 렌더링 과정에 대한 이전 글]
  • 이 과정에서 함수 실행 컨텍스트들(->함수라고 봐도 무방)이 콜 스택에 쌓여서 동작을 수행한다.
  • 다소 수행하는데 시간이 걸리는 Promise, setTimeout과 같은(즉, Web API에서 처리하는) 비동기 관련 콜백들이 큐에 등록되어 대기
  • stack에서는 선입 후출의 룰에 따라 가장 마지막에 들어온 함수가 먼저 실행된다.
  • stack에 쌓여있던 코드(함수)들이 모두 실행되면 큐에서 첫번째 task(비동기 콜백들)를 콜 스택에 쌓고, 해당 task가 실행된다.

사실 queue에는 한가지가 아닌 여러 종류가 존재하고 이벤트 루프는 각각의 queue에 대한 우선순위를 갖고 있다.
그 순서는 다음과 같다.

Microtask Queue > Animation Frames > task queue

각각의 큐에 대해 자세히 알아보자.

Microtask Queue

ES6에서 도입된 새로운 개념
Promise의 Promise.then 콜백이 저장되는 큐이다.
이벤트 루프가 큐들을 확인할 때 최우선 순위를 가짐
-> 스택이 비었을 때, 이벤트 루프는 우선적으로 Microtask Queue를 확인하고, promise.then 콜백이 등록되 있다면 이를 먼저 스택에 담고 실행시킨다.

Animation Frames

이벤트 루프가 큐들을 확인할 때 Microtask Queue 다음의 우선순위를 가짐
(예외적으로 input Event는 rAF보다 높은 우선순위를 가진다.(Microtask queue와 비슷한 시점) 참고 )
화면을 갱신해야 될 필요가 있을 때 (ex) 사용자가 스크롤 이동, 어떤 요소를 클릭 등)
requestAnimationFrame API를 사용하면 이것은 Animation Frames이라는 queue에 등록이 된다.
-> 이벤트 루프가 확인하고 브라우저 렌더링이 발생한다.

window.requestAnimationFrame(callback) :
애니메이션 구현을 위해 만들어진 비동기 함수로, 브라우저가 실행 시기를 결정한다. 즉, 브라우저는 모니터의 주사율에 맞추어 함수를 실행한다. 가령, 모니터의 평균 주사율이 60FPS이면 1초에 60번 함수를 실행하는 것이다.

requestAnimationFrame vs setInterval/setTimeout
콜백함수의 인자로 timestap가(밀리세컨드 단위의 시간값) 제공되기 때문에 setInterval 함수보다 편하게 애니메이션 구현가능하다.
또, setTimeout은 다른 task에 의해 지연될 가능성이 있지만 rAF는 무조건 브라우저가 렌더링될 때 함께 실행되므로(위 그림 참고) 1프레임당 1번의 호출이 보장된다.
참고블로그

Task queue

이벤트 루프가 큐들을 확인할 때 마지막의 우선순위를 가짐
setTimeout, setInterval 등과 같은 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 보관되는 큐
콜백을 하나씩 실행 -> 콜백을 하나 실행하고 이벤트 루프를 놓아주어 다른 동작 수행가능하게 함(재귀적으로 setTimeout 콜백을 Task queue에 넣어도 브라우저는 정상 작동)

Microtask Queue vs Animation Frames vs task queue의 순서를 직접 확인해보면 다음과 같다.

//1. script 실행 (log)
console.log("script start");

//2. script 실행 (setTimeout callback task queue에 등록)
setTimeout(function () {
  //11. Task 실행
  console.log("setTimeout");
}, 0);

//3. script 실행 (Promise then callback Microtask queue에 등록)
Promise.resolve()
  .then(function () {
    // 7. MicroTask 실행
    console.log("promise1");
  }) // 8. script 실행 (Promise then callback Microtask queue에 등록)
  .then(function () {
    // 9. MicroTask 실행
    console.log("promise2");
  });

//4. script 실행 (AnimationFrame Animation frames에 등록)
requestAnimationFrame(function () {
  //10. Animation Frame 실행
  console.log("animation");
});

//5. script 실행
console.log("script end");
//6. Stack의 모든 Task 실행완료
---------------------------------------------------------------
  
// 결과 
script start
script end
promise1
promise2
animation
setTimeout

주의할 점

queue가 호출되는 순서는 브라우저마다 차이가 있을 수도 있다.
promise가 ECMA Spec이므로 브라우저마다 처리하는 방식이 다르기 때문
특정 브라우저에서는 promise를 microtask로 처리하는 것이 아니라 task로 처리한다.
(이 글에서는 크롬 브라우저 위주로 다룸)


참고

https://iamsjy17.github.io/javascript/2019/07/20/how-to-works-js.html
https://tecoble.techcourse.co.kr/post/2021-08-28-event-loop/
https://www.youtube.com/watch?v=cCOL7MC4Pl0

profile
Creative Developer

0개의 댓글