이벤트 루프(Event Loop)

자바스크립트 이벤트 루프는 자바스크립트의 비동기 처리 모델의 핵심 메커니즘으로, 단일 스레드 언어인 자바스크립트가 어떻게 비동기 작업을 효율적으로 처리하는지 설명하는 개념이다.

이벤트 루프(Event Loop)는 자바스크립트의 비동기 작업을 관리하고 실행하는 핵심 메커니즘으로, 싱글 스레드 언어인 자바스크립트가 비동기적으로 동작할 수 있게 해주는 중요한 요소이다.

끊임없이 실행되는 프로세스로, 콜 스택이 비어있는지 확인하고 태스크 큐에서 콜백 함수를 가져와 실행하며, 자바스크립트 엔진이 아닌 호스팅 환경(브라우저 또는 Node.js)에서 제공되는 메커니즘이다. 또한, 비동기 작업의 완료 및 해당 콜백의 실행을 조정한다.

콜 스택, 힙, 태스크 큐, 마이크로태스크 큐, 그리고 이벤트 루프의 상호작용을 이해하면 자바스크립트 애플리케이션의 동작 방식을 더 깊이 이해하고, 더 효율적인 비동기 코드를 작성할 수 있다.

이벤트 루프의 이해는 콜백 지옥, Promise 체인, async/await와 같은 비동기 패턴을 효과적으로 활용하는 데 필수적인 지식이다.

자바스크립트의 실행 환경 구성요소

자바스크립트의 실행 환경은 다음과 같은 주요 구성요소로 이루어져 있다:

  1. 콜 스택(Call Stack): 함수 호출을 기록하는 자료구조로, 현재 실행 중인 코드의 위치를 추적한다.
  2. 힙(Heap): 객체가 할당되는 메모리 공간이다.
  3. 태스크 큐(Task Queue): 비동기 작업의 콜백 함수가 대기하는 큐.
  4. 마이크로태스크 큐(Microtask Queue): Promise의 콜백과 같은 마이크로태스크가 대기하는 큐.
  5. 이벤트 루프(Event Loop): 콜 스택과 태스크 큐를 모니터링하며 비동기 작업을 관리.

Event Loop
https://blog.kakaocdn.net/dn/bEeJN4/btsabeBnUWX/exb9jS9LXWWW7oM1Yk832K/img.png

이벤트 루프의 작동 원리

이벤트 루프는 다음과 같은 간단한 규칙으로 작동한다:

  1. 콜 스택이 비어있는지 확인한다.
  2. 콜 스택이 비어있다면, 마이크로태스크 큐에서 작업을 꺼내 콜 스택에 추가한다.
  3. 마이크로태스크 큐가 비어있다면, 태스크 큐에서 작업을 꺼내 콜 스택에 추가한다.
  4. 콜 스택에 추가된 함수가 실행되며, 이 과정이 반복된다.

이러한 방식으로 자바스크립트는 단일 스레드임에도 불구하고 비동기 작업을 효율적으로 처리할 수 있다.

이벤트 루프 세부 작동 과정 분석

아래 예제를 통해 이벤트 루프의 세부 작동 과정을 분석:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
console.log('시작');

setTimeout(() => {
  console.log('타임아웃 콜백');
}, 0);

Promise.resolve().then(() => {
  console.log('프로미스 콜백');
});

console.log('종료');

이 코드의 실행 순서는 다음과 같다:

  1. console.log('시작') 이 콜 스택에 추가되고 실행된다. 실행 후 콜 스택에서 제거된다.
  2. setTimeout 함수가 콜 스택에 추가되고 실행된다. Web API에 의해 타이머가 설정되고, 타이머가 완료되면 콜백 함수가 태스크 큐에 추가된다.
  3. Promise.resolve().then() 이 실행되며, 프로미스가 이미 이행된 상태이므로 콜백 함수는 마이크로태스크 큐에 추가된다. 니다.
  4. console.log('종료') 가 콜 스택에 추가되고 실행된다. 실행 후 콜 스택에서 제거된다.
  5. 이제 콜 스택이 비었으므로, 이벤트 루프는 마이크로태스크 큐를 확인하고 프로미스 콜백을 콜 스택에 추가한다.
  6. 프로미스 콜백이 실행되고 console.log('프로미스 콜백') 이 출력된다.
  7. 마이크로태스크 큐가 비었으므로, 이벤트 루프는 태스크 큐를 확인하고 타임아웃 콜백을 콜 스택에 추가한다.
  8. 타임아웃 콜백이 실행되고 console.log('타임아웃 콜백') 이 출력된다.

