자바스크립트, 어떻게 멈추지 않고 실행될까? 🤔

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

JavaScript Event Loop diagram showing call stack and queue interaction Tech Illustration

비동기 처리의 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

Visual representation of microtask queue and callback queue priority Technology Concept Image

이벤트 루프의 작동 메커니즘과 우선순위 🎯

마이크로태스크 큐의 절대적 우선권

이벤트 루프는 콜 스택이 비어있는 순간, 항상 마이크로태스크 큐를 먼저 확인합니다. 마이크로태스크 큐가 완전히 비워질 때까지 태스크 큐는 실행되지 않습니다. 이 우선순위는 Promise, MutationObserver, queueMicrotask() API에 의해 생성된 콜백에 적용됩니다.

함수 기아 현상 (Starvation)

만약 마이크로태스크 큐에 계속해서 새로운 작업이 추가된다면(예: Promise 체인 내에서 새로운 Promise를 생성), 태스크 큐에 있는 setTimeout 콜백은 영원히 실행되지 못할 수 있습니다. 이것을 **함수 기아 현상(Starvation)**이라고 하며, 프론트엔드 면접에서 자주 등장하는 중요한 개념입니다.

실행 순서 비교 표

구분마이크로태스크 큐 (Microtask Queue)태스크 큐 / 콜백 큐 (Task Queue)
우선순위높음 (먼저 실행)낮음 (마이크로태스크 큐가 빈 후 실행)
포함 APIPromise.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 커뮤니티에서는 이 우선순위 차이를 이해하지 못해 발생하는 예상치 못한 버그에 대한 논의가 자주 이루어집니다.

Asynchronous JavaScript code execution flow diagram Digital Device Concept

정리: 비동기 자바스크립트를 지배하는 4가지 법칙 📝

  1. 모든 동기 코드는 콜 스택에서 즉시 실행됩니다.
  2. Web API가 시간이 오래 걸리는 작업을 대신 처리하여 메인 스레드를 차단하지 않습니다.
  3. 작업 완료 후 콜백은 종류에 따라 마이크로태스크 큐(고우선순위) 또는 태스크 큐(저우선순위) 에 저장됩니다.
  4. 이벤트 루프는 콜 스택이 비었을 때, 마이크로태스크 큐를 먼저 완전히 비운 후 태스크 큐를 처리합니다.

이 4가지 법칙을 이해하면 async/await, Promise, setTimeout이 혼합된 복잡한 코드의 실행 결과를 정확히 예측할 수 있습니다.

📅 정보 기준일: 2024-05-24

함께 보면 좋은 글

Server-side JavaScript runtime environment visualization Future Tech Concept

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.