이벤트 루프 — JavaScript가 비동기를 처리하는 방법
이 토픽을 마치면
JavaScript가 싱글 스레드이면서 비동기를 처리할 수 있는 원리를 이해하게 됩니다.
싱글 스레드인데 어떻게?
JavaScript는 한 번에 하나의 작업만 실행합니다. 싱글 스레드입니다. 그런데 타이머, 네트워크 요청, 클릭 이벤트를 동시에 처리합니다. 어떻게?
비밀은 **이벤트 루프(Event Loop)**에 있습니다. JavaScript 엔진(V8)이 혼자 하는 게 아닙니다. 브라우저(또는 Node.js)가 도와줍니다.
세 가지 구성 요소
콜 스택(Call Stack): 현재 실행 중인 함수가 쌓이는 곳입니다. 함수를 호출하면 쌓이고, 실행이 끝나면 빠집니다. LIFO(Last In, First Out) 구조입니다.
Web API: 브라우저가 제공하는 기능입니다. setTimeout, fetch, DOM 이벤트 리스너 등. JavaScript 엔진 밖에서 동작합니다.
태스크 큐(Task Queue): Web API에서 완료된 콜백이 대기하는 줄입니다. 콜 스택이 비면 여기서 하나씩 꺼내서 실행합니다.
실행 순서
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");출력: 1, 3, 2.
0ms 타이머인데도 "2"가 마지막입니다. 흐름을 따라가면:
console.log("1")→ 콜 스택에서 실행 → 출력setTimeout→ Web API에 타이머 등록 → 콜 스택에서 빠짐console.log("3")→ 콜 스택에서 실행 → 출력- 콜 스택 비어있음 → 이벤트 루프가 태스크 큐 확인 → 콜백 실행 → "2" 출력
setTimeout(fn, 0)은 "지금 당장"이 아니라 "콜 스택이 비면 그때"라는 뜻입니다.
마이크로태스크 큐
Promise의 .then()은 태스크 큐가 아니라 **마이크로태스크 큐(Microtask Queue)**에 들어갑니다. 마이크로태스크는 태스크보다 먼저 실행됩니다.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");출력: 1, 4, 3, 2.
콜 스택이 비면 이벤트 루프는:
- 먼저 마이크로태스크 큐를 전부 비웁니다 (Promise 콜백)
- 그 다음 태스크 큐에서 하나를 꺼냅니다 (setTimeout 콜백)
async/await도 내부적으로 Promise를 쓰므로 마이크로태스크입니다.
콜 스택을 오래 점유하면
while (true) {
// 무한 루프
}콜 스택이 비지 않으면 이벤트 루프가 돌지 않습니다. 클릭해도 반응 없고, 타이머도 실행 안 됩니다. 브라우저가 "이 페이지가 응답하지 않습니다"를 표시합니다.
무거운 계산을 해야 하면 Web Worker를 쓰거나, 작업을 쪼개서 콜 스택을 주기적으로 비워줘야 합니다.
핵심
JavaScript는 싱글 스레드지만, 브라우저의 Web API와 이벤트 루프로 비동기를 처리합니다.
setTimeout(fn, 0)은 "콜 스택이 비면 실행"이라는 뜻입니다. **마이크로태스크(Promise)**는 태스크(setTimeout)보다 항상 먼저 실행됩니다.