10월 초부터 스터디를 통해 '모던 자바스크립트 딥다이브' 완독에 도전하고 있다.
비동기 프로그래밍 챕터에서 이벤트 루프에 대한 설명을 보고 좀 더 깊게 알아보고 싶어서 정리한 내용이다.
이벤트 루프에 대해 알아보기 전에, 자바스크립트의 동시성 개념에 대해 알 필요가 있다.
자바스크립트는 싱글 스레드 언어이다. 즉, 한 번에 하나의 태스크만 처리할 수 있다는 것을 의미한다.
그러나 브라우저를 사용할 때를 떠올려보면, HTTP 요청을 통해 다른 데이터를 가져오면서도 동시에 UI를 렌더링하거나, 사용자 입력 이벤트를 처리하는 등 많은 작업들이 동시에 처리되는 것처럼 보인다.
이러한 ‘동시에 실행되는 것처럼 보이는’ 작업들은 자바스크립트의 동시성을 통해 이루어진다.
이처럼, 동시성은 작업(task)들이 빠르게 전환하면서 실행되어, 동시에 실행되는 것처럼 보이는 것을 말한다.
자바스크립트는 동시성을 보장하기 위해 비동기 처리와 Non-blocking I/O 작업을 적극적으로 활용한다.
그렇다면, 이벤트 루프는 어떻게 자바스크립트의 동시성을 지원하는 것일까?
자바스크립트 엔진은 오로지 소스코드의 평가와 실행만을 담당한다.
실제로 비동기 작업들을 스케줄링하고 실행하는 것은 자바스크립트를 구동하는 런타임 환경(브라우저 또는 Node.js)의 역할이다.
브라우저는 자바스크립트 엔진과 별개로 Web API를 제공한다. Web API는 브라우저에서 제공하는 다양한 기능들의 집합이다.
이러한 Web API들은 자바스크립트 엔진의 메인 스레드와는 별개로 브라우저의 다른 스레드에서 실행된다.
즉, 자바스크립트 엔진은 싱글 스레드 방식으로 동작하지만, 이를 구동하는 환경인 브라우저는 멀티 스레드 방식으로 동작하기 때문에 동시성을 지원할 수 있다.
이벤트 루프는 다음과 같은 과정을 반복한다.
이벤트 루프의 동작 과정
즉, 이벤트 루프는 ‘비동기 함수들을 적절한 시점에 실행시키는 관리자’라고 볼 수 있다.
setTimeout 메서드를 사용한 예제를 보자.
console.log("Start");
setTimeout(() => {
console.log("Timeout Callback");
}, 1000);
console.log("End");
위 코드는 다음과 같이 실행된다.
setTimeout
은 비동기 작업으로, 자바스크립트 엔진은 이 작업을 Web api에게 위임한다.만약 delay를 0초로 바꾸면 출력 결과가 달라질까?
console.log("Start");
setTimeout(() => {
console.log("Timeout Callback");
}, 0);
console.log("End");
출력 결과는 달라지지 않는다.
그 이유는 delay가 0초라고 해도, setTimeout의 콜백 함수는 콜 스택이 비워진 이후에야 실행될 수 있기 때문이다.
그렇다면, setTimeout의 인자로 넘겨주는 delay는 정확한 시간일까? 그렇지 않다.
만약 delay만큼의 시간이 지나고 setTimeout의 콜백 함수가 태스크 큐에 넘어갔더라도, 콜 스택에 매우 많은 양의 함수가 쌓여있다면 그 함수들의 실행이 모두 완료될 때까지(콜 스택이 비워질 때까지) 실행이 지연될 수 있기 때문이다.
따라서, setTimeout의 인자로 넘겨주는 delay는 콜백 함수의 실행을 지연시킬 최소 시간이라고 할 수 있다.
이벤트 루프에서 처리되는 ‘작업’에는 두 가지 종류가 있다.
setTimeout
, setInterval
, I/O 작업, 네트워크 요청 등의 비동기 작업이 포함된다.Promise.then()
, Promise.catch()
, async/await
에서의 코드매크로태스크와 마이크로태스크는 각각 매크로태스크 큐
, 마이크로태스크 큐
에서 관리된다.
이벤트 루프는 콜 스택이 비면, 먼저 마이크로태스크 큐에서 대기하고 있는 모든 함수를 가져와 실행한다.
이후 마이크로태스크 큐가 비면, 매크로태스크 큐에서 대기하고 있는 함수를 가져와 실행한다.
이처럼 마이크로태스크는 다른 매크로태스크가 실행되기 전에 처리된다.
이런 처리순서가 아주 중요한 이유는 (마우스 좌표 변경이나 네트워크 통신에 의한 데이터 변경 같이 애플리케이션 환경에 변화를 주는 작업에 영향을 받지 않고) 모든 마이크로태스크를 동일한 환경에서 처리할 수 있기 때문이다.
(출처: 모던 JavaScript 튜토리얼)
코드와 함께 실행 흐름을 정리해보면 다음과 같다.
console.log("Start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve()
.then(() => {
console.log("Promise 1");
})
.then(() => {
console.log("Promise 2");
});
console.log("End");
막연히 ‘setTimeout
을 사용하면 함수 호출을 지연할 수 있다’ 정도로만 알고 있었는데, 그 과정에 대해 자세하게 알고 나니 전체적인 흐름이 눈에 그려졌다. 단순히 책을 읽고 마는 것이 아니라 궁금한 부분을 더 찾아보고 정리하면서 학습하니 훨씬 더 재미있게 느껴지기도 했다.
브라우저의 렌더링 과정 또한 이벤트 루프와 관련이 있다고 하는데, 아직 이 부분까지는 자세히 보지 못해서 나중에 한번 더 보고 업데이트할 예정이다.