JavaScript는 싱글 스레드 언어입니다.
그런데 어떻게 비동기 작업을 처리할 수 있을까요?
**이벤트 루프(Event Loop)**를 사용할 수 있습니다.
이벤트 루프란?
이벤트 루프는 JavaScript 엔진이 비동기 작업을 처리하는 메커니즘입니다. 코드 실행을 관리하고, 대기 중인 작업들을 순서대로 처리합니다.
핵심 구성 요소
┌─────────────┐
│ Call Stack │ ← 현재 실행 중인 코드
└─────────────┘
↓
┌─────────────────┐
│ Microtask Queue │ ← Promise, queueMicrotask (우선순위 높음)
└─────────────────┘
↓
┌─────────────────┐
│ Task Queue │ ← setTimeout, setInterval (우선순위 낮음)
└─────────────────┘
1. Call Stack (콜 스택)
현재 실행 중인 함수들이 쌓이는 곳입니다.
function first() {
console.log('첫 번째');
second();
}
function second() {
console.log('두 번째');
}
first();
// 실행 순서:
// 1. first() 스택에 push
// 2. console.log('첫 번째') 실행 후 pop
// 3. second() 스택에 push
// 4. console.log('두 번째') 실행 후 pop
// 5. second() pop
// 6. first() pop
2. Task Queue (Macro Task Queue)
대표 API:
- setTimeout
- setInterval
- setImmediate (Node.js)
- I/O 작업
- UI 렌더링
setTimeout(() => {
console.log('Task Queue에서 실행');
}, 0);
3. Microtask Queue
대표 API:
- Promise.then(), catch(), finally()
- queueMicrotask()
- async/await
- MutationObserver
Promise.resolve().then(() => {
console.log('Microtask Queue에서 실행');
});
이벤트 루프 실행 순서
핵심 규칙
1. Call Stack의 모든 동기 코드 실행
2. Microtask Queue의 모든 작업 실행
3. Task Queue에서 하나의 작업 실행
4. 2번으로 돌아가기
중요: Microtask는 Task보다 항상 먼저 실행됩니다!
실전 예제
예제 1: 기본 개념
console.log('1'); // 동기
setTimeout(() => {
console.log('2'); // Task Queue
}, 0);
Promise.resolve().then(() => {
console.log('3'); // Microtask Queue
});
console.log('4'); // 동기
// 출력: 1, 4, 3, 2
실행 과정:
- console.log('1') 즉시 실행 → 1 출력
- setTimeout → Task Queue에 등록
- Promise → Microtask Queue에 등록
- console.log('4') 즉시 실행 → 4 출력
- Call Stack 비움
- Microtask Queue 확인 → 3 출력
- Task Queue 확인 → 2 출력
예제 2: Promise 체이닝
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
})
.then(() => {
console.log('Promise 2');
});
console.log('End');
// 출력: Start, End, Promise 1, Promise 2, Timeout 1
포인트:
- .then() 체이닝도 각각 Microtask로 처리됩니다
- 모든 Microtask가 끝나야 Task가 실행됩니다
예제 3: 중첩된 비동기 (복잡)
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => {
console.log('Promise in Timeout');
});
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('Timeout 2');
}, 0);
})
.then(() => {
console.log('Promise 2');
});
console.log('End');
// 출력: Start, End, Promise 1, Promise 2,
// Timeout 1, Promise in Timeout, Timeout 2
핵심:
- Task 실행 중에 Microtask가 생성되면, 해당 Task 종료 후 즉시 Microtask를 처리합니다
- 다음 Task로 넘어가기 전에 Microtask Queue를 비웁니다
실무에서의 활용
1. async/await
async function fetchData() {
console.log('1');
await fetch('/api/data'); // Microtask
console.log('2'); // await 이후는 .then()처럼 동작
}
console.log('3');
fetchData();
console.log('4');
// 출력: 3, 1, 4, 2
await의 동작:
- await 이전 코드는 동기 실행
- await는 Promise를 기다림
- await 이후 코드는 Microtask로 등록
2. 순서 보장이 필요한 경우
// ❌ 순서 보장 안 됨
function saveData() {
setTimeout(() => saveToDatabase(), 0);
setTimeout(() => sendNotification(), 0);
}
// ✅ 순서 보장
async function saveData() {
await saveToDatabase();
await sendNotification();
}
!! 정리
Task Queue (Macro Task)
- setTimeout, setInterval
- 우선순위 낮음
- 한 번에 하나씩 처리
Microtask Queue
- Promise, async/await
- 우선순위 높음
- 큐가 빌 때까지 모두 처리
실행 순서
동기 코드 → Microtask Queue → Task Queue → 반복
이벤트 루프는 JavaScript의 비동기 동작을 이해하는 핵심 개념입니다.
처음에는 복잡해 보이지만, **"Microtask가 Task보다 우선순위가 높다"**는 핵심만 기억하면 대부분의 상황을 이해할 수 있습니다.
실제 코드를 작성하며 직접 실행 순서를 예측해보는 연습을 하면, 금방 익숙해질 것입니다!
참고 자료
반응형