전체 목록
JavaScriptMedium#03

이벤트 루프(Event Loop)와 비동기 처리 흐름을 설명해주세요.

#JS#비동기#이벤트루프
힌트

Call Stack, Web APIs, Callback Queue, Microtask Queue의 역할을 떠올려보세요.

정답 및 해설

이벤트 루프(Event Loop)와 비동기 처리 흐름을 설명해주세요.

JavaScript는 단일 스레드(Single Thread) 언어입니다. 즉, 한 번에 하나의 작업만 처리할 수 있습니다. 그럼에도 네트워크 요청, 타이머, 사용자 입력 등 비동기 작업을 처리할 수 있는 이유가 바로 이벤트 루프 덕분입니다. 이벤트 루프는 JavaScript 런타임이 어떻게 비동기 작업을 스케줄링하고 실행하는지 결정하는 핵심 메커니즘입니다.

JavaScript 런타임 구성 요소

이벤트 루프를 이해하려면 먼저 런타임을 구성하는 요소들을 알아야 합니다.

┌─────────────────────────────────────────┐
│           JavaScript Engine             │
│  ┌─────────────┐   ┌─────────────────┐  │
│  │  Call Stack  │   │      Heap       │  │
│  │             │   │  (객체 메모리)   │  │
│  └─────────────┘   └─────────────────┘  │
└─────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────┐
│              Web APIs / Node APIs        │
│  (setTimeout, fetch, DOM events, ...)   │
└─────────────────────────────────────────┘
         │
         ▼
┌──────────────────┐   ┌──────────────────┐
│  Microtask Queue │   │  Callback Queue  │
│  (Promise, qMT)  │   │ (setTimeout, ...) │
└──────────────────┘   └──────────────────┘
         │                      │
         └──────────┬───────────┘
                    ▼
              Event Loop

Call Stack (콜 스택)

함수 호출이 쌓이는 LIFO(Last In, First Out) 자료구조입니다. 현재 실행 중인 함수들이 여기에 놓입니다.

function greet(name) {
  console.log('Hello, ' + name);
}

function main() {
  greet('Alice');
}

main();
// 콜 스택 변화:
// 1. main() 추가
// 2. greet('Alice') 추가
// 3. console.log 추가 → 실행 → 제거
// 4. greet() 제거
// 5. main() 제거

Web APIs

setTimeout, fetch, addEventListener 등은 JavaScript 엔진이 아닌 **브라우저(또는 Node.js)**가 제공하는 API입니다. 비동기 작업을 이쪽에 위임하여 콜 스택을 블로킹하지 않습니다.


큐(Queue)의 종류

Callback Queue (Macrotask Queue)

setTimeout, setInterval, I/O 콜백 등이 완료되면 여기에 쌓입니다.

Microtask Queue

Promise.then() / .catch() / .finally() 핸들러, queueMicrotask(), MutationObserver 콜백이 여기에 쌓입니다.

핵심: 이벤트 루프는 콜 스택이 비면, Microtask Queue를 먼저, 그 다음 Callback Queue를 처리합니다.


이벤트 루프 동작 원리

이벤트 루프의 한 사이클(Tick)은 다음과 같이 동작합니다.

  1. 콜 스택에서 현재 실행 중인 태스크를 완료합니다.
  2. Microtask Queue가 빌 때까지 모든 마이크로태스크를 처리합니다.
  3. 필요하면 화면을 렌더링합니다 (브라우저).
  4. Callback Queue에서 태스크 하나를 가져와 콜 스택에 넣습니다.
  5. 1번으로 돌아갑니다.

실행 순서 예제

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

// 출력 순서: 1 → 4 → 3 → 2

단계별 분석:

  1. console.log('1') → 콜 스택 실행 → 출력: 1
  2. setTimeout(cb, 0) → Web APIs로 위임, 0ms 후 Callback Queue에 콜백 추가
  3. Promise.resolve().then(cb) → Microtask Queue에 콜백 추가
  4. console.log('4') → 콜 스택 실행 → 출력: 4
  5. 콜 스택 비워짐 → Microtask Queue 처리 → 출력: 3
  6. Callback Queue 처리 → 출력: 2

더 복잡한 예제

Microtask가 Macrotask보다 먼저 실행되는 이유

console.log('start');

setTimeout(() => {
  console.log('timeout 1');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('promise 1');
    return Promise.resolve();
  })
  .then(() => {
    console.log('promise 2');
  });

setTimeout(() => {
  console.log('timeout 2');
}, 0);

console.log('end');

// 출력:
// start
// end
// promise 1
// promise 2
// timeout 1
// timeout 2

Promise 체인에서 .then() 하나가 실행될 때마다 다음 .then()이 Microtask Queue에 추가됩니다. Microtask Queue가 완전히 빌 때까지 Callback Queue는 처리되지 않습니다.

async/await와 이벤트 루프

async/await는 내부적으로 Promise를 사용하므로 같은 규칙이 적용됩니다.

async function fetchData() {
  console.log('fetch start'); // 동기
  const result = await Promise.resolve('data'); // 이 이후는 마이크로태스크
  console.log('fetch end:', result);
}

console.log('before');
fetchData();
console.log('after');

// 출력:
// before
// fetch start
// after
// fetch end: data

await 이후의 코드는 Microtask Queue에 들어가므로, fetchData() 호출 이후의 동기 코드('after')가 먼저 실행됩니다.


스택 오버플로우와 블로킹

콜 스택 블로킹 예

// 나쁜 예 — 무거운 연산이 콜 스택을 점유
function heavyWork() {
  let sum = 0;
  for (let i = 0; i < 1_000_000_000; i++) {
    sum += i;
  }
  return sum;
}

heavyWork(); // 이 동안 UI가 완전히 멈춤

해결책 — setTimeout으로 청크 분할

function heavyWorkAsync(callback) {
  let sum = 0;
  let i = 0;

  function chunk() {
    const end = Math.min(i + 1_000_000, 1_000_000_000);
    for (; i < end; i++) {
      sum += i;
    }
    if (i < 1_000_000_000) {
      setTimeout(chunk, 0); // 이벤트 루프에 제어권 반환
    } else {
      callback(sum);
    }
  }

  chunk();
}

정리

구성 요소역할
Call Stack현재 실행 중인 함수들의 스택
Web APIs비동기 작업을 처리하는 브라우저/런타임 API
Microtask QueuePromise 콜백, 높은 우선순위
Callback QueuesetTimeout 등, 낮은 우선순위
Event Loop콜 스택이 비면 큐에서 작업을 꺼내 실행

이벤트 루프를 이해하면 코드의 실행 순서를 예측하고, 불필요한 블로킹을 방지하며, 성능 최적화에 대한 근거 있는 결정을 내릴 수 있습니다.