본문으로 건너뛰기
JavaScript 이벤트 루프 다이어그램

# JavaScript 이벤트 루프 설명: 비동기 동작의 핵심 원리

Table of Contents

이벤트 루프(Event Loop)는 JavaScript가 단일 스레드 언어임에도 불구하고 비동기 작업을 효율적으로 처리할 수 있게 해주는 핵심 메커니즘입니다. 이벤트 루프를 제대로 이해하면 JavaScript의 비동기 동작을 완벽히 파악하고, 성능 문제를 진단하며, 더 효율적인 코드를 작성할 수 있습니다.

기본 개념

JavaScript는 기본적으로 단일 스레드(Single Thread) 언어이지만, 이벤트 루프와 비동기 API를 통해 동시성(Concurrency)을 구현합니다.

console.log('Start');

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

console.log('End');

// 출력 결과:
// Start
// End
// Timeout

setTimeout의 지연 시간이 0임에도 불구하고 마지막에 실행될까요? 이것이 바로 이벤트 루프의 작동 방식 때문입니다.

:::important 핵심 개념 JavaScript 런타임은 콜 스택(Call Stack), 태스크 큐(Task Queue), 그리고 **이벤트 루프(Event Loop)**로 구성됩니다. 이벤트 루프는 콜 스택이 완전히 비어있을 때만 큐에 대기 중인 작업을 가져와 실행합니다. :::

콜 스택 (Call Stack)

콜 스택은 함수 호출을 기록하는 데이터 구조로, LIFO(Last In, First Out) 방식으로 작동합니다.

function first() {
  console.log('first 시작');
  second();
  console.log('first 종료');
}

function second() {
  console.log('second 시작');
  third();
  console.log('second 종료');
}

function third() {
  console.log('third 실행');
}

first();

// 콜 스택 상태 변화:
// 1. [first]
// 2. [first, second]
// 3. [first, second, third]
// 4. [first, second] (third 완료 후 제거)
// 5. [first] (second 완료 후 제거)
// 6. [] (first 완료 후 제거)

// 출력:
// first 시작
// second 시작
// third 실행
// second 종료
// first 종료

스택 오버플로우 (Stack Overflow)

function recursiveFunction() {
  recursiveFunction(); // 무한 재귀 호출
}

recursiveFunction();
// Uncaught RangeError: Maximum call stack size exceeded

해결 방법: 비동기 처리를 통해 스택을 비우고 다시 실행하도록 변경합니다.

function safeRecursive(count = 0) {
  if (count > 1000) return;

  setTimeout(() => {
    console.log(count);
    safeRecursive(count + 1);
  }, 0);
}

safeRecursive(); // 스택 오버플로우 없이 실행됨

태스크 큐 (Task Queue / Callback Queue)

setTimeout, setInterval 등 비동기 작업의 콜백 함수가 대기하는 곳입니다.

console.log('1');

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

setTimeout(() => {
  console.log('3');
}, 100);

console.log('4');

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

// 실행 순서:
// 1. console.log('1') 실행 (콜 스택)
// 2. setTimeout 콜백 등록 (Web API로 이동)
// 3. setTimeout 콜백 등록 (Web API로 이동)
// 4. console.log('4') 실행 (콜 스택)
// 5. 콜 스택이 비었으므로, 첫 번째 setTimeout 콜백 실행 (태스크 큐 -> 콜 스택)
// 6. 100ms 후, 두 번째 setTimeout 콜백 실행

:::tip 타이머의 정확성 setTimeout(fn, 0)은 즉시 실행을 보장하지 않습니다. 브라우저 정책에 따라 최소 지연 시간(약 4ms)이 존재하며, 무엇보다 콜 스택이 비워져야만 실행될 수 있습니다. :::

마이크로태스크 큐 (Microtask Queue)

Promise의 .then(), .catch(), .finally() 핸들러나 MutationObserver 같은 마이크로태스크는 일반 태스크보다 더 높은 우선순위를 가집니다.

console.log('1');

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

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

console.log('4');

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

// 마이크로태스크(Promise)가 태스크(setTimeout)보다 먼저 실행됩니다.

마이크로태스크 vs 태스크 실행 순서

console.log('Script start');

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

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

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

console.log('Script end');

// 출력 순서:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout 1
// setTimeout 3
// setTimeout 2

:::important 우선순위 규칙 이벤트 루프는 다음 순서로 작업을 처리합니다.

  1. 콜 스택의 모든 동기 코드 실행
  2. 마이크로태스크 큐의 모든 작업 실행 (중간에 추가된 것도 포함)
  3. 태스크 큐에서 하나의 작업 실행
  4. 렌더링 업데이트 (필요한 경우)
  5. 2번으로 돌아가 반복 :::

Web APIs

브라우저가 제공하는 비동기 API들로, JavaScript 엔진 외부에서 실행됩니다.

// 1. setTimeout / setInterval
const timerId = setTimeout(() => {
  console.log('1초 후 실행');
}, 1000);

clearTimeout(timerId); // 취소 가능

// 2. fetch (네트워크 요청)
fetch('https://api.example.com/data')
  .then((response) => response.json())
  .then((data) => console.log(data));

// 3. DOM 이벤트
document.addEventListener('click', (event) => {
  console.log('클릭됨:', event.target);
});

requestAnimationFrame

화면 갱신(렌더링) 주기에 맞춰 실행되는 특별한 큐입니다. 브라우저가 화면을 그리기 직전에 실행됩니다.

console.log('1');

requestAnimationFrame(() => {
  console.log('2 - rAF');
});

setTimeout(() => {
  console.log('3 - setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('4 - Promise');
});

console.log('5');

// 일반적인 출력: 1, 5, 4, 2, 3
// (브라우저와 상황에 따라 rAF와 setTimeout의 순서는 달라질 수 있습니다)

Node.js의 이벤트 루프 (process.nextTick)

Node.js 환경에는 마이크로태스크보다도 더 높은 우선순위를 가진 process.nextTick 큐가 존재합니다.

console.log('1');

process.nextTick(() => {
  console.log('2 - nextTick');
});

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

setTimeout(() => {
  console.log('4 - setTimeout');
}, 0);

console.log('5');

// 출력: 1, 5, 2, 3, 4
// nextTick이 Promise보다 항상 먼저 실행됩니다.

정리

이벤트 루프는 JavaScript 비동기 프로그래밍의 핵심 엔진입니다.

  1. 콜 스택: 동기 코드가 실행되는 곳입니다.
  2. 마이크로태스크 큐: Promise 등이 대기하며, 콜 스택이 비면 가장 먼저, 모두 실행됩니다.
  3. 태스크 큐: setTimeout 등이 대기하며, 마이크로태스크가 다 비워진 후 실행됩니다.
  4. 렌더링: 브라우저는 적절한 시점에 화면을 업데이트합니다.

이 우선순위를 이해하면 복잡한 비동기 로직의 실행 순서를 정확히 예측하고 제어할 수 있습니다.

이 글 공유하기:
My avatar

글을 마치며

이 글이 도움이 되었기를 바랍니다. 궁금한 점이나 의견이 있다면 댓글로 남겨주세요.

더 많은 기술 인사이트와 개발 경험을 공유하고 있으니, 다른 포스트도 확인해보세요.

유럽살며 여행하며 코딩하는 노마드의 여정을 함께 나누며, 함께 성장하는 개발자 커뮤니티를 만들어가요! 🚀


관련 포스트