momo
6 min readMay 29, 2022

--

Browser Event Loop

안녕하세요! 2주만에 글로 인사드리네요 ㅎㅎ 저번주는 몸이 너무 안 좋아서.. 포스팅을 하지 못했기 때문에 더욱더 알찬 포스팅으로 인사드리고자 합니다!

바로 보시죠!

들어가기 전

  • 프로세스

운영체제가 프로그램의 실행을 위해 프로그램 메모리를 할당하는 단위 정도로 이해하시면 좋을 것 같습니다.

  • 스레드

‘Javascript는 싱글 스레드이다’ 라는 말을 많이 접하셨을텐데,

스레드는 기본적으로 프로세스가 할당받은 메모리를 실행하는 단위입니다.

하나의 프로세스가 여러 스레드로 나뉠 수 있습니다.

  • task queue

콜 스택에 들어가기 전에 setTimeout, 사용자 이벤트 콜백 등이 저장되는 큐입니다.

  • microtask queue

Promise.then 콜백이 저장되는 큐입니다.

Event Loop

브라우저 메인 스레드 동작 타이밍을 관리하는 관리자입니다. 메인 스레드는
rendering(화면을 그리는 작업) + process js code (js코드실행) 를 담당하는데 브라우저의 주된 동작이 수행되는 스레드로 생각하시면 될 것 같네요.

그렇다면 EventLoop는 왜 중요할까요?

답은 브라우저 동작의 대부분이 메인 스레드에서 싱글 스레드로 실행되기 때문입니다.

WEB API에서 제공하는 fetch, setTimeout과 같은 비동기 함수들과 워커 종류를 제외한 대부분의 자바스크립트 코드와 렌더링은 메인 스레드에서 실행됩니다.

여기서 메인 스레드가 싱글 스레드로 동작하는 것이 중요한 이유는. 싱글 스레드에서 하나의 작업을 하고 있다면 다른 작업은 동시성을 갖지 못하고 지연되기 때문인데요. 이때문에 Javascript는 동기식 언어 즉, 한 번에 하나의 작업을 수행하게 됩니다. 단일 스레드(싱글 스레드)이자 동기성을 가지고 있습니다.

대표적으로 크롬 개발자 도구에서 while(1){}을 입력해보면, 위와 같은 이유 때문에 브라우저가 먹통이 되는 현상을 쉽게 경험할 수 있습니다.

때문에 브라우저의 싱글 스레드 관리는 매우 중요하게 됩니다. 이를 담당하는 것이 바로 이벤트 루프라 생각하시면 될 것 같네요.

메인 스레드와 같은 싱글 스레드에선 하나의 작업이 오래 실행되어선 안되고, 여러 작업 중 어떤 작업을 우선으로 동작시킬지 결정하는 과정도 매우 중요하게 다뤄져야합니다. 실제로 작업 간 전환 속도를 빠르게 하여 한 번에 하나의 작업씩 수행되는 느낌이 아닌 동시에 수행되는 것처럼 느껴지게 동작하게 됩니다.

조금더 깊게 살펴보시죠.

화면에 나타나는 섹션은 각각 다음을 나타냅니다.

T: Task Queue

rAF: requestAnimationFrame

S: Style
L: Layout
P: Paint

이벤트루프는 첫째, 초기 콜 스택에 쌓여있는 task를 모두 처리합니다. 처음 html을 가져오고 script 태그를 만나는 순간에 브라우저는 동작을 잠깐 멈추고 js 코드를 읽기 시작합니다. JS가 DOM 트리를 수정할 수 있기 때문인데요. 이 과정에 코드들이 콜 스택에 올라가 동작을 수행하게 되고 Promise나 setTimeout과 같은 비동기 관련 콜백들이 큐에 등록되게 됩니다.

이후 Promise.then 콜백이 microtask queue에 등록되어 있다면 실행됩니다. 처음 콜 스택에 있는 코드들이 모두 실행되고 난 후부터는 Promise가 가장 높은 우선순위를 차지합니다. Promise.then 콜백이 등록된 microtask queue는 특징이 하나 있는데, queue에 등록된 모든 콜백이 처리될 때까지 계속 수행한다는 것입니다.

function loop() {
function infinityThen() {
Promise.resolve().then(infinityThen);
}
Promise.resolve().then(infinityThen);
}
loop();

Promise가 resolve 될 때 Promise.then 콜백을 재귀적으로 등록하면 앞서 예시와 비슷하게 브라우저는 먹통이 되어버리는데요. Promise를 처리하느라 바쁜 나머지 다른 작업을 아무것도 못 하게 되는 것입니다.

그 다음, 화면 갱신이 필요하다면 렌더링 파이프라인으로 이동하게 됩니다.

화면 갱신이 필요하다면 이라고 했는데 이는 이벤트 루프가 판단하는 것입니다. 사용자가 스크롤 이동을 했거나, 어떤 요소를 클릭했거나 등등 화면을 갱신해야 될 필요가 있다면 렌더링 파이프라인으로 이동합니다. (그림 1에서 오른쪽 path)

이후 드디어 setTimeout과 사용자 이벤트 콜백이 실행됩니다. Promise와는 다르게 task queue는 콜백을 하나 실행하고 이벤트 루프를 놓아주어 다른 동작을 수행할 수 있도록 합니다. 앞서 Promise의 예시와 같이 재귀적으로 setTimeout 콜백을 task queue에 집어넣어도, 브라우저는 정상 작동합니다.

requestAnimationFrame vs setTimeout

지금까지 이벤트 루프를 알아보았습니다. 생각보다 복잡한 일을 하는 녀석인데 마지막으로 rAF와 setTimeout의 동작 차이를 알아보며 마무리해보시죠.

rAF와 setTimeout의 가장 큰 차이점은 1프레임 당 호출이 보장되느냐 되지 않느냐의 차이가 있습니다. 흔히 웹에서 애니메이션을 보여주기 위해 setTimeout 대신 rAF 사용을 권장합니다. 그 이유는 애니메이션을 위해 setTimeout을 16ms마다 동작하도록 코드를 작성하여도, 다른 task에 의해서 지연될 가능성이 있어 1프레임 당 1번의 호출이 보장되지 않기 때문입니다.

그림 1을 보면 setTimeout은 task queue에 올라가 동작하고, rAF는 렌더링 파이프라인과 붙어 동작하는 것을 확인할 수 있습니다. 이런 구조 상 rAF는 무조건 1프레임 당 1번의 호출이 보장되고 setTimeout은 지연되어 2프레임 당 1번, 또는 3프레임 당 1번 호출될 가능성도 있습니다. 이런 현상은 버벅거리는 애니메이션을 제공하여 사용자 경험을 떨어뜨리기 때문에 지양하는 것이 좋겠습니다.

Ref:

--

--