🔄 이벤트 루프
📖 정의
**이벤트 루프(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에
// 순서대로 실행됨
Q3. 이벤트 루프가 블로킹되면 어떻게 되나요?
A: UI가 멈추고 사용자 경험이 나빠집니다:
// ❌ 블로킹 코드
function blockingTask() {
const start = Date.now();
// 5초 동안 블로킹
while (Date.now() - start < 5000) {
// CPU 100% 사용
}
console.log('5초 경과');
}
button.addEventListener('click', () => {
blockingTask(); // 클릭 시 5초간 멈춤!
});
// 문제점:
// - 버튼 클릭 안 됨
// - 스크롤 안 됨
// - 애니메이션 멈춤
// - 타이머 지연
// - 사용자 화남!
// ✅ 해결 1: 작업 분할
function nonBlockingTask(iterations) {
let count = 0;
function doWork() {
// 작은 청크만 처리
const chunk = Math.min(1000, iterations - count);
for (let i = 0; i < chunk; i++) {
// 작업 수행
count++;
}
if (count < iterations) {
// 다음 청크는 Task Queue에
setTimeout(doWork, 0);
} else {
console.log('완료!');
}
}
doWork();
}
// ✅ 해결 2: requestIdleCallback
function lowPriorityTask() {
requestIdleCallback((deadline) => {
// 브라우저가 한가할 때만 실행
while (deadline.timeRemaining() > 0 && hasWork()) {
doWork();
}
if (hasWork()) {
lowPriorityTask(); // 다음에 계속
}
});
}
// ✅ 해결 3: Web Worker
// main.js
const worker = new Worker('heavy-worker.js');
button.addEventListener('click', () => {
worker.postMessage({ task: 'heavy', data: largeData });
console.log('작업 시작 (메인 스레드는 계속 반응)');
});
worker.onmessage = (e) => {
console.log('결과:', e.data);
};
// heavy-worker.js
self.onmessage = (e) => {
// 무거운 작업
const result = heavyComputation(e.data.data);
self.postMessage(result);
};
// ✅ 해결 4: 프로그레스 표시
async function longTask(data) {
const total = data.length;
let processed = 0;
for (let i = 0; i < data.length; i += 100) {
// 청크 처리
const chunk = data.slice(i, i + 100);
await processChunk(chunk);
processed += chunk.length;
// 진행률 업데이트
updateProgress((processed / total) * 100);
// UI 업데이트 기회
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// 블로킹 측정
function measureBlocking() {
let lastTime = performance.now();
setInterval(() => {
const now = performance.now();
const diff = now - lastTime - 100;
if (diff > 50) {
console.warn(`⚠️ 블로킹: ${diff.toFixed(2)}ms`);
}
lastTime = now;
}, 100);
}
measureBlocking();
Q4. async/await는 어떻게 동작하나요?
A: Promise를 기반으로 동기적으로 보이게 만듭니다:
// async/await 변환
// async/await 코드
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
// Promise로 변환하면:
function fetchUserPromise(id) {
return fetch(`/api/users/${id}`)
.then(response => response.json())
.then(user => user);
}
// 동작 방식
async function example() {
console.log('A'); // 동기 실행
const result = await Promise.resolve('B');
// await 이후는 Promise.then과 동일
console.log(result); // Microtask로 실행
console.log('C'); // Microtask로 실행
}
// 다음과 동일:
function examplePromise() {
console.log('A');
return Promise.resolve('B').then(result => {
console.log(result);
console.log('C');
});
}
// 여러 await
async function multiple() {
console.log('1');
await Promise.resolve();
console.log('2'); // Microtask 1
await Promise.resolve();
console.log('3'); // Microtask 2
return '4'; // Microtask 3
}
// 에러 처리
async function withError() {
try {
const data = await fetch('/api/data');
return data;
} catch (error) {
console.error('에러:', error);
}
}
// Promise 변환:
function withErrorPromise() {
return fetch('/api/data')
.then(data => data)
.catch(error => {
console.error('에러:', error);
});
}
// 병렬 실행
// ❌ 순차 실행 (느림)
async function sequential() {
const user = await fetch('/api/user'); // 1초
const posts = await fetch('/api/posts'); // 1초
const comments = await fetch('/api/comments'); // 1초
// 총 3초
}
// ✅ 병렬 실행 (빠름)
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments')
]);
// 총 1초 (동시 실행)
}
// async 함수의 특징
const characteristics = {
반환값: 'Promise 항상 반환',
await: 'async 함수 내에서만 사용',
예외: 'try-catch로 처리',
체이닝: 'then/catch 사용 가능',
실행: 'await 이전은 동기',
이후: 'Microtask로 실행'
};
Q5. requestAnimationFrame은 언제 실행되나요?
A: 브라우저가 다음 화면을 그리기 직전에 실행됩니다:
// requestAnimationFrame (rAF)
/*
이벤트 루프 사이클:
1. Macrotask 실행
2. Microtask 모두 실행
3. 렌더링 필요 시:
a. requestAnimationFrame 실행
b. Layout 계산
c. Paint
4. 다시 1번으로
*/
// 실행 순서
console.log('1. Start');
setTimeout(() => {
console.log('2. setTimeout');
}, 0);
requestAnimationFrame(() => {
console.log('3. rAF');
});
Promise.resolve().then(() => {
console.log('4. Promise');
});
console.log('5. End');
// 출력:
// 1. Start (동기)
// 5. End (동기)
// 4. Promise (Microtask)
// 3. rAF (렌더링 전 - 16ms마다)
// 2. setTimeout (다음 Task)
// 애니메이션에 사용
function animate() {
let position = 0;
function step() {
position += 1;
element.style.transform = `translateX(${position}px)`;
if (position < 500) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
// ❌ setTimeout 사용 (부정확)
function animateWithTimeout() {
let position = 0;
function step() {
position += 1;
element.style.transform = `translateX(${position}px)`;
if (position < 500) {
setTimeout(step, 16); // 60fps를 위한 16ms
}
}
setTimeout(step, 16);
}
// 문제: 실제 렌더링과 동기화 안 됨
// ✅ requestAnimationFrame (정확)
// 브라우저 렌더링과 완벽히 동기화
// 60fps 보장 (브라우저가 조절)
// 성능 측정
let frameCount = 0;
let lastTime = performance.now();
function measureFPS() {
const now = performance.now();
frameCount++;
if (now >= lastTime + 1000) {
console.log(`FPS: ${frameCount}`);
frameCount = 0;
lastTime = now;
}
requestAnimationFrame(measureFPS);
}
measureFPS();
// 탭이 비활성화되면?
let animationId;
function startAnimation() {
function animate() {
// 애니메이션 로직
console.log('Animating...');
animationId = requestAnimationFrame(animate);
}
animate();
}
// 탭 비활성화 시 자동 정지
// 배터리 절약!
// 중지
function stopAnimation() {
cancelAnimationFrame(animationId);
}
// 스무스 스크롤
function smoothScrollTo(targetY, duration) {
const startY = window.scrollY;
const distance = targetY - startY;
const startTime = performance.now();
function scroll(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing 함수
const easeProgress = easeInOutQuad(progress);
window.scrollTo(0, startY + distance * easeProgress);
if (progress < 1) {
requestAnimationFrame(scroll);
}
}
requestAnimationFrame(scroll);
}
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
// 사용
smoothScrollTo(1000, 1000); // 1초 동안 1000px 스크롤
🎓 다음 단계
이벤트 루프를 이해했다면, 다음을 학습해보세요:
🎬 마무리
이벤트 루프는 JavaScript의 핵심 실행 모델입니다:
- Call Stack: 현재 실행 중인 코드
- Task Queue: setTimeout 등의 콜백
- Microtask Queue: Promise, async/await (우선순위 높음)
- Event Loop: 이들을 조율하여 비동기 처리
이벤트 루프를 이해하면 JavaScript의 비동기 동작을 완전히 마스터할 수 있습니다!