프론트엔드 기본개념 복습/Javascript

[Javascript] 이벤트 루프 (Event Loop)

콘요맘떼 2022. 2. 23. 18:14

  자바스크립트는 '단일 스레드'기반 언어이다. 즉, 한 번에 하나의 작업만을 처리할 수 있다. 보다 정확히 표현하자면 자바스크립트 엔진은 '단일 콜 스택'을 활용하고 하나의 메인 스레드를 가진다. 그러나 자바스크립트가 돌아가는 실제 환경(브라우저, Node 등)은 여러 개의 스레드가 사용된다. 하나의 콜 스택을 활용하는 자바스크립트 엔진과 여러 스레드를 활용하는 실제 구동 환경이 서로 연동될 수 있도록 해주는 것이 바로 '이벤트 루프'이다. (이벤트 루프는 앞서 언급한 단일 메인 스레드를 관리해준다.) 자바스크립트는 이벤트 루프를 통해 비동기 방식으로 동시성을 지원한다. 참고로 이벤트 루프는 자바스크립트 엔진에서 지원되는 것이 아니라 브라우저 혹은 NodeJS에 의해 제공된다. (NodeJS는 libuv라는 라이브러리를 통해 이벤트 루프를 제공한다고 한다.)

 

실제 돌아가는 브라우저의 환경 (이미지 출처 : https://meetup.toast.com/posts/89)

 

Web API

브라우저에서 DOM 이벤트, AJAX, setTimeout비동기 작업을 수행할 수 있도록 지원하는 API를 의미한다. 콜스택에 쌓여있던 비동기 작업이 실행되면 자바스크립트 엔진은 해당 비동기 작업을 Web API에게 넘겨준다.

 

Task Queue

Web API가 비동기 작업을 완료하면 이후 실행될 콜백 함수들이 Task Queue에 담기게 된다. Task Queue에 담겨있던 콜백 함수들은 콜 스택이 비어있을 때 하나씩 순서대로 들어가게 된다.

 

Microtask Queue

모든 비동기 콜백 함수들이 동일한 우선순위를 가지는 것은 아니다. Microtask Queue에 담긴 Micro Task들은 일반 Task보다 높은 우선순위를 가지게 된다. 일반 Task Queue에 있는 작업들은 Microtask Queue가 텅 비었을 때만 콜 스택으로 넘어갈 수 있다. Promise의 콜백 함수들이 바로 Microtask Queue로 넘어간다.

 

※ Heap : 콜 스택과 함께 자바스크립트 엔진을 구성하며 메모리 할당이 일어나는 곳이다. 우리가 선언한 변수, 함수 등이 담겨져 있다.

 

동작방법 정리

1. 콜 스택이 비기 전까지는 어떠한 작업도 콜 스택으로 넘어올 수 없다.

2. 콜 스택의 작업을 처리하는 도중에 비동기 작업이 나온다면 자바스크립트 엔진은 해당 작업을 Web API에게 위임한다.

3. 해당 비동기 작업이 완료되면 그 콜백 함수는 이벤트 루프를 통해 Task Queue에 들어간다. 여기서 Promise.then( )의 콜백은 우선순위가 높은 Microtask Queue에 등록되고 그 외는 일반 Task Queue에 등록된다.

4. 콜 스택이 텅 비었다면 이벤트 루프는 우선적으로 Microtask Queue를 살펴본다. 만약 Microtask Queue에 작업들이 있다면 해당 Queue가 텅 빌 때까지 작업들을 실행한다.

5. Microtask Queue가 텅 비었다면 이번에는 일반 Task Queue를 확인한다. 일반 Task Queue는 Microtask Queue와 다르게 하나의 콜백을 실행한 후 이벤트 루프를 다시 놓아준다. 그렇기 때문에 다른 작업들도 수행할 수 있으며 Microtask Queue에 담긴 작업들의 우선순위가 보장될 수 있다.

 

 

Animation Frames

여기서 끝이 아니다. 화면 갱신을 요청하는 requestAnimationFrame API를 실행하면 브라우저에게 원하는 애니메이션을 지시할 수 있다. requestAnimationFrame API가 실행되면 우리가 원하는 애니메이션 업데이트가 담긴 콜백 함수가 Animation Frames에 담기게 된다. 요청된 애니메이션은 다음 리페인트 전에 업데이트가 이루어져야 한다. 따라서 이벤트 루프는 최대한 모니터 주사율 안에 맞춰서 렌더링 파이프라인에 진입하고자 노력한다.

 

 

결국 총 세 가지 유형의 콜백 함수들이 등장하였다.

(1) 일반적인 비동기 작업들의 콜백 함수들이 담기는 일반 Task Queue

(2) Promise.then( )의 콜백함수들이 담기는 Microtask Queue

(3) requestAnimationFrame API에 의해 실행되어 화면을 갱신시키는 작업들이 담긴 Animation Frames

 

이들의 우선순위는

Microtask Queue > Animation Frames > 일반 Task Queue

순서로 적용된다.

 

 

응용 1 - setTimeout vs requestAnimationFrame

  따라서 웹에서 애니메이션을 보여주고자 한다면 사용자 경험의 관점에서 setTimeout보다 window.requestAnimationFrame을 활용하는 것이 권장된다. 후자는 1프레임당 호출이 보장되는 반면에 전자는 다른 task들에 의해 지연될 수 있기 때문에 1프레임당 1호출이 보장되지 않기 때문이다.

 

 

응용 2 - setTimeout(fn, 0)

  setTimeout(fn, 0)은 언뜻 보면 곧바로 콜백 함수를 실행할 것처럼 보이지만 실상은 그렇지 않다. setTimeout의 콜백 함수는 곧바로 콜 스택에 올라가는 것이 아니라 Task Queue에 들어가기 때문이다. 또한 실제로 작용하는 delay는 무조건적으로 0초 이상이 적용된다. 왜냐하면 브라우저마다 setTimeout의 콜백이 실행되기까지 최소 단위의 시간이 존재하기 때문이다. 예를 들어서 크롬 브라우저에는 4ms의 최소 단위가 적용된다. 그렇기 때문에 4ms 미만의 delay를 지정해도 실제로는 최소 4ms의 딜레이가 적용된다는 것이다. (최소라고 표현한 이유는 해당 task가 콜 스택으로 바로 올라오지 못할 수 있기 때문이다.)

  다시 본론으로 돌아와서 delay를 0으로 지정하고 내부 콜백 함수에 시간이 오래 걸리는 작업을 넣어둔다면 적절한 순간까지 이벤트 루프를 활용하여 해당 작업을 딜레이시킴으로써 전체 어플리케이션이 동작이 중단되는 등의 문제를 예방할 수 있다.