자바스크립트, 어떻게 멈추지 않고 실행될까? 🤔
자바스크립트는 단일 스레드(single-threaded) 언어임에도 불구하고, 웹사이트는 수많은 네트워크 요청, 타이머, 사용자 이벤트를 동시에 처리하며 멈추지 않습니다. 이 마법 같은 동시성(concurrency)의 비밀은 바로 이벤트 루프(Event Loop) 시스템에 있습니다. 이 가이드에서는 콜 스택(Call Stack), Web API, 태스크 큐(Task Queue), 그리고 마이크로태스크 큐(Microtask Queue)가 어떻게 유기적으로 연결되어 비동기 작업을 처리하는지 단계별로 분석합니다. 이 원리를 이해하는 것은 단순히 면접을 준비하는 것을 넘어, 복잡한 애플리케이션의 동작을 예측하고 디버깅하는 시니어 개발자의 사고방식을 갖추는 첫걸음입니다.

비동기 처리의 4가지 핵심 구성 요소 🧩
콜 스택 (Call Stack) - 실행의 기본 단위
자바스크립트 엔진은 단 하나의 콜 스택을 가집니다. 모든 함수 호출은 이 스택에 push되고, 실행이 완료되면 pop됩니다. 이는 코드가 순차적으로 실행되도록 보장합니다.
Web API - 외부 세계와의 연결고리
setTimeout, fetch, DOM 이벤트 리스너와 같은 기능들은 자바스크립트 엔진 자체의 기능이 아니라 브라우저 또는 Node.js 런타임이 제공하는 Web API입니다. 이 API들은 시간이 오래 걸리는 작업(타이머, 네트워크 요청)을 콜 스택 밖에서 처리하여 메인 스레드가 블로킹되는 것을 방지합니다.
태스크 큐 & 마이크로태스크 큐 - 대기실의 두 가지 등급
Web API에서 작업이 완료되면 콜백 함수는 바로 콜 스택으로 가지 않고 큐(Queue)에 저장됩니다. 여기서 중요한 것은 큐의 종류가 두 가지라는 점입니다.
graph TD
A[Call Stack] -->|Empty?| B{Event Loop}
B -->|Yes| C[Microtask Queue]
B -->|No| A
C -->|Higher Priority| A
D[Task Queue] -->|Lower Priority| B
E[Web APIs] -->|Callback Ready| D
F[Promise/MutationObserver] -->|Callback Ready| C

이벤트 루프의 작동 메커니즘과 우선순위 🎯
마이크로태스크 큐의 절대적 우선권
이벤트 루프는 콜 스택이 비어있는 순간, 항상 마이크로태스크 큐를 먼저 확인합니다. 마이크로태스크 큐가 완전히 비워질 때까지 태스크 큐는 실행되지 않습니다. 이 우선순위는 Promise, MutationObserver, queueMicrotask() API에 의해 생성된 콜백에 적용됩니다.
함수 기아 현상 (Starvation)
만약 마이크로태스크 큐에 계속해서 새로운 작업이 추가된다면(예: Promise 체인 내에서 새로운 Promise를 생성), 태스크 큐에 있는 setTimeout 콜백은 영원히 실행되지 못할 수 있습니다. 이것을 **함수 기아 현상(Starvation)**이라고 하며, 프론트엔드 면접에서 자주 등장하는 중요한 개념입니다.
실행 순서 비교 표
| 구분 | 마이크로태스크 큐 (Microtask Queue) | 태스크 큐 / 콜백 큐 (Task Queue) |
|---|---|---|
| 우선순위 | 높음 (먼저 실행) | 낮음 (마이크로태스크 큐가 빈 후 실행) |
| 포함 API | Promise.then(), .catch(), .finally(), MutationObserver, queueMicrotask(), async/await 내부 코드 | setTimeout(), setInterval(), fetch() (네트워크 응답), DOM 이벤트 리스너, setImmediate() (Node.js) |
| 특징 | 한 번에 하나씩 처리되며, 큐가 완전히 비워질 때까지 다음 태스크 큐로 넘어가지 않음 | 한 번에 하나의 콜백을 꺼내 실행 |
코드 예시로 보는 실행 순서
console.log('1. 동기 코드');
setTimeout(() => console.log('2. 태스크 큐'), 0);
Promise.resolve().then(() => console.log('3. 마이크로태스크 큐'));
console.log('4. 동기 코드 끝');
// 출력 결과:
// 1. 동기 코드
// 4. 동기 코드 끝
// 3. 마이크로태스크 큐
// 2. 태스크 큐
위 예제에서 setTimeout의 지연 시간이 0ms임에도 불구하고, Promise의 콜백이 먼저 실행되는 것을 확인할 수 있습니다. 이는 마이크로태스크 큐가 태스크 큐보다 항상 높은 우선순위를 갖기 때문입니다. 레딧(Reddit)의 JavaScript 커뮤니티에서는 이 우선순위 차이를 이해하지 못해 발생하는 예상치 못한 버그에 대한 논의가 자주 이루어집니다.

정리: 비동기 자바스크립트를 지배하는 4가지 법칙 📝
- 모든 동기 코드는 콜 스택에서 즉시 실행됩니다.
- Web API가 시간이 오래 걸리는 작업을 대신 처리하여 메인 스레드를 차단하지 않습니다.
- 작업 완료 후 콜백은 종류에 따라 마이크로태스크 큐(고우선순위) 또는 태스크 큐(저우선순위) 에 저장됩니다.
- 이벤트 루프는 콜 스택이 비었을 때, 마이크로태스크 큐를 먼저 완전히 비운 후 태스크 큐를 처리합니다.
이 4가지 법칙을 이해하면 async/await, Promise, setTimeout이 혼합된 복잡한 코드의 실행 결과를 정확히 예측할 수 있습니다.
📅 정보 기준일: 2024-05-24
함께 보면 좋은 글
