

JavaScript는 '싱글 스레드' 언어이지만, 실행 환경(브라우저의 Web APIs, Node.js의 libuv) 덕분에 비동기 작업이 가능합니다. 이 모델은 다음 네 가지 핵심 요소로 구성됩니다.
콜 스택 (Call Stack)
push되어 실행되고, 실행이 끝나면 pop되어 사라집니다.백그라운드 (Web APIs / libuv)
setTimeout, fetch (네트워크 요청), 파일 I/O 등의 비동기 함수가 호출되면, 실제 작업은 이곳으로 위임됩니다.태스크 큐 (Task Queue) = 콜백 큐 (Callback Queue)
이벤트 루프 (Event Loop)
핵심 요약:
콜 스택에 함수가 쌓이고, 비동기 함수는 백그라운드로 작업을 위임합니다.
작업이 끝나면 콜백이 태스크 큐(= 콜백 큐)로 들어가고, 이벤트 루프는 콜 스택이 비었을 때 그 콜백을 스택에 올려 실행합니다.
이 구조 덕분에 싱글 스레드임에도 불구하고 논블로킹(Non-blocking) 작업이 가능합니다.
아래 코드를 통해 각 구성 요소가 어떻게 상호작용하는지 시간 순으로 분석해 보겠습니다.
console.log("시작"); // 1번
// 2번
setTimeout(() => {
console.log("1000ms 후 실행");
}, 1000);
// 3번
setTimeout(() => {
console.log("2000ms 후 실행");
}, 2000);
console.log("끝"); // 4번
Step 1: console.log("시작") 실행
console.log("시작")이 콜 스택에 push -> 실행 -> pop 됩니다.시작| Call Stack | Web APIs | Callback Queue |
|---|---|---|
console.log("시작") | (비어있음) | (비어있음) |
Step 2 & 3: setTimeout 등록
setTimeout 함수들이 차례로 콜 스택에 push 됩니다.pop 됩니다.| Call Stack | Web APIs | Callback Queue |
|---|---|---|
setTimeout | Timer(1000ms), Timer(2000ms) | (비어있음) |
Step 4: console.log("끝") 실행
console.log("끝")이 콜 스택에 push -> 실행 -> pop 됩니다.끝| Call Stack | Web APIs | Callback Queue |
|---|---|---|
| (비어있음) | Timer(1000ms), Timer(2000ms) | (비어있음) |
Step 5: 1000ms 경과 시점
push 합니다.pop 됩니다.1000ms 후 실행| Call Stack | Web APIs | Callback Queue |
|---|---|---|
() => { console.log(...) } | Timer(2000ms) | (비어있음) |
Step 6: 2000ms 경과 시점
push -> 실행 -> pop.2000ms 후 실행setTimeout의 지연 시간은 '최소' 보장 시간이다setTimeout(callback, 1000)은 "정확히 1000ms 후에 콜백을 실행하라"는 의미가 아닙니다. "최소 1000ms가 지난 후에 콜백을 큐에 넣어라"는 의미입니다.
만약 콜 스택에 오래 실행되는 동기 코드가 있다면, 1000ms가 지나 콜백이 큐에 도착했더라도 콜 스택이 비워질 때까지 계속 대기해야 합니다.
태스크 큐는 실제로는 두 종류로 나뉘며, 마이크로태스크 큐가 항상 우선순위가 높습니다.
setTimeout, setInterval, setImmediate, I/O 작업, UI 렌더링 등. 일반적인 '태스크 큐'를 의미합니다.Promise.then/catch/finally, process.nextTick (Node.js), queueMicrotask 등.이벤트 루프의 정확한 실행 순서:
1. 콜 스택에서 하나의 매크로태스크를 실행합니다. (최초 실행 시에는 전역 코드)
2. 콜 스택이 비워지면, 마이크로태스크 큐에 있는 모든 작업을 전부 실행합니다.
3. 하나의 매크로태스크를 큐에서 꺼내와 실행합니다.
4. 다시 2번으로 돌아가 반복합니다.
console.log("1. 동기 코드 시작");
// 매크로태스크 큐에 등록
setTimeout(() => {
console.log("5. setTimeout 콜백 (매크로태스크)");
}, 0);
// 마이크로태스크 큐에 등록
Promise.resolve().then(() => {
console.log("3. Promise 콜백 (마이크로태스크)");
});
// 마이크로태스크 큐에 등록
queueMicrotask(() => {
console.log("4. queueMicrotask 콜백 (마이크로태스크)");
});
console.log("2. 동기 코드 끝");
출력 결과:
1. 동기 코드 시작
2. 동기 코드 끝
3. Promise 콜백 (마이크로태스크)
4. queueMicrotask 콜백 (마이크로태스크)
5. setTimeout 콜백 (매크로태스크)
해설:
1. 동기 코드(1, 2)가 모두 실행되어 콜 스택이 비워집니다.
2. 이벤트 루프는 매크로태스크 큐를 보기 전에 마이크로태스크 큐를 먼저 확인합니다.
3. 마이크로태스크 큐에 있는 Promise와 queueMicrotask의 콜백(3, 4)을 순서대로 모두 실행합니다.
4. 마이크로태스크 큐가 비워진 것을 확인한 후, 이제 매크로태스크 큐를 확인하여 setTimeout의 콜백(5)을 실행합니다.
동기 코드 실행 -> 이벤트 루프가 큐 확인 -> 큐의 콜백을 스택으로 이동 후 실행하나의 매크로태스크 실행 -> 마이크로태스크 큐 전체 비우기 -> (필요시 렌더링) -> 다음 매크로태스크 실행setTimeout의 지연 시간은 최소 보장 시간이며, 실제 실행은 콜 스택과 마이크로태스크 큐의 상태에 따라 달라집니다.