이벤트 루프(Event Loop)를 알면 좋은 점
1️⃣ 이벤트 루프란?
Javascript가 브라우저에서 동작하는 방식입니다. 브라우저 JS 런타임은 이벤트 루프에 기반하여 실행되며, 구성은 다음과 같습니다.
- Call Stack: 스택에 함수가 저장되었다가 순차적으로 실행된다.
- Web API 컨테이너: 브라우저에서 제공하는 API가 콜 스택에서 호출되면, 함수가 Web API 컨테이너에 저장된다. 함수 실행 시점이 되면 함수(callback)를 콜백 큐로 이동시킨다.
- Callback Queue: 콜백 큐에 저장된 함수(callback)는 이벤트 루프에 의해 순차적으로 콜 스택으로 이동한다.
- Event Loop: 콜 스택이 모두 비어있을 때 콜백 큐에 저장된 함수를 콜 스택으로 이동시킨다.
2️⃣ 왜 이벤트 루프를 만들었을까?
싱글 스레드인 Javascript가 브라우저에서 동시성(Concurrency)을 갖기 위해서 입니다.
시간을 잘게 나누어 각 시간마다 서로 다른 일을 번갈아 작업한다. 마치 동시에 여러 작업을 처리하는 것 같지만 실제로는 한 번에 하나의 일만 한다.
병렬성/평행성(Parallelism)이란?
같은 시간 동안 여러 작업을 처리하는 것으로, 멀티스레드가 병렬성을 가진다.
API를 통해 데이터를 가져오거나 이벤트 처리 등 웹 서비스에서 비동기로 처리해야 하는 작업이 있습니다. 하지만 싱글 스레드인 JS는 한 번에 하나의 일만 순차적으로 할 수 있습니다. 즉, 중간에 긴 시간을 소요하는 작업이 있다면 JS는 다음 작업으로 넘어가지 못하고, 사용자 입장에서 웹이 멈췄다고 생각할 겁니다.
console.log("시작");
runLongTask();
console.log("긴 시간 뒤에 실행");
function runLongTask() {
let count = 0;
for (let i = 0; i < 1e9; i++) {
count++;
}
}
// 시작
// (긴 시간 소요)
// 긴 시간 뒤에 실행
다행히도 Event Loop와 브라우저에서 제공하는 Web APIs 덕분에 JS는 복잡하고 긴 작업을 비동기로 처리할 수 있습니다.
3️⃣ 이벤트 루프 동작 방식
위 그림에 보이는 코드를 실행하면 다음과 같은 결과가 나옵니다.
start
end
I sec delay
JS는 콜 스택에 저장된 함수를 순차적으로 실행하며, setTimeout도 순서에 맞게 실행됩니다. 다만 Web API 함수는 콜 스택에서 빠져나와 Web API 컨테이너에 저장되며, JS는 callback이 실행되길 기다리지 않고 바로 다음 작업을 합니다. 타이머를 체크하고 delay 만큼 대기하는 작업을 브라우저가 대신하며, 대기가 끝나면 setTimeout에 연결된 callback은 콜백 큐로 이동합니다. 이때 콜 스택이 비어있다면 ,이벤트 루프에 의해 콜백 큐에 저장된 함수를 실행합니다.
4️⃣ 이벤트 루프를 알면 좋은 점
이벤트 루프 덕분에 JS가 브라우저에서 비동기 작업을 처리할 수 있다는 건 알겠습니다. 하지만 이 개념이 실전에서 어떻게 사용될 수 있을까요? 그건 콜백 큐를 더 자세히 살펴봐야 합니다.
콜백 큐는 실제로 세 가지 큐로 구성되어 있습니다.
- (Macro)Task Queue: setTimeout, setInterval, DOM, URL 등
- Animation Frame Queue
- Microtask Queue: Promise callback, Mutation observer API, await/async callback 등
Web APIs 콜백은 우선순위를 가지며, 아래로 갈 수록 우선 순위가 높습니다 (Micro > Animation > Macro). Microtask는 큐를 모두 비울 때까지 작업을 수행합니다. 즉, Microtask를 완료하기 전까지는 랜더링(Animation)과 Macrotask가 실행되지 않습니다.
각 태스크의 우선순위와 동작 방식을 알고 함수를 작성해야 웹이 멈춰버리는 문제를 줄일 수 있습니다. Microtask를 처리하느라 다른 큐의 작업이 지연되는 예시를 보면 다음과 같습니다.
// 출처: Claude로 생성 및 수정
let frameCount = 0;
function animateElement() {
const element = document.getElementById("animatedElement");
element.style.transform = `translateX(${frameCount}px)`;
frameCount++;
requestAnimationFrame(animateElement);
}
function createMicrotasks() {
Promise.resolve().then(() => {
let result = 0;
for (let i = 0; i < 1e9; i++) {
result += Math.random();
}
});
}
function badMicrotaskOverflow() {
console.log("Starting animation and creating microtasks...");
// Start the animation
animateElement();
// Create a large number of microtasks
createMicrotasks();
// Microtask 때문에 지연되는 Macrotask Queue
setTimeout(() => {
console.log("Animation frame count:", frameCount);
console.log("This message is delayed!");
}, 0);
}
badMicrotaskOverflow();
버튼이 오른쪽으로 계속 이동하는 애니메이션을 구현했습니다. 하지만 Promise의 콜백을 처리 중일 때는 translateX 작업이 지연되며, 로딩이 끝난 후에야 translateX와 setTimeout이 수행됩니다. 위 코드를 실행하면 아래 이미지에서 보이는 것처럼 Microtask를 수행하느라 버튼이 표시되지 않는 것을 확인할 수 있습니다.
작업의 지연을 막기 위해 위 코드를 수정해보겠습니다. Microtask 작업을 한 번에 처리하지 않고 나눠서 작업하면, 그 동안 Animation Frame과 Macrotask도 수행할 수 있습니다.
// 출처: Claude로 생성 및 수정
let frameCount = 0;
function animateElement() {
const element = document.getElementById("animatedElement");
element.style.transform = `translateX(${frameCount}px)`;
frameCount++;
requestAnimationFrame(animateElement);
}
function createMicrotasks(
totalIterations,
batchSize = 1000000,
currentIteration = 0
) {
return new Promise((resolve) => {
let result = 0;
const endIteration = Math.min(
currentIteration + batchSize,
totalIterations
);
for (let i = currentIteration; i < endIteration; i++) {
result += Math.random();
}
if (endIteration < totalIterations) {
// Use setTimeout to create next microtask
setTimeout(() => {
createMicrotasks(totalIterations, batchSize, endIteration);
}, 0);
console.log("Microtask created for iteration:", endIteration);
} else {
console.log("All microtasks completed!");
resolve(result);
}
});
}
function improvedMicrotaskHandling() {
console.log("Starting animation and creating microtasks...");
// Start the animation
animateElement();
// Divide a large number of microtasks
const totalIterations = 1e9;
createMicrotasks(totalIterations);
setTimeout(() => {
console.log("Animation frame count:", frameCount);
}, 0);
}
// Call the function to start the process
improvedMicrotaskHandling();
위 코드를 실행하면 버튼이 움직이면서 Microtask도 수행하고 있음을 확인할 수 있습니다.
1e9번의 작업을 모두 수행한 후에 완료 메시지도 표시되었습니다 :)
여기까지 이벤트 루프의 개념을 알아봤습니다. 우연히 책이나 강의를 통해 기술 지식을 얻고나면 '이걸 알면 뭐가 좋을까?'가 가장 궁금한데, 원티드에서는 그 부분도 다뤄줘서 좋았습니다 😊 이벤트 루프를 모르면 발생할 수 있는 문제와 이를 해결하기 위한 방법까지 정리해봤습니다.
이전에 추천드린 대로 아래 유튜브가 개념을 자세하게 다루고 있으니 한 번 보셔도 좋을 것 같습니다. 틀린 부분 있으면 댓글 부탁드립니다. 읽어주셔서 감사합니다 🙇♀️🙇
참고자료
- https://youtu.be/eiC58R16hb8?feature=shared
- https://dev.to/bipinrajbhar/how-javascript-works-web-apis-callback-queue-and-event-loop-2p3e
- https://medium.com/@burak.bburuk/what-is-the-event-loop-in-javascript-and-why-is-it-essential-to-understand-b11af520a28b
- https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
- https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth
- https://iamsjy17.github.io/javascript/2019/07/20/how-to-works-js.html