# JavaScript 이벤트 루프 설명: 비동기 동작의 핵심 원리
Table of Contents
이벤트 루프(Event Loop)는 JavaScript가 단일 스레드 언어임에도 불구하고 비동기 작업을 효율적으로 처리할 수 있게 해주는 핵심 메커니즘입니다. 이벤트 루프를 제대로 이해하면 JavaScript의 비동기 동작을 완벽히 파악하고, 성능 문제를 진단하며, 더 효율적인 코드를 작성할 수 있습니다.
기본 개념
JavaScript는 기본적으로 단일 스레드(Single Thread) 언어이지만, 이벤트 루프와 비동기 API를 통해 동시성(Concurrency)을 구현합니다.
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
// 출력 결과:
// Start
// End
// Timeout
왜 setTimeout의 지연 시간이 0임에도 불구하고 마지막에 실행될까요? 이것이 바로 이벤트 루프의 작동 방식 때문입니다.
:::important 핵심 개념 JavaScript 런타임은 콜 스택(Call Stack), 태스크 큐(Task Queue), 그리고 **이벤트 루프(Event Loop)**로 구성됩니다. 이벤트 루프는 콜 스택이 완전히 비어있을 때만 큐에 대기 중인 작업을 가져와 실행합니다. :::
콜 스택 (Call Stack)
콜 스택은 함수 호출을 기록하는 데이터 구조로, LIFO(Last In, First Out) 방식으로 작동합니다.
function first() {
console.log('first 시작');
second();
console.log('first 종료');
}
function second() {
console.log('second 시작');
third();
console.log('second 종료');
}
function third() {
console.log('third 실행');
}
first();
// 콜 스택 상태 변화:
// 1. [first]
// 2. [first, second]
// 3. [first, second, third]
// 4. [first, second] (third 완료 후 제거)
// 5. [first] (second 완료 후 제거)
// 6. [] (first 완료 후 제거)
// 출력:
// first 시작
// second 시작
// third 실행
// second 종료
// first 종료
스택 오버플로우 (Stack Overflow)
function recursiveFunction() {
recursiveFunction(); // 무한 재귀 호출
}
recursiveFunction();
// Uncaught RangeError: Maximum call stack size exceeded
해결 방법: 비동기 처리를 통해 스택을 비우고 다시 실행하도록 변경합니다.
function safeRecursive(count = 0) {
if (count > 1000) return;
setTimeout(() => {
console.log(count);
safeRecursive(count + 1);
}, 0);
}
safeRecursive(); // 스택 오버플로우 없이 실행됨
태스크 큐 (Task Queue / Callback Queue)
setTimeout, setInterval 등 비동기 작업의 콜백 함수가 대기하는 곳입니다.
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
setTimeout(() => {
console.log('3');
}, 100);
console.log('4');
// 출력: 1, 4, 2, 3
// 실행 순서:
// 1. console.log('1') 실행 (콜 스택)
// 2. setTimeout 콜백 등록 (Web API로 이동)
// 3. setTimeout 콜백 등록 (Web API로 이동)
// 4. console.log('4') 실행 (콜 스택)
// 5. 콜 스택이 비었으므로, 첫 번째 setTimeout 콜백 실행 (태스크 큐 -> 콜 스택)
// 6. 100ms 후, 두 번째 setTimeout 콜백 실행
:::tip 타이머의 정확성
setTimeout(fn, 0)은 즉시 실행을 보장하지 않습니다. 브라우저 정책에 따라 최소 지연 시간(약 4ms)이 존재하며, 무엇보다 콜 스택이 비워져야만 실행될 수 있습니다.
:::
마이크로태스크 큐 (Microtask Queue)
Promise의 .then(), .catch(), .finally() 핸들러나 MutationObserver 같은 마이크로태스크는 일반 태스크보다 더 높은 우선순위를 가집니다.
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// 출력: 1, 4, 3, 2
// 마이크로태스크(Promise)가 태스크(setTimeout)보다 먼저 실행됩니다.
마이크로태스크 vs 태스크 실행 순서
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
})
.then(() => {
console.log('Promise 2');
});
setTimeout(() => {
console.log('setTimeout 3');
}, 0);
console.log('Script end');
// 출력 순서:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout 1
// setTimeout 3
// setTimeout 2
:::important 우선순위 규칙 이벤트 루프는 다음 순서로 작업을 처리합니다.
- 콜 스택의 모든 동기 코드 실행
- 마이크로태스크 큐의 모든 작업 실행 (중간에 추가된 것도 포함)
- 태스크 큐에서 하나의 작업 실행
- 렌더링 업데이트 (필요한 경우)
- 2번으로 돌아가 반복 :::
Web APIs
브라우저가 제공하는 비동기 API들로, JavaScript 엔진 외부에서 실행됩니다.
// 1. setTimeout / setInterval
const timerId = setTimeout(() => {
console.log('1초 후 실행');
}, 1000);
clearTimeout(timerId); // 취소 가능
// 2. fetch (네트워크 요청)
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => console.log(data));
// 3. DOM 이벤트
document.addEventListener('click', (event) => {
console.log('클릭됨:', event.target);
});
requestAnimationFrame
화면 갱신(렌더링) 주기에 맞춰 실행되는 특별한 큐입니다. 브라우저가 화면을 그리기 직전에 실행됩니다.
console.log('1');
requestAnimationFrame(() => {
console.log('2 - rAF');
});
setTimeout(() => {
console.log('3 - setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('4 - Promise');
});
console.log('5');
// 일반적인 출력: 1, 5, 4, 2, 3
// (브라우저와 상황에 따라 rAF와 setTimeout의 순서는 달라질 수 있습니다)
Node.js의 이벤트 루프 (process.nextTick)
Node.js 환경에는 마이크로태스크보다도 더 높은 우선순위를 가진 process.nextTick 큐가 존재합니다.
console.log('1');
process.nextTick(() => {
console.log('2 - nextTick');
});
Promise.resolve().then(() => {
console.log('3 - Promise');
});
setTimeout(() => {
console.log('4 - setTimeout');
}, 0);
console.log('5');
// 출력: 1, 5, 2, 3, 4
// nextTick이 Promise보다 항상 먼저 실행됩니다.
정리
이벤트 루프는 JavaScript 비동기 프로그래밍의 핵심 엔진입니다.
- 콜 스택: 동기 코드가 실행되는 곳입니다.
- 마이크로태스크 큐: Promise 등이 대기하며, 콜 스택이 비면 가장 먼저, 모두 실행됩니다.
- 태스크 큐: setTimeout 등이 대기하며, 마이크로태스크가 다 비워진 후 실행됩니다.
- 렌더링: 브라우저는 적절한 시점에 화면을 업데이트합니다.
이 우선순위를 이해하면 복잡한 비동기 로직의 실행 순서를 정확히 예측하고 제어할 수 있습니다.