자바스크립트의 비동기 작업 처리 방식과 런타임에서 이벤트 루프가 어떻게 동작하는지 정리한 글

자바스크립트는 싱글 스레드 런타임을 가진다.
한 번에 하나의 콜 스택만 가지고 있고, 하나의 프로그램은 한 코드만 실행할 수 있다는 뜻이다. 그런데 우리는 타이머, 네트워크 요청, 이벤트 핸들러가 동시에 동작하는 것처럼 느낀다.
이것이 가능한 이유가 바로 이벤트 루프다.
이벤트 루프는 Call Stack, Web APIs, Callback Queue를 조합해 싱글 스레드 환경에서 비동기 작업을 처리한다.

위 그림은 브라우저 환경에서 자바스크립트가 어떻게 논블로킹으로 동작하는지 보여준다.
자바스크립트 엔진 자체는 싱글 스레드지만, 브라우저(또는 Node.js)가 제공하는 Web API, Callback Queue, Event Loop를 통해 동시성을 흉내 낸다.

런타임은 크게 두 영역으로 구성된다.
콜 스택은 실행 순서를 기억하는 자료구조다.
함수를 호출하면 스택에 쌓이고, 함수가 return되면 스택 최상단에서 꺼낸다.
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
const squared = square(n);
console.log(squared);
}
printSquare(4);위 코드를 실행하면 콜 스택은 아래 순서로 쌓이고 비워진다.

printSquare(4) 추가
→ square(4) 추가
→ multiply(4, 4) 추가 → return → 제거
→ square return → 제거
→ console.log(16) 추가 → 출력 → 제거
→ printSquare return → 제거
블로킹은 느리게 동작하는 코드가 콜 스택을 점유하고 있는 상태를 말한다.
네트워크 요청, 이미지 처리 같은 무거운 작업이 스택에 남아있으면 브라우저는 해당 작업이 끝날 때까지 아무것도 하지 못한다. 렌더링도, 사용자 입력도 전부 멈춘다.
이를 해결하기 위해 비동기 콜백을 사용한다.
console.log("First");
setTimeout(() => {
console.log("Timeout!");
}, 5000);
console.log("Second");실행 결과는 First → Second → Timeout! 순이다. setTimeout이 5초를 기다리는 동안 나머지 코드가 먼저 실행된다.
콜 스택 입장에서 보면 setTimeout은 스택에 추가됐다가 곧바로 사라지고, 5초 뒤에 갑자기 콜백이 스택에 올라온다. 어떻게 가능한 걸까?

setTimeout은 자바스크립트 엔진이 아닌 브라우저(Web API) 가 제공하는 기능이다.
setTimeout이 호출되면 타이머 실행을 브라우저에 위임하고 콜 스택에서는 즉시 제거된다. 그 사이 다른 코드가 계속 실행된다.

브라우저에서 타이머가 완료되면 콜백 함수를 Callback Queue(Task Queue) 에 넣는다.
Callback Queue는 실행 대기 중인 콜백들이 줄 서는 곳이다.
이벤트 루프는 딱 하나의 일을 반복한다.
Call Stack이 비어 있으면, Callback Queue의 첫 번째 콜백을 꺼내 Call Stack에 올린다.
while (true) {
if (callStack.isEmpty() && callbackQueue.hasTask()) {
callStack.push(callbackQueue.shift());
}
}
이 단순한 루프 덕분에 싱글 스레드 환경에서 비동기가 가능해진다.
setTimeout(fn, 0)은 0ms 후에 실행하라는 의미지만, 실제로는 콜 스택이 완전히 비워진 뒤에 실행된다.
console.log("start");
setTimeout(() => {
console.log("timeout");
}, 0);
console.log("end");
// 출력: start → end → timeout이 특성을 이용해 현재 실행 중인 동기 작업이 끝난 직후로 특정 작업을 미루는 패턴에 활용된다.
브라우저는 렌더링도 일종의 큐(Render Queue)를 통해 처리하는데, 콜 스택에 느린 작업이 쌓이면 렌더링도 함께 지연된다.
스크롤 이벤트처럼 짧은 시간에 수백 번 발생하는 이벤트를 별다른 처리 없이 사용하면 Callback Queue가 넘쳐 성능 저하로 이어진다.
이를 방지하기 위한 패턴으로 debounce, throttle이 있다.
// throttle — 일정 간격마다 한 번만 실행
window.addEventListener("scroll", throttle(handleScroll, 100));
// debounce — 마지막 이벤트 후 일정 시간이 지나면 실행
window.addEventListener("resize", debounce(handleResize, 300));Callback Queue 외에 Microtask Queue라는 별도의 큐도 존재한다.
Promise.then, queueMicrotask, MutationObserver 콜백이 여기에 쌓인다.
이벤트 루프는 Callback Queue보다 Microtask Queue를 먼저 처리한다.
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");
// 출력: start → end → promise → timeout실행 우선순위는 다음과 같다.
Call Stack → Microtask Queue → Render Queue → Callback Queue (Task Queue)
Promise 기반 비동기 코드가 setTimeout보다 먼저 실행되는 이유가 여기에 있다.
| 구성 요소 | 역할 |
|---|---|
| Call Stack | 함수 실행 순서 관리 (LIFO) |
| Web API | 비동기 작업 위임 처리 (타이머, 네트워크 등) |
| Callback Queue | Web API 완료 후 콜백 대기열 |
| Microtask Queue | Promise 등 고우선순위 콜백 대기열 |
| Event Loop | Stack이 비면 Queue에서 콜백을 꺼내 Stack에 올림 |
자바스크립트가 싱글 스레드임에도 비동기를 처리할 수 있는 건, 엔진 자체가 아닌 런타임 환경(브라우저/Node.js) 이 제공하는 구조 덕분이다.
- YouTube
Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.