본문 바로가기
프로그래밍 언어/JavaScript

[JavaScript] 이벤트 루프에 대해서...

by BK0625 2025. 3. 26.
반응형

 

 

이벤트 루프는 자바스크립트의 비동기 처리 메커니즘으로, 싱글 스레드 환경에서도 비동기 작업(예: 타이머, 네트워크 요청 등)을 처리할 수 있게 도와주는 비동기 실행 모델이다.

 

자바스크립트는 기본적으로 한 번에 하나의 작업만 처리할 수 있는 싱글 스레드 언어지만, 이벤트 루프 덕분에 비동기 처리가 가능해진다.

 

동작 원리

1. 콜 스택 (Call Stack)

  • 실행 중인 함수들이 쌓이는 스택이다.
  • 가장 위에 있는 함수가 실행되며, 실행이 끝나면 스택에서 제거 된다.

2. 웹 API / 백그라운드 작업

  • 브라우저나 Node.js 런타임이 제공하는 기능 (예: setTimeout, fetch, 이벤트 리스너)이 실행된다.
  • 비동기 작업은 콜 스택에서 바로 실행되지 않고, 백그라운드로 넘겨진다.

3. 태스크 큐(Task Queue) / 콜백 큐

  • 비동기 작업이 완료되면, 해당 콜백 함수가 이 큐에 들어가게 된다.
  • 이벤트 루프가 콜 스택이 비었을 때 큐에서 작업을 하나씩 꺼내 실행한다.

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

  • Promise.then() 이나 process.nextTick() 같은 작업들이 들어가는 큐이다.
  • 일반 태스크 큐보다 우선순위가 높아서 먼저 실행되게 된다.

 

간단한 예시 코드

console.log('Start');

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

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

console.log('End');
  • 실행 순서
Start
End
Promise resolved
Timeout callback
  • 설명
    • console.log(’Start’)와 console.log(’End’)는 동기적으로 실행 (콜 스택)
    • Promise는 마이크로태스크 큐로 이동, 이벤트 루프가 바로 실행
    • setTimeout은 태스크 큐에 들어가고, 이벤트 루프가 다음 틱에 실행

 

이벤트 루프의 한 사이클(Tick)의 의미

이벤트 루프는 끊임없이 반복하면서 다음 순서를 따르게 된다.

  1. 콜 스택(Call Stack) 처리
    1. 현재 실행 중인 동기 코드를 모두 처리해 콜 스택이 완전히 비워질 때까지 실행
  2. 마이크로태스크 큐(Microtask Queue) 처리
    1. 콜 스택이 비워진 후, 마이크로태스크 큐에 있는 작업들(Promise의 then, catch, finally 등)을 모두 실행
  3. 태스크 큐(Task Queue) 처리
    1. 마이크로태스크 큐도 비워졌다면, 이제 이벤트 큐(태스크 큐)에서 작업(예: setTimeout, setInterval)을 꺼내 콜 스택으로 넘긴다.

이 과정을 한 번 완료하면 한 번의 틱(Tick)이 끝난다. 그리고 이 작업이 완료되면 이벤트 루프는 다시 처음으로 돌아가 새로운 틱을 시작해 동일한 과정을 반복한다.

setTimeout의 다음 틱은 언제 실행되나?

setTimeout()으로 예약된 콜백은 최소한 다음과 같은 조건이 충족되어야 실행된다.

  1. 명시된 시간이 지나야 한다.(예: setTimeout(fn,0)은 최소한 0ms 이후 실행 준비)
  2. 현재 콜 스택이 모두 비워져야 한다.
  3. 마이크로태스크 큐에 있는 작업이 모두 처리되어야 한다.

즉 다음 틱이란

  • 현재 실행 중인 동기 코드(콜 스택)가 모두 끝난 이후,
  • 예약된 비동기 작업이 실행될 수 있는 다음 이벤트 루프 사이클을 말한다.

 

이벤트 루프 내부 구조

Node.js 이벤트 루프의 구조는 크게 6단계로 나눌 수 있다. 이 이벤트 루프는 libuv라는 C++ 기반의 라이브러리로 구현되어 있다.

