JavaScript 이벤트 루프: 실제 작동 방식, 호출 스택, 마이크로태스크 및 매크로태스크
이벤트 루프는 JavaScript의 핵심이지만 대부분의 개발자는 이를 이해하지 못합니다. 모호하다. 우리는 콜 스택, 웹 API, 콜백 큐, 마이크로태스크 큐(Promise), 매크로태스크 대기열(setTimeout) 및 실행 순서가 놀라운 이유
JavaScript는 단일 스레드입니다: 실제로 의미하는 것
자바스크립트에는 단일 실행 스레드: 언제든지 하나만 JavaScript 문이 실행 중입니다. 이것은 런타임에 관한 사실이지 실제적인 사실이 아닙니다. 언어의 한계. JavaScript 엔진(Chrome/Node.js의 V8, Firefox의 SpiderMonkey) 코드를 순차적으로 실행합니다.
그러나 Node.js는 수만 개의 동시 요청을 처리합니다. 처럼? 통해
이벤트 루프: 비동기 I/O 작업을 기다릴 수 있는 메커니즘
(네트워크, 파일 시스템) 스레드를 차단하지 않고 실제 작업을 운영 체제에 위임합니다.
통해 libuv.
이벤트 루프의 구성요소
Node.js(V8 + libuv)의 이벤트 루프는 다음으로 구성됩니다.
- 호출 스택: JavaScript 코드의 동기 실행 스택입니다. 언제 함수가 호출되면 스택에 "푸시"됩니다. 끝나면 "먹이"됩니다. 스택이 비어 있지 않으면 스레드가 사용 중입니다.
-
웹 API/노드 API: 환경(브라우저 또는 Node.js)에서 제공하는 인터페이스
비동기 I/O 작업의 경우:
setTimeout,fetch,fs.readFile, WebSocket 등. 이러한 작업은 C++ 런타임(libuv)에 위임되며 JS 스레드를 사용하지 않습니다. - 콜백 대기열(매크로태스크 대기열): 다음에 실행할 콜백 대기열 I/O 작업 완료, 시간 초과, 간격 호출 스택이 비어 있는 후에 실행됨 마이크로태스크 대기열은 비워집니다.
-
마이크로태스크 대기열: Promise 콜백을 위한 높은 우선순위 대기열
(
.then(),async/await) 그리고queueMicrotask(). 그 사람이 온다 각 매크로 작업 전에 완전히 비워집니다.
실행 순서: 기본 규칙
이벤트 루프의 가장 중요한 규칙은 다음과 같습니다.
- 호출 스택에서 모든 동기 코드 실행
- 마이크로태스크 대기열을 완전히 지웁니다(Promise, MutationObserver).
- 실행 다음 매크로태스크 콜백 대기열에서(setTimeout, I/O 콜백)
- 포인트 2로 돌아가세요.
이는 많은 개발자를 놀라게 하는 동작을 설명합니다.
console.log('1 - sincrono');
setTimeout(() => console.log('4 - macrotask'), 0);
Promise.resolve()
.then(() => console.log('2 - microtask 1'))
.then(() => console.log('3 - microtask 2'));
console.log('1b - sincrono');
// Output:
// 1 - sincrono
// 1b - sincrono
// 2 - microtask 1
// 3 - microtask 2
// 4 - macrotask
// Perché? Il setTimeout con delay=0 è comunque un macrotask.
// Le Promise sono microtask, eseguite PRIMA del prossimo macrotask.
이벤트 루프를 단계별로 살펴보세요
// Esempio completo — traccia mentale dell'esecuzione
async function main() {
console.log('A'); // [1] push main, push log('A')
setTimeout(() => console.log('B'), 0); // [2] delega a Web APIs
await Promise.resolve('resolved'); // [3] sospende main, schedula microtask
console.log('C'); // [6] riprende dopo microtask
}
console.log('D'); // [4] esegue sincrono
main(); // [1] chiama main
console.log('E'); // [5] esegue sincrono dopo main si sospende
// Esecuzione:
// [1] D (sincrono prima di main)
// [2] A (main inizia, prima console.log)
// [3] main si sospende su await
// [4] E (codice sincrono dopo main())
// [5] Stack vuoto, microtask queue: Promise.resolve callback
// [6] C (main riprende dopo await)
// [7] Stack vuoto, macrotask queue: setTimeout callback
// [8] B
// Output finale: D, A, E, C, B
약속 및 비동기/대기: 세부 정보
async/await 그것은 약속 위에 구문 설탕입니다. 컴파일러는 변환합니다
모든 await in un .then(). 변환을 이해하면 이해하는 데 도움이 됩니다.
실행 순서:
// Questa funzione async:
async function processUser(id) {
const user = await fetchUser(id); // await punto 1
const orders = await fetchOrders(user); // await punto 2
return { user, orders };
}
// È equivalente a:
function processUserWithPromises(id) {
return fetchUser(id)
.then(user => {
return fetchOrders(user)
.then(orders => { return { user, orders }; });
});
}
// La funzione si "sospende" a ogni await e
// riprende quando la Promise risolve (tramite microtask queue)
마이크로태스크 기아: 피해야 할 안티 패턴
마이크로태스크 대기열이 결코 비워지지 않으면 매크로태스크(따라서 I/O 콜백)도 비어 있지 않습니다. 수행한 적이 없습니다. 이것은 호출됩니다 마이크로태스크 기아:
// ANTI-PATTERN: loop infinito di microtask — blocca tutto!
function recursiveMicrotask() {
Promise.resolve().then(recursiveMicrotask); // schedula infiniti microtask
}
recursiveMicrotask();
// setTimeout di seguito non verrà MAI eseguito!
// PATTERN CORRETTO: usa setImmediate (Node.js) o setTimeout per cedere il controllo
function processLargeDataset(data, index = 0) {
if (index >= data.length) return;
processItem(data[index]);
// Cede il controllo all'event loop ogni 100 elementi
if (index % 100 === 0) {
setImmediate(() => processLargeDataset(data, index + 1));
} else {
processLargeDataset(data, index + 1);
}
}
Node.js 이벤트 루프: 단계
Node.js 이벤트 루프에는 브라우저를 넘어서는 특정 단계(libuv)가 있습니다.
// Le fasi del Node.js event loop (semplificato)
// 1. timers: esegue callback di setTimeout e setInterval
// 2. I/O callbacks: callback I/O differite (errori socket)
// 3. idle, prepare: uso interno Node.js
// 4. poll: recupera I/O events, esegue callback I/O
// 5. check: esegue callback di setImmediate()
// 6. close callbacks: es. socket.on('close', callback)
// setImmediate vs setTimeout(fn, 0): NON garantiti nell'ordine
// dipende dal momento di chiamata nel ciclo
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Output non deterministico se eseguiti nel main module
// Ma dentro una callback I/O:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Qui 'immediate' è sempre PRIMA — siamo già nella fase poll
});
이벤트 루프 차단: 고전적인 실수
이벤트 루프를 차단하는 작업
- 동기식 CPU 집약적 컴퓨팅: 2초 블록을 계산하는 루프 다른 모든 요청은 2초 동안 지속됩니다. CPU 바인딩 작업에는 작업자 스레드를 사용합니다.
- 거대한 페이로드에 대한 JSON.parse(): 10MB JSON을 구문 분석하는 것은 동기식 및 블록. JSON 스트림을 사용하거나 작업자 스레드에 위임하세요.
- 서버 코드의 fs.readFileSync(): 모든 Node.js API의 버전 동기화 그들은 스레드를 차단합니다. 항상 콜백과 함께 비동기 버전을 사용하거나 대기하세요.
-
플래그가 없는 암호화 작업: 암호화 작업은 CPU에 바인딩됩니다.
crypto.scrypt()콜백을 수락합니다.crypto.scryptSync()차단하다.
결론
이벤트 루프는 단일 스레드임에도 불구하고 JavaScript를 효율적으로 만드는 것입니다.
기억해야 할 핵심: 마이크로태스크 대기열은 매크로태스크 대기열보다 우선순위가 높습니다. await
스레드를 차단하지 않지만 현재 기능을 일시 중지합니다. CPU 바인딩된 동기 작업 블록
전체 이벤트 루프.
다음 기사에서는 고루틴과 Go 채널: 모델 CSP를 기반으로 하며 수백만 개의 작업으로 확장되도록 설계된 완전히 다른 동시성 플랫폼 경쟁자.







