🔄 이벤트 루프
📖 정의
**이벤트 루프(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의 비동기 동작을 완전히 마스터할 수 있습니다!