🔄 이벤트 루프
📖 정의
**이벤트 루프(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)
5. 이벤트 루프 시각화
// 이벤트 루프 동작 완전 분석
console.log('Script Start'); // 1
setTimeout(() => {
console.log('setTimeout 1'); // 5
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1'); // 3
setTimeout(() => {
console.log('setTimeout 2'); // 6
}, 0);
})
.then(() => {
console.log('Promise 2'); // 4
});
console.log('Script End'); // 2
// 상세 실행 과정:
/*
=== 초기 상태 ===
Call Stack: [전역 실행 컨텍스트]
Task Queue: []
Microtask Queue: []
=== Step 1: console.log('Script Start') ===
출력: "Script Start"
Call Stack: [console.log]
Task Queue: []
Microtask Queue: []
=== Step 2: setTimeout ===
Web API로 이동 (0ms 후 Task Queue에)
Call Stack: []
Task Queue: []
Microtask Queue: []
Web API: [setTimeout 1 콜백]
=== Step 3: Promise.resolve().then() ===
첫 번째 then → Microtask Queue
Call Stack: []
Task Queue: []
Microtask Queue: [Promise 1]
Web API: [setTimeout 1 콜백]
=== Step 4: console.log('Script End') ===
출력: "Script End"
Call Stack: [console.log]
Task Queue: []
Microtask Queue: [Promise 1]
=== Step 5: Call Stack 비움 ===
Call Stack: []
Task Queue: [setTimeout 1 콜백]
Microtask Queue: [Promise 1]
=== Step 6: Microtask 처리 (Promise 1) ===
출력: "Promise 1"
setTimeout 2 → Web API
두 번째 then → Microtask Queue
Call Stack: []
Task Queue: [setTimeout 1 콜백]
Microtask Queue: [Promise 2]
Web API: [setTimeout 2 콜백]
=== Step 7: Microtask 처리 (Promise 2) ===
출력: "Promise 2"
Call Stack: []
Task Queue: [setTimeout 1 콜백, setTimeout 2 콜백]
Microtask Queue: []
=== Step 8: Task 처리 (setTimeout 1) ===
출력: "setTimeout 1"
Call Stack: []
Task Queue: [setTimeout 2 콜백]
Microtask Queue: []
=== Step 9: Task 처리 (setTimeout 2) ===
출력: "setTimeout 2"
Call Stack: []
Task Queue: []
Microtask Queue: []
=== 완료 ===
*/
💡 실제 예시
setTimeout(fn, 0)의 비밀
// setTimeout(fn, 0)은 즉시 실행되지 않음!
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
// 출력: 1, 3, 2
// 이유:
// 1. setTimeout은 Task Queue에
// 2. 동기 코드 먼저 실행
// 3. Call Stack 비면 Task 실행
// 실전 활용: UI 블로킹 방지
function heavyTask(data) {
const results = [];
for (let i = 0; i < data.length; i++) {
// 무거운 계산
results.push(processData(data[i]));
}
return results;
}
// ❌ 블로킹 (UI 멈춤)
function blockingProcess(data) {
const results = heavyTask(data);
displayResults(results);
}
// ✅ 비블로킹 (UI 반응)
function nonBlockingProcess(data, chunkSize = 100) {
const results = [];
let index = 0;
function processChunk() {
const chunk = data.slice(index, index + chunkSize);
chunk.forEach(item => {
results.push(processData(item));
});
index += chunkSize;
if (index < data.length) {
// 다음 청크를 Task Queue에
setTimeout(processChunk, 0);
} else {
displayResults(results);
}
}
processChunk();
}
// requestAnimationFrame과의 차이
console.log('A');
setTimeout(() => {
console.log('B - setTimeout');
}, 0);
requestAnimationFrame(() => {
console.log('C - rAF');
});
Promise.resolve().then(() => {
console.log('D - Promise');
});
console.log('E');
// 출력:
// A (동기)
// E (동기)
// D (Microtask)
// C (렌더링 전 - 브라우저에 따라 다름)
// B (Task)
Promise 체이닝과 이벤트 루프
// Promise 체이닝의 실행 순서
Promise.resolve()
.then(() => {
console.log('1');
return Promise.resolve();
})
.then(() => {
console.log('2');
});
Promise.resolve()
.then(() => {
console.log('3');
})
.then(() => {
console.log('4');
});
// 출력: 1, 3, 2, 4
// 왜?
/*
Microtask Queue 순서:
1. 첫 번째 Promise의 첫 then
2. 두 번째 Promise의 첫 then
3. 첫 번째 Promise의 두 번째 then
4. 두 번째 Promise의 두 번째 then
각 then은 새로운 Microtask를 생성!
*/
// async/await로 동일 코드
async function example1() {
console.log('1');
await Promise.resolve();
console.log('2');
}
async function example2() {
console.log('3');
await Promise.resolve();
console.log('4');
}
example1();
example2();
// 출력: 1, 3, 2, 4 (동일)
// 복잡한 케이스
async function test() {
console.log('A');
Promise.resolve().then(() => {
console.log('B');
});
await Promise.resolve();
console.log('C');
Promise.resolve().then(() => {
console.log('D');
});
console.log('E');
}
console.log('Start');
test();
console.log('End');
// 출력:
// Start (동기)
// A (동기 - await 전)
// End (동기)
// B (Microtask - Promise.then)
// C (Microtask - await 후)
// E (동기 - await 블록 내)
// D (Microtask - Promise.then)
이벤트 핸들러와 이벤트 루프
// DOM 이벤트는 Task Queue에
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log('Click handler');
Promise.resolve().then(() => {
console.log('Promise in handler');
});
setTimeout(() => {
console.log('setTimeout in handler');
}, 0);
});
// 클릭 시 출력:
// Click handler (Task - 이벤트)
// Promise in handler (Microtask)
// setTimeout in handler (Task - 다음 사이클)
// 연속 클릭
button.addEventListener('click', () => {
console.log('First');
Promise.resolve().then(() => console.log('Promise'));
});
button.addEventListener('click', () => {
console.log('Second');
});
// 클릭 시:
// First (Task)
// Promise (Microtask)
// Second (Task)
// 이벤트 위임과 성능
// ❌ 많은 이벤트 리스너
items.forEach((item, index) => {
item.addEventListener('click', () => {
console.log(`Item ${index} clicked`);
});
});
// ✅ 이벤트 위임
container.addEventListener('click', (e) => {
if (e.target.classList.contains('item')) {
console.log('Item clicked');
}
});
// debounce와 이벤트 루프
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
console.log('Search:', e.target.value);
// API 호출
}, 300));
// 타이핑할 때마다:
// - 이전 setTimeout 취소
// - 새 setTimeout Task Queue에
// - 300ms 후 실행
Node.js의 이벤트 루프
// Node.js는 단계별 이벤트 루프
/*
Node.js Event Loop Phases:
┌───────────────────────────┐
┌─>│ timers │ setTimeout, setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ I/O 콜백
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ 내부용
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ I/O 이벤트 대기
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ setImmediate
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──│ close callbacks │ close 이벤트
└───────────────────────────┘
각 단계 사이에 Microtask Queue 실행!
*/
// setImmediate vs setTimeout
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
// 출력 순서: 상황에 따라 다름!
// (timers와 check 단계의 타이밍)
// I/O 안에서는 순서 보장
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
// 항상 출력:
// setImmediate (check 단계가 먼저)
// setTimeout (다음 사이클 timers)
// process.nextTick (최우선)
console.log('Start');
setImmediate(() => {
console.log('Immediate');
});
process.nextTick(() => {
console.log('nextTick');
});
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
// 출력:
// Start (동기)
// End (동기)
// nextTick (최우선 Microtask)
// Promise (Microtask)
// Immediate (check 단계)
// 무거운 작업 분할 (Node.js)
function processLargeData(data) {
return new Promise((resolve) => {
let index = 0;
const chunkSize = 1000;
const results = [];
function processChunk() {
const chunk = data.slice(index, index + chunkSize);
chunk.forEach(item => {
results.push(heavyComputation(item));
});
index += chunkSize;
if (index < data.length) {
// 다른 작업이 실행될 기회
setImmediate(processChunk);
} else {
resolve(results);
}
}
processChunk();
});
}
// 사용
processLargeData(largeArray)
.then(results => {
console.log('처리 완료:', results.length);
});
실전 디버깅
// 이벤트 루프 시각화 도구
function logEventLoop(label) {
console.log(`\n=== ${label} ===`);
console.log('Stack:', new Error().stack.split('\n').slice(2, 5).join('\n'));
}
// 사용
logEventLoop('Start');
setTimeout(() => {
logEventLoop('Timeout');
}, 0);
Promise.resolve().then(() => {
logEventLoop('Promise');
});
// 실행 시간 측정
console.time('Total');
setTimeout(() => {
console.timeEnd('Total');
}, 0);
// Task 실행 간격 측정
let lastTime = Date.now();
function measureInterval(label) {
const now = Date.now();
console.log(`${label}: ${now - lastTime}ms`);
lastTime = now;
}
console.log('Start');
measureInterval('Start');
setTimeout(() => {
measureInterval('setTimeout 1');
}, 0);
Promise.resolve().then(() => {
measureInterval('Promise');
});
setTimeout(() => {
measureInterval('setTimeout 2');
}, 10);
// 이벤트 루프 블로킹 감지
function detectBlocking(threshold = 100) {
let lastCheck = Date.now();
setInterval(() => {
const now = Date.now();
const delay = now - lastCheck - 100;
if (delay > threshold) {
console.warn(`⚠️ 블로킹 감지: ${delay}ms`);
}
lastCheck = now;
}, 100);
}
detectBlocking();
// 무거운 작업 실행
// function slowTask() {
// const start = Date.now();
// while (Date.now() - start < 500) {
// // 500ms 블로킹
// }
// }
// slowTask(); // ⚠️ 블로킹 감지: 500ms
// Performance API
performance.mark('start');
setTimeout(() => {
performance.mark('timeout');
performance.measure('timeout-duration', 'start', 'timeout');
const measure = performance.getEntriesByName('timeout-duration')[0];
console.log(`Timeout 실행까지: ${measure.duration}ms`);
}, 0);
Promise.resolve().then(() => {
performance.mark('promise');
performance.measure('promise-duration', 'start', 'promise');
const measure = performance.getEntriesByName('promise-duration')[0];
console.log(`Promise 실행까지: ${measure.duration}ms`);
});
React와 이벤트 루프
import React, { useState, useEffect, useTransition } from 'react';
// 1. setState와 이벤트 루프
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// 여러 setState는 배치 처리됨
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
// 한 번만 리렌더링!
console.log(count); // 이전 값 출력 (비동기)
// 업데이트 후 실행하려면
setTimeout(() => {
console.log(count); // 업데이트된 값
}, 0);
};
return <button onClick={handleClick}>Count: {count}</button>;
}
// 2. useEffect와 이벤트 루프
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
console.log('Effect Start');
fetch('/api/data')
.then(res => res.json())
.then(data => {
console.log('Data fetched');
setData(data);
});
console.log('Effect End');
// Effect End가 먼저 출력됨!
// fetch는 비 동기
}, []);
return <div>{data ? data.title : 'Loading...'}</div>;
}
// 3. useTransition (React 18)
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// 즉시 업데이트 (높은 우선순위)
setQuery(value);
// 지연 업데이트 (낮은 우선순위)
startTransition(() => {
const filtered = hugeData.filter(item =>
item.includes(value)
);
setResults(filtered);
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending ? <div>검색 중...</div> : null}
<ul>
{results.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
}
// 4. 커스텀 Hook - 비동기 상태
function useAsyncState(initialState) {
const [state, setState] = useState(initialState);
const setAsyncState = (newState) => {
return new Promise((resolve) => {
setState(newState);
// State 업데이트 후 실행
setTimeout(() => {
resolve(newState);
}, 0);
});
};
return [state, setAsyncState];
}
// 사용
function Component() {
const [count, setCount] = useAsyncState(0);
const increment = async () => {
await setCount(count + 1);
console.log('Updated!');
};
return <button onClick={increment}>Count: {count}</button>;
}
🤔 자주 묻는 질문
Q1. 왜 JavaScript는 단일 스레드인가요?
A: JavaScript는 브라우저 환경에서 설계되었기 때문입니다:
// 단일 스레드의 이유
const reasons = {
역사적배경: `
JavaScript는 원래 간단한 웹 페이지 상호작용용
- DOM 조작
- 폼 검증
- 간단한 애니메이션
멀티스레드는 복잡성을 증가시킴
`,
DOM조작: `
여러 스레드가 동시에 DOM을 수정하면?
- 경쟁 조건 (Race Condition)
- 데드락 (Deadlock)
- 동기화 문제
예시:
// 스레드 1
element.style.color = 'red';
// 스레드 2 (동시)
element.remove();
// 어떤 결과? 예측 불가능!
`,
단순성: `
단일 스레드는 이해하기 쉬움
- 순서 보장
- 예측 가능
- 디버깅 쉬움
`
};
// 하지만 비동기로 해결!
async function fetchData() {
// 네트워크 요청 중에도
const data = await fetch('/api/data');
// UI는 반응함!
return data;
}
// Web Workers (진짜 멀티스레드)
// 메인 스레드와 분리된 스레드
const worker = new Worker('worker.js');
// 무거운 계산을 Worker에서
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
console.log('계산 결과:', e.data);
};
// worker.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// 장단점
const comparison = {
단일스레드: {
장점: [
'간단한 모델',
'동기화 문제 없음',
'예측 가능한 실행',
'디버깅 쉬움'
],
단점: [
'블로킹 위험',
'CPU 코어 활용 제한',
'무거운 계산 느림'
]
},
Web_Workers: {
장점: [
'진짜 병렬 처리',
'CPU 집약적 작업',
'메인 스레드 안 막힘'
],
단점: [
'DOM 접근 불가',
'메시지 전달만 가능',
'복잡성 증가'
]
}
};
Q2. Microtask와 Macrotask의 차이는?
A: 우선순위와 실행 시점이 다릅니다:
// Microtask (높은 우선순위)
const microtasks = {
종류: [
'Promise.then()',
'Promise.catch()',
'Promise.finally()',
'async/await',
'queueMicrotask()',
'MutationObserver',
'process.nextTick() (Node.js)'
],
특징: '현재 Task 끝나면 즉시 모두 실행',
예시: `
Promise.resolve().then(() => {
console.log('Microtask 1');
});
Promise.resolve().then(() => {
console.log('Microtask 2');
});
// 둘 다 현재 Task 끝나면 즉시 실행
`
};
// Macrotask (낮은 우선순위)
const macrotasks = {
종류: [
'setTimeout()',
'setInterval()',
'setImmediate() (Node.js)',
'requestAnimationFrame()',
'I/O',
'UI 렌더링',
'MessageChannel'
],
특징: '한 번에 하나씩만 실행',
예시: `
setTimeout(() => {
console.log('Task 1');
}, 0);
setTimeout(() => {
console.log('Task 2');
}, 0);
// Task 1 실행 → Microtask 확인 → Task 2 실행
`
};
// 실행 순서 비교
console.log('1. Start');
setTimeout(() => {
console.log('2. Timeout 1');
Promise.resolve().then(() => {
console.log('3. Promise in Timeout 1');
});
setTimeout(() => {
console.log('4. Timeout in Timeout');
}, 0);
}, 0);
Promise.resolve().then(() => {
console.log('5. Promise 1');
setTimeout(() => {
console.log('6. Timeout in Promise');
}, 0);
});
setTimeout(() => {
console.log('7. Timeout 2');
}, 0);
Promise.resolve().then(() => {
console.log('8. Promise 2');
});
console.log('9. End');
// 출력 순서:
// 1. Start (동기)
// 9. End (동기)
// 5. Promise 1 (Microtask)
// 8. Promise 2 (Microtask)
// 2. Timeout 1 (Task)
// 3. Promise in Timeout 1 (Microtask)
// 7. Timeout 2 (Task)
// 6. Timeout in Promise (Task)
// 4. Timeout in Timeout (Task)
/*
상세 실행:
1. 동기 코드: 1, 9
2. Microtask: 5, 8
3. Task 1: 2
└─ Microtask: 3
4. Task 2: 7
5. Task 3: 6
6. Task 4: 4
*/
// queueMicrotask 사용
queueMicrotask(() => {
console.log('Custom Microtask');
});
Promise.resolve().then(() => {
console.log('Promise Microtask');
});
// 둘 다 Microtask Queue에
// 순서대로 실행됨