🔄 이벤트 루프
📖 정의
**이벤트 루프(Event Loop)**는 JavaScript가 단일 스레드임에도 불구하고 비동기 작업을 처리할 수 있게 해주는 메커니즘입니다. Call Stack, Task Queue, Microtask Queue를 조율하여 코드 실행 순서를 관리합니다.
🎯 비유로 이해하기
식당 주방 비유
이벤트 루프를 식당 주방에 비유하면:
주방장 (Call Stack)
├─ 한 번에 하나의 요리만 가능
├─ 현재 만들고 있는 요리에 집중
└─ 요리 완성되면 다음 요리 시작
주문 게시판 (Task Queue)
├─ 일반 주문들이 대기
├─ setTimeout, 이벤트 등
└─ 주방장이 한가할 때 처리
VIP 주문 (Microtask Queue)
├─ Promise, async/await
├─ 일반 주문보다 우선순위 높음
└─ 주방장이 한가하면 VIP부터
웨이터 (Event Loop)
├─ 주방장 상태 확인
├─ 한가하면 VIP 주문 전달
├─ VIP 없으면 일반 주문 전달
└─ 계속 순환하며 확인
과정:
1. 주방장이 요리 중 (Call Stack 실행)
2. 요리 완료 (Stack 비움)
3. 웨이터가 VIP 주문 확인 (Microtask)
4. VIP 주문 있으면 주방장에게 전달
5. VIP 주문 없으면 일반 주문 확인 (Task)
6. 일반 주문 주방장에게 전달
7. 계속 반복
은행 창구 비유
창구 직원 (JavaScript 엔진)
└─ 한 명만 있음 (단일 스레드)
처리 중인 업무 (Call Stack)
└─ 현재 처리하는 고객
일반 대기번호 (Task Queue)
├─ 입출금
├─ 계좌 조회
└─ setTimeout, setInterval
우선 대기번호 (Microtask Queue)
├─ 긴급 업무
├─ Promise
└─ async/await
매니저 (Event Loop)
├─ 직원이 한가한지 계속 확인
├─ 우선 번호부터 처리
└─ 일반 번호 처리
실제 상황:
직원: "다음 손님~"
매니저: "우선 번호 있나요?" (Microtask 확인)
매니저: "없으면 일반 번호" (Task 확인)
직원: 업무 처리
매니저: 다시 확인 (Loop)
놀이공원 놀이기구 비유
놀이기구 (JavaScript 실행)
└─ 한 번에 한 팀만 탑승
현재 탑승 중 (Call Stack)
└─ 지금 타고 있는 사람들
일반 줄 (Task Queue)
├─ 일반 티켓 소지자
├─ setTimeout
└─ 이벤트 핸들러
패스트패스 줄 (Microtask Queue)
├─ 우선권 소지자
├─ Promise
└─ async/await
직원 (Event Loop)
1. 놀이기구 비었나요? (Stack 확인)
2. 패스트패스 있나요? (Microtask)
3. 패스트패스 전부 태우기
4. 일반 줄에서 1팀 태우기
5. 다시 1번으로
특징:
- 놀이기구는 항상 한 팀만
- 패스트패스가 항상 우선
- 일반 줄은 한 번에 1팀씩만
⚙️ 작동 원리
1. JavaScript 실행 모델
// JavaScript는 단일 스레드
/*
JavaScript 실행 환경:
┌────────────────────────────────────┐
│ JavaScript Engine │
│ │
│ ┌──────────────────────────┐ │
│ │ Call Stack │ │ 동기 코드 실행
│ │ (실행 중인 함수 들) │ │
│ └──────────────────────────┘ │
│ │
└────────────────────────────────────┘
↓
┌────────────────────────────────────┐
│ Web APIs │
│ (브라우저 / Node.js 제공) │
│ │
│ • setTimeout │
│ • setInterval │
│ • fetch (HTTP 요청) │
│ • DOM Events │
│ • Promise │
│ │
└────────────────────────────────────┘
↓
┌────────────────────────────────────┐
│ Microtask Queue │
│ (높은 우선순위) │
│ │
│ • Promise.then() │
│ • async/await │
│ • queueMicrotask() │
│ • MutationObserver │
│ │
└────────────────────────────────────┘
↓
┌────────────────────────────────────┐
│ Task Queue │
│ (Macrotask Queue) │
│ (낮은 우선순위) │
│ │
│ • setTimeout │
│ • setInterval │
│ • setImmediate (Node.js) │
│ • I/O 작업 │
│ • UI 렌더링 │
│ │
└────────────────────────────────────┘
↓
┌─────────────┐
│ Event Loop │ ← 계속 순환
└─────────────┘
*/
// 실행 순서:
// 1. Call Stack의 동기 코드 실행
// 2. Stack이 비면 Microtask Queue 확인
// 3. Microtask를 모두 실행
// 4. Task Queue에서 하나 실행
// 5. 1번으로 돌아가기
2. Call Stack (호출 스택)
// Call Stack: 실행 중인 함수들의 스택
function first() {
console.log('첫 번째');
second();
console.log('첫 번째 끝');
}
function second() {
console.log('두 번째');
third();
console.log('두 번째 끝');
}
function third() {
console.log('세 번째');
}
first();
// 실행 과정:
/*
Step 1: first() 호출
┌─────────────┐
│ first() │
└─────────────┘
출력: "첫 번째"
Step 2: second() 호출
┌─────────────┐
│ second() │
├─────────────┤
│ first() │
└─────────────┘
출력: "두 번째"
Step 3: third() 호출
┌─────────────┐
│ third() │
├─────────────┤
│ second() │
├─────────────┤
│ first() │
└─────────────┘
출력: "세 번째"
Step 4: third() 완료
┌─────────────┐
│ second() │
├─────────────┤
│ first() │
└─────────────┘
출력: "두 번째 끝"
Step 5: second() 완료
┌─────────────┐
│ first() │
└─────────────┘
출력: "첫 번째 끝"
Step 6: first() 완료
┌─────────────┐
│ (비어있음) │
└─────────────┘
*/
// Stack Overflow
function recursion() {
recursion(); // 끝나지 않는 재귀
}
// recursion(); // Maximum call stack size exceeded
// 해결: 비동기로 변경
function safeRecursion(n) {
if (n === 0) return;
console.log(n);
// Stack을 비우고 다음 실행
setTimeout(() => {
safeRecursion(n - 1);
}, 0);
}
safeRecursion(5);
3. Task Queue vs Microtask Queue
// 우선순위 차이
console.log('1. Script Start');
setTimeout(() => {
console.log('2. setTimeout (Task)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise (Microtask)');
});
console.log('4. Script End');
// 출력 순서:
// 1. Script Start (동기)
// 4. Script End (동기)
// 3. Promise (Microtask - 우선)
// 2. setTimeout (Task - 나중)
// 상세 분석:
/*
1. 동기 코드 실행:
├─ "1. Script Start" 출력
├─ setTimeout → Web API로 (0ms 후 Task Queue에)
├─ Promise.then → Microtask Queue에
└─ "4. Script End" 출력
2. Call Stack 비움
3. Microtask Queue 확인:
└─ Promise.then 실행
└─ "3. Promise" 출력
4. Microtask 모두 완료
5. Task Queue 확인:
└─ setTimeout 콜백 실행
└─ "2. setTimeout" 출력
*/
// 복잡한 예시
console.log('A');
setTimeout(() => {
console.log('B');
Promise.resolve().then(() => {
console.log('C');
});
}, 0);
Promise.resolve()
.then(() => {
console.log('D');
})
.then(() => {
console.log('E');
});
setTimeout(() => {
console.log('F');
Promise.resolve().then(() => {
console.log('G');
});
}, 0);
console.log('H');
// 출력:
// A (동기)
// H (동기)
// D (Microtask)
// E (Microtask)
// B (Task 1)
// C (Microtask - B 실행 후)
// F (Task 2)
// G (Microtask - F 실행 후)
/*
실행 단계:
1. 동기 코드:
A, H
2. Microtask Queue:
D, E
3. Task Queue (첫 번째):
B
└─ Microtask: C
4. Task Queue (두 번째):
F
└─ Microtask: G
*/
4. async/await와 이벤트 루프
// async/await는 Promise의 문법적 설탕
async function example() {
console.log('1');
await Promise.resolve();
console.log('2');
}
console.log('3');
example();
console.log('4');
// 출력:
// 3 (동기)
// 1 (동기 - await 이전)
// 4 (동기)
// 2 (Microtask - await 이후)
// await는 다음과 같이 변환됨:
function exampleTransformed() {
console.log('1');
return Promise.resolve().then(() => {
console.log('2');
});
}
// 복잡한 예시
async function complex() {
console.log('A');
await Promise.resolve();
console.log('B');
await Promise.resolve();
console.log('C');
return 'Done';
}
console.log('Start');
complex().then(result => {
console.log(result);
});
console.log('End');
// 출력:
// Start (동기)
// A (동기 - 첫 await 전)
// End (동기)
// B (Microtask - 첫 await 후)
// C (Microtask - 두번째 await 후)
// Done (Microtask - return)
// async 함수 여러 개
async function func1() {
console.log('Func1 Start');
await Promise.resolve();
console.log('Func1 End');
}
async function func2() {
console.log('Func2 Start');
await Promise.resolve();
console.log('Func2 End');
}
console.log('Main Start');
func1();
func2();
console.log('Main End');
// 출력:
// Main Start (동기)
// Func1 Start (동기)
// Func2 Start (동기)
// Main End (동기)
// Func1 End (Microtask)
// Func2 End (Microtask)