본문 바로가기

개념이해/javascript

JavaScript 이벤트 루프: Task Queue vs Microtask Queue

JavaScript는 싱글 스레드 언어입니다.

그런데 어떻게 비동기 작업을 처리할 수 있을까요?

**이벤트 루프(Event Loop)**를 사용할 수 있습니다.

이벤트 루프란?

이벤트 루프는 JavaScript 엔진이 비동기 작업을 처리하는 메커니즘입니다. 코드 실행을 관리하고, 대기 중인 작업들을 순서대로 처리합니다.

핵심 구성 요소

┌─────────────┐
│ Call Stack  │ ← 현재 실행 중인 코드
└─────────────┘
       ↓
┌─────────────────┐
│ Microtask Queue │ ← Promise, queueMicrotask (우선순위 높음)
└─────────────────┘
       ↓
┌─────────────────┐
│   Task Queue    │ ← setTimeout, setInterval (우선순위 낮음)
└─────────────────┘

1. Call Stack (콜 스택)

현재 실행 중인 함수들이 쌓이는 곳입니다.

function first() {
  console.log('첫 번째');
  second();
}

function second() {
  console.log('두 번째');
}

first();

// 실행 순서:
// 1. first() 스택에 push
// 2. console.log('첫 번째') 실행 후 pop
// 3. second() 스택에 push
// 4. console.log('두 번째') 실행 후 pop
// 5. second() pop
// 6. first() pop

2. Task Queue (Macro Task Queue)

대표 API:

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • I/O 작업
  • UI 렌더링
setTimeout(() => {
  console.log('Task Queue에서 실행');
}, 0);

3. Microtask Queue

대표 API:

  • Promise.then(), catch(), finally()
  • queueMicrotask()
  • async/await
  • MutationObserver
Promise.resolve().then(() => {
  console.log('Microtask Queue에서 실행');
});

이벤트 루프 실행 순서

핵심 규칙

1. Call Stack의 모든 동기 코드 실행
2. Microtask Queue의 모든 작업 실행
3. Task Queue에서 하나의 작업 실행
4. 2번으로 돌아가기

중요: Microtask는 Task보다 항상 먼저 실행됩니다!


실전 예제

예제 1: 기본 개념

console.log('1'); // 동기

setTimeout(() => {
  console.log('2'); // Task Queue
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // Microtask Queue
});

console.log('4'); // 동기

// 출력: 1, 4, 3, 2

실행 과정:

  1. console.log('1') 즉시 실행 → 1 출력
  2. setTimeout → Task Queue에 등록
  3. Promise → Microtask Queue에 등록
  4. console.log('4') 즉시 실행 → 4 출력
  5. Call Stack 비움
  6. Microtask Queue 확인3 출력
  7. Task Queue 확인2 출력

예제 2: Promise 체이닝

console.log('Start');

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

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

console.log('End');

// 출력: Start, End, Promise 1, Promise 2, Timeout 1

포인트:

  • .then() 체이닝도 각각 Microtask로 처리됩니다
  • 모든 Microtask가 끝나야 Task가 실행됩니다

예제 3: 중첩된 비동기 (복잡)

console.log('Start');

setTimeout(() => {
  console.log('Timeout 1');
  Promise.resolve().then(() => {
    console.log('Promise in Timeout');
  });
}, 0);

Promise.resolve()
  .then(() => {
    console.log('Promise 1');
    setTimeout(() => {
      console.log('Timeout 2');
    }, 0);
  })
  .then(() => {
    console.log('Promise 2');
  });

console.log('End');

// 출력: Start, End, Promise 1, Promise 2, 
//       Timeout 1, Promise in Timeout, Timeout 2

핵심:

  • Task 실행 중에 Microtask가 생성되면, 해당 Task 종료 후 즉시 Microtask를 처리합니다
  • 다음 Task로 넘어가기 전에 Microtask Queue를 비웁니다

실무에서의 활용

1. async/await

async function fetchData() {
  console.log('1');
  
  await fetch('/api/data'); // Microtask
  
  console.log('2'); // await 이후는 .then()처럼 동작
}

console.log('3');
fetchData();
console.log('4');

// 출력: 3, 1, 4, 2

await의 동작:

  • await 이전 코드는 동기 실행
  • await는 Promise를 기다림
  • await 이후 코드는 Microtask로 등록

2. 순서 보장이 필요한 경우

// ❌ 순서 보장 안 됨
function saveData() {
  setTimeout(() => saveToDatabase(), 0);
  setTimeout(() => sendNotification(), 0);
}

// ✅ 순서 보장
async function saveData() {
  await saveToDatabase();
  await sendNotification();
}

!! 정리

Task Queue (Macro Task)

  • setTimeout, setInterval
  • 우선순위 낮음
  • 한 번에 하나씩 처리

Microtask Queue

  • Promise, async/await
  • 우선순위 높음
  • 큐가 빌 때까지 모두 처리

실행 순서

동기 코드 → Microtask Queue → Task Queue → 반복

 

이벤트 루프는 JavaScript의 비동기 동작을 이해하는 핵심 개념입니다.

처음에는 복잡해 보이지만, **"Microtask가 Task보다 우선순위가 높다"**는 핵심만 기억하면 대부분의 상황을 이해할 수 있습니다.

실제 코드를 작성하며 직접 실행 순서를 예측해보는 연습을 하면, 금방 익숙해질 것입니다!


참고 자료

반응형