JavaScript의 비동기 처리 시스템은 아키텍처적으로 정교하게 설계되어 있습니다. 이 시스템의 핵심 컴포넌트와 실행 흐름을 분석해보겠습니다.
이벤트 루프 아키텍처
JavaScript는 싱글 스레드 언어이지만, 호스트 환경(브라우저/Node.js)은 멀티스레딩을 활용합니다:
+--------------------------------------------------+
| JavaScript 런타임 |
| +-----------+ +-------------------------+ |
| | 콜 스택 | | 힙(메모리) | |
| +-----------+ +-------------------------+ |
+--------------------------------------------------+
| ^
v |
+--------------------------------------------------+
| Web APIs / Node.js APIs |
| +--------+ +--------+ +---------------------+ |
| | DOM | | AJAX | | setTimeout/timers | |
| +--------+ +--------+ +---------------------+ |
+--------------------------------------------------+
| ^
v |
+--------------------------------------------------+
| 이벤트 루프 |
| +------------------+ +---------------------+ |
| | 태스크 큐(매크로) | | 마이크로태스크 큐 | |
| +------------------+ +---------------------+ |
+--------------------------------------------------+
실행 흐름의 정밀 분석
- 태스크 스케줄링 메커니즘:
- 비동기 작업은 호스트 환경(브라우저/Node.js)의 스레드풀로 오프로드됩니다.
- JavaScript 메인 스레드는 블로킹되지 않고 계속 실행됩니다.
- Promise와 마이크로태스크 우선순위:
console.log('Script start'); setTimeout(() => { console.log('setTimeout'); // 매크로태스크 }, 0); Promise.resolve().then(() => { console.log('Promise'); // 마이크로태스크 }); console.log('Script end'); // 출력: "Script start", "Script end", "Promise", "setTimeout"
마이크로태스크는 각 매크로태스크 완료 후 큐가 비워질 때까지 처리되므로 우선순위가 높습니다. - async/await 변환 과정:
async function example() { const result = await asyncOperation(); return result; }
컴파일러는 이를 다음과 같이 변환합니다:function example() { return new Promise((resolve, reject) => { asyncOperation() .then(result => { resolve(result); }) .catch(err => { reject(err); }); }); }
이벤트 루프 실행 알고리즘 (간략화)
while (true) {
// 콜 스택이 비워질 때까지 현재 태스크 실행
while (callStack.isNotEmpty()) {
executeCurrentTask();
}
// 모든 마이크로태스크 처리
while (microtaskQueue.isNotEmpty()) {
executeNextMicrotask();
}
// 렌더링 업데이트 (브라우저에만 해당)
if (shouldRender()) {
render();
}
// 다음 매크로태스크 실행 준비
if (taskQueue.isNotEmpty()) {
task = taskQueue.dequeue();
pushToCallStack(task);
} else {
// 대기
wait();
}
}
브라우저와 Node.js의 주요 차이점
브라우저와 Node.js는 둘 다 이벤트 루프를 사용하지만, 구현 방식에 차이가 있습니다:
- 브라우저: 단일 이벤트 루프 + 렌더링 엔진과 통합
- Node.js: libuv 라이브러리 기반 이벤트 루프 + 여러 단계의 큐(phases)로 구성:
- timers
- pending callbacks
- idle, prepare
- poll
- check
- close callbacks
이러한 아키텍처는 JavaScript가 싱글 스레드임에도 불구하고 효율적인 비동기 처리를 가능하게 하며, 백그라운드 스레드풀을 활용하여 CPU 바운드 작업과 I/O 바운드 작업을 모두 효과적으로 처리합니다.