이벤트 루프의 6가지 단계

  1. timers
    1. setTimeout(), setInterval() 의 콜백이 실행된다.
    2. 설정된 시간이 지난 후, 해당 콜백이 큐에 들어간다.
  2. pending callbacks
    1. 일부 비동기 작업(예: 네트워크 요청)에 대한 콜백이 실행된다.
  3. idle, prepare(내부 작업)
    1. 내부적으로 시스템이 준비해야 하는 작업들을 처리(거의 신경 쓸 필요 없음)
  4. poll
    1. I/O 이벤트가 발생할 때까지 대기하고, 발생한 이벤트의 콜백 실행
    2. 예: 파일 읽기, 네트워크 요청 등
    3. 여기서 큐가 비어 있고, 타이머가 준비되지 않았다면 여기서 대기할 수도 있음
  5. check
    1. setImmediate() 로 등록된 콜백이 실행됨
    2. setTimeout() 보다 빠르게 실행될 수 있음
  6. close callbacks
    1. 소켓이나 핸들 같은 리소스가 닫힐 때 실행됨 (예: socket.on(’close’, …) )

우선 순위 정리 (Queue 순서)

  1. process.nextTick() → 최우선 처리 (이벤트 루프 외부에서 실행됨)
  2. Microtasks Queue → Promise 콜백 처리
  3. Event Loop Phase → 위 6단계 순서대로 실행
  4. setImmediate() → 특정 시점에 바로 실행하고 싶을 때 사용
  5. setTimeout() / setInterval() → 타이머 만료 후 실행

이벤트 루프와 멀티 스레딩 (libuv의 역할)

Node.js는 싱글 스레드 기반이지만, 내부적으로는 libuv라는 비동기 I/O 라이브러리를 사용해 멀티스레딩을 지원한다.

  • 파일 시스템 작업, 네트워크 요청, 암호화 작업 등을 백그라운드 스레드 풀에서 처리
  • 처리 결과가 완료되면 해당 콜백이 이벤트 루프의 큐에 들어가 실행됨
const crypto = require('crypto');

console.log('Start');

// CPU 집약적인 작업
crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', () => {
  console.log('Hash calculated.');
});

console.log('End');
  • 이 작업은 libuv의 스레드 풀에서 실행되므로, 메인 스레드는 막히지 않고 다른 작업을 계속 처리할 수 있음

 

심화 개념

1. process.nextTick() vs Promise vs setTimeout()

  • process.nextTick() → Node.js에서 사용되며, 가장 높은 우선순위로, 마이크로태스크보다 먼저 실행.
  • Promise.then() → 마이크로태스크로 실행돼서, 태스크 큐보다 먼저 실행
  • setTimeout() → 태스크 큐에서 실행되며, 위 두 개보다 우선 순위가 낮음

예시 코드

console.log('Start');

process.nextTick(() => console.log('Next Tick'));
Promise.resolve().then(() => console.log('Promise'));
setTimeout(() => console.log('Timeout'), 0);

console.log('End');

출력 순서

Start
End
Next Tick
Promise
Timeout

2. I/O 작업의 이벤트 루프 처리

파일 시스템, 네트워크 요청 같은 I/O 작업도 이벤트 루프에서 처리되지만, 그 과정에서 내부적으로 libuv라는 라이브러리가 동작한다.

  • 예를 들어, 파일 읽기 작업은 별도의 스레드 풀에서 비동기적으로 처리되고, 완료되면 이벤트 루프의 태스크 큐에 콜백이 등록된다.

3. Blocking vs Non-Blocking 코드

  • Blocking code란 동기적으로 동작하는 코드로, 현재 작업이 끝나야 다음 작업이 시작된다.
  • 이벤트 루프가 해당 작업이 끝날 때까지 멈춰 있으므로(block), 다른 요청을 처리할 수 없다.
  • 동기 작업이 너무 오래 걸리면 이벤트 루프가 막혀버려 전체 애플리케이션이 멈추는 문제가 발생할 수 있음.
  • 예를 들어 while(true) {} 같은 무한 루프는 콜 스택을 점유한 채 이벤트 루프가 돌아갈 수 없게 만든다.
  • 따라서 웹 서버 같은 경우, 여러 사용자의 요청을 동시에 처리해야 되기 때문에, Non-blocking I/O가 필수적이다.

4. Timers Phase의 오해

  • setTimeout(fn, 0)이 정확히 0ms 후에 실행된다는 보장은 없다.
  • 최소 대기 시간이 0ms 일 뿐, 이벤트 루프의 상태에 따라 실행 타이밍이 밀릴 수 있다.

5. Garbage Collection과 이벤트 루프

  • 자바스크립트의 가비지 컬렉션도 이벤트 루프 틱 사이사이에 실행되는데, 이 과정에서 짧게나마 앱이 멈출 수 있음. 왠만 하면 거의 느껴지지 않음.

 

 

공부하면서 정리한 내용입니다. 모든 지적 감사히 받겠습니다 :)

반응형