따라서 최종 출력 결과는 다음과 같다:

1
2
3
4
시작
종료
프로미스 콜백
타임아웃 콜백

마이크로태스크 (Microtasks)와 매크로태스크 (Macrotasks)

이벤트 루프는 **마이크로태스크(Microtask)**와 매크로태스크(Macrotask) 두 가지 유형의 태스크를 처리한다:

이벤트 루프의 한 사이클에서 모든 마이크로태스크는 매크로태스크보다 먼저 처리된다. 즉, 마이크로태스크 큐가 완전히 비워진 후에야 매크로태스크 큐에서 다음 작업을 가져온다.

마이크로태스크 (Microtasks)

마이크로태스크는 현재 실행 중인 스크립트나 태스크가 완료된 직후에 실행되는 작은 태스크.
이들은 높은 우선순위를 가지며, 다른 매크로태스크나 렌더링 작업보다 먼저 처리된다.

주요 특징:

예시:

매크로태스크 (Macrotasks)

매크로태스크는 일반적인 비동기 작업을 나타내며, 마이크로태스크보다 낮은 우선순위를 가진다.
이들은 마이크로태스크 큐가 비어있을 때만 실행된다.

주요 특징:

예시:

마이크로태스크 (Microtasks)와 매크로태스크 (Macrotasks) 비교교

구분마이크로태스크 (Microtask)매크로태스크 (Macrotask)
우선순위높음 (매크로태스크보다 먼저 실행)낮음
실행 시점현재 매크로태스크 완료 직후다음 이벤트 루프 사이클
주요 예시Promise.then/catch/finally
process.nextTick
queueMicrotask
MutationObserver
setTimeout/setInterval
setImmediate
requestAnimationFrame
I/O 작업
UI 렌더링
실행 특성큐의 모든 태스크가 완료될 때까지 연속 실행한 번에 하나씩 실행
메모리 사용일반적으로 더 적은 메모리 사용상대적으로 더 많은 메모리 사용
사용 목적즉각적인 상태 업데이트
데이터 일관성 유지
무거운 계산
I/O 작업
타이밍 기반 작업
에러 처리동기적으로 처리 가능try-catch로 직접 처리 불가
실행 컨텍스트현재 실행 컨텍스트 유지새로운 실행 컨텍스트 생성
디버깅상대적으로 쉬움비동기 특성으로 인해 더 복잡
태스크 취소일반적으로 불가능clearTimeout 등으로 가능

이제 각각의 특징을 자세히 살펴보자:

특성마이크로태스크매크로태스크
실행 시점현재 작업 완료 직후, 렌더링 전마이크로태스크 큐가 비어있을 때
처리 방식큐가 비워질 때까지 연속 실행한 번에 하나씩 실행
큐 처리모든 마이크로태스크 처리 후 매크로태스크로 이동각 매크로태스크 사이에 마이크로태스크 확인

브라우저 환경과 Node.js 환경의 차이

자바스크립트의 **이벤트 루프(Event Loop)**는 브라우저 환경과 Node.js 환경에서 비동기 작업을 처리하는 핵심 메커니즘이다.
두 환경 모두 이벤트 루프를 사용하지만, 동작 방식과 처리하는 작업의 종류에서 차이가 있다.

브라우저 환경

브라우저는 사용자 인터페이스(UI)와의 상호작용을 처리하기 위해 이벤트 루프를 활용한다.

주요 구성 요소는 다음과 같다:

브라우저에서 이벤트 루프는 다음과 같은 단계로 작동한다:

  1. 매크로태스크 하나를 실행한다.
  2. 모든 마이크로태스크를 실행한다.
  3. UI 렌더링을 업데이트한다.
  4. 다음 매크로태스크를 실행한다.

이벤트 루프는 콜 스택이 비어 있을 때 마이크로태스크 큐를 우선적으로 처리하고, 이후 태스크 큐의 작업을 처리한다. 이를 통해 브라우저는 사용자 이벤트, 타이머, 네트워크 요청 등의 비동기 작업을 효율적으로 관리한다.

Node.js 환경

Node.js는 서버 사이드 자바스크립트 환경으로, 이벤트 루프를 통해 비동기 I/O 작업을 처리한다.
Node.js의 이벤트 루프는 libuv 라이브러리에 의해 구현되어 있으며, 다음과 같은 단계로 구성된다.

  1. 타이머(Timers): setTimeout()setInterval()로 예약된 콜백을 실행한다.
  2. 펜딩 콜백(Pending Callbacks): 이전 반복에서 지연된 I/O 콜백을 실행한다.
  3. 아이들, 준비(Idle, Prepare): 내부적으로 사용되는 단계로, 일반적인 애플리케이션 개발에서는 직접적으로 다루지 않는다.
  4. 폴(Poll): 새로운 I/O 이벤트를 가져오고, I/O 관련 콜백을 실행한다. 이 단계에서는 파일 읽기/쓰기, 네트워크 작업 등의 콜백이 처리된다.
  5. 체크(Check): setImmediate()로 예약된 콜백을 실행한다.
  6. 클로즈 콜백(Close Callbacks): 소켓이나 핸들이 갑작스럽게 종료될 때 호출되는 콜백을 처리한다.

Node.js의 이벤트 루프는 libuv 라이브러리를 기반으로 하며, 다음과 같은 단계로 작동한다:

  1. timers: setTimeoutsetInterval 콜백을 실행한다.
  2. pending callbacks: 이전 루프에서 연기된 I/O 콜백을 실행한다.
  3. idle, prepare: 내부적으로 사용된다.
  4. poll: 새로운 I/O 이벤트를 가져오고 해당 콜백을 실행한다.
  5. check: setImmediate 콜백을 실행한다.
  6. close callbacks: 소켓 종료와 같은 ‘close’ 이벤트 콜백을 실행한다.

각 단계 사이에 마이크로태스크 큐에 있는 모든 작업을 실행한다.

주요 차이점

이러한 차이점에도 불구하고, 두 환경 모두 이벤트 루프를 통해 비동기 작업을 효율적으로 처리하여 높은 성능과 응답성을 제공한다.

실제 예제를 통한 이해

다음과 같은 복잡한 예제를 통해 이벤트 루프의 작동 방식을 더 깊이 이해해 보자:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
console.log('1');

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

new Promise((resolve) => {
  console.log('4');
  resolve();
}).then(() => {
  console.log('5');
  setTimeout(() => {
    console.log('6');
  }, 0);
});

console.log('7');

이 코드의 실행 순서는 다음과 같다:

  1. console.log('1') 실행 (동기 코드)
  2. setTimeout 함수 실행, 콜백은 태스크 큐에 추가
  3. 새 Promise 생성 및 console.log('4') 실행 (동기 코드)
  4. Promise가 즉시 이행되어 .then 콜백이 마이크로태스크 큐에 추가
  5. console.log('7') 실행 (동기 코드)
  6. 콜 스택이 비었으므로, 마이크로태스크 큐에서 첫 번째 작업(Promise 콜백) 실행
  7. console.log('5') 실행 및 새 setTimeout 콜백이 태스크 큐에 추가
  8. 마이크로태스크 큐가 비었으므로, 태스크 큐에서 첫 번째 작업(첫 번째 setTimeout 콜백) 실행
  9. console.log('2') 실행 및 새 Promise 콜백이 마이크로태스크 큐에 추가
  10. 마이크로태스크 큐에서 새 작업(두 번째 Promise 콜백) 실행
  11. console.log('3') 실행
  12. 마이크로태스크 큐가 비었으므로, 태스크 큐에서 다음 작업(두 번째 setTimeout 콜백) 실행
  13. console.log('6') 실행

최종 출력 결과:

1
2
3
4
5
6
7
1
4
7
5
2
3
6

이벤트 루프의 성능 고려사항

이벤트 루프를 효율적으로 활용하려면 다음과 같은 성능 고려사항을 염두에 두어야 한다:

  1. 긴 작업 피하기: 콜 스택에서 실행되는 작업이 오래 걸리면 전체 이벤트 루프가 차단된다.
    오래 걸리는 계산은 Web Workers 또는 작은 작업으로 분할해야 한다.
  2. 마이크로태스크 큐 과부하 방지: 마이크로태스크가 새로운 마이크로태스크를 계속 생성하면 매크로태스크가 실행되지 못할 수 있다.
  3. setTimeout(0) vs Promise: 즉시 실행해야 하는 비동기 작업은 setTimeout(fn, 0)보다 Promise.resolve().then(fn) 또는 queueMicrotask(fn)을 사용하는 것이 더 효율적이다.
  4. 렌더링 최적화: 브라우저에서 애니메이션이나 UI 업데이트는 requestAnimationFrame을 사용하여 렌더링 단계와 동기화하는 것이 좋다.

참고 및 출처