728x90
정의
자바스크립트 이벤트 루프는 코드 실행 관리, 이벤트 수집 및 처리, 대기 중인 작업 실행을 핵심적으로 담당한다.
자바스크립트는 단일 스레드 환경에서 작동하므로 한 번에 하나의 코드만 실행되며, 이벤트 루프는 이러한 실행 순서를 관리하여 비동기 프로그래밍을 가능하게 한다.
예를 들어, 네트워크 요청, 파일 읽기, 타이머 등 다양한 비동기 작업이 올바른 순서로 처리되도록 도와준다.
이벤트 루프 구성 요소
- Call Stack(콜스택) : 현재 실행 중인 함수(또는 스크립트)가 쌓이는 스택 구조
- Event/Task Queue(매크로 태스크 큐): setTimeout, setInterval, I/O 콜백, DOM 이벤트, 그리고 requestAnimationFrame 등의 콜백이 대기하는 큐
- Microtask Queue(마이크로 태스크 큐):
Promise.then()
,async/await
의 후속 처리,MutationObserver
등과 같은 마이크로태스크들이 대기하는 큐 - process.nextTick() (Node.js 전용) : Node.js에서는 ECMAScript 표준의 마이크로태스크와는 별도로 process.nextTick() 함수를 제공한다. 이 함수에 등록된 콜백은 현재 작업이 모두 종료된 후, 다른 마이크로태스크보다 먼저 실행되어 process.nextTick()은 매우 높은 우선순위를 가지며, 사용 시 주의가 필요합니다.
이벤트 루프의 동작 과정
- 콜 스택(Call Stack) 확인
- 자바스크립트는 단일 스레드로 동작하며, 현재 실행 중인 함수(또는 실행 컨텍스트)가 콜 스택에 쌓여 있습니다.
- 이벤트 루프는 콜 스택이 비어 있는지를 지속적으로 관찰합니다.
- 콜 스택이 비어 있지 않으면
- 콜 스택이 비어 있지 않다면, 스택의 맨 위에 있는 함수를 실행합니다. (동기 작업은 실행이 완료될 때까지 블로킹됩니다.)
- 여기에는 Promise.then(), async/await, MutationObserver 등이 포함된다.
- Node.js의 process.nextTick()은 기술적으로 별도의 nextTick 큐에서 관리되지만, 동일 tick 내에서 항상 마이크로태스크보다 먼저 실행되며, 마이크로태스크 큐(및 Node.js의 경우 nextTick 큐)에 대기 중인 모든 작업이 실행될 때까지 계속 처리한다..
- 콜 스택이 비어 있지 않다면, 스택의 맨 위에 있는 함수를 실행합니다. (동기 작업은 실행이 완료될 때까지 블로킹됩니다.)
- 콜 스택이 비어 있는 순간, 먼저 마이크로태스크 큐(Microtask Queue) 를 확인
- Promise.then(), async/await, MutationObserver, Node.js의 process.nextTick() 등이 여기에 해당합니다.
- 마이크로태스크 큐에 대기 중인 모든 작업(콜백)을 전부 실행 완료할 때까지 비우고, 그 과정에서 새로 추가된 마이크로태스크가 있어도 계속 처리합니다.
- 즉, "마이크로태스크 큐가 빌 때까지" 반복 실행합니다.
- 마이크로태스크가 모두 처리된 후, 태스크 큐(매크로 태스크 큐, Callback Queue)를 확인
- setTimeout, setInterval, I/O 콜백, DOM 이벤트, requestAnimationFrame(콜백 등록 시점에 따라 다름) 등이 매크로태스크 큐에 들어갑니다.
- 이 큐에서 맨 앞에 있는 작업을 하나 가져와 콜 스택에 푸시하고 실행합니다.
- 실행(Execution) 및 콜백 처리
- 위에서 스택에 푸시된 함수가 실행되면서, 추가적인 비동기 작업이 발생할 수 있습니다(예: 다음 타이머 등록, 네트워크 요청 등).
- 함수 실행이 종료되어 콜 스택이 다시 비면, 이벤트 루프는 다시 마이크로태스크 -> 매크로태스크 순으로 확인하며 필요한 작업을 처리합니다.
- 이 과정을 계속 반복(Repeat)
- 결과적으로 자바스크립트 엔진은 콜 스택이 빌 때마다 마이크로태스크 큐, 그 다음에 매크로태스크 큐 순으로 작업들을 하나씩(마이크로태스크는 모두) 실행하면서 무한히 순환(Event Loop)합니다.
예제
console.log('시작');
setTimeout(() => {
console.log('매크로 태스크(태스크 큐에 들어갑니다.)');
}, 0);
Promise.resolve().then(() => {
console.log('마이크로 태스크(마이크로 태스크 큐에 들어갑니다.)');
});
console.log('끝');
// 출력 결과
// 시작
// 끝
// 마이크로 태스크(마이크로 태스크 큐에 들어갑니다.)
// 매크로 태스크(태스크 큐에 들어갑니다.)
- 동기 코드 실행
- console.log("시작") → "시작" 출력
- setTimeout(...) 등록 (비동기) → 타이머 콜백은 매크로태스크 큐에 등록
- Promise.resolve().then(...) 등록 (비동기) → then() 콜백은 마이크로태스크 큐에 배치될 예정
- console.log("끝") → "끝" 출력
- 이제 콜 스택이 비어짐
- 마이크로태스크 큐 확인
- Promise의 then() 콜백이 즉시 실행되어 "마이크로 태스크(마이크로태스크 큐 실행)" 출력
- 매크로태스크 큐 확인
- setTimeout 콜백이 실행되어 "매크로 태스크(태스크 큐 실행)" 출력
- 이후 반복
- 다시 콜 스택 비면 마이크로태스크 → 매크로태스크 순으로 확인. 현재는 추가 작업이 없으므로 종료.
역사와 엔진별 특징 변화
브라우저 환경
초기(1990년대 말 ~ 2000년대 초반)
- 주로 각 브라우저 벤더가 독자적으로 구현한 이벤트 처리 메커니즘을 사용
- 마이크로 태스크라는 개념보다는, 단순한 콜백 큐가 존재하는 형태로 구현
HTML5표준화 과정(2008년 ~ 2014년 전후)
- WHATWG HTML Living Standard에서 이벤트 루프 개념을 명확히 정의하면서, setTimeout, DOM 이벤트 등 다양한 작업이 표준화된 이벤트 루프를 통해 처리되었다.
ES6(2015년) 이후
- ES6에서 Promise가 표준화되면서 마이크로태스크 큐가 더욱 중요해졌다.
- V8(Chrome), SpiderMonkey(Firefox), JavascriptCore(Safari) 모두 마이크로태스크를 처리하기 위한 내부 큐를 두고, 이벤트 루프 진행 단계에서 우선적으로 처리하도록 구현되었다.
- 이후 async/await (ES2017) 등장으로 비동기 처리를 더욱 간단히 표현할 수 있게 되었으며, 내부적으로는 여전히 마이크로태스크 큐를 사용한다.
Node.js 환경
초기(2009년 출시 직후)
- Node.js는 구글 V8엔진을 사용하지만, 이벤트 루프 자체는 libuv라이브러리를 통해 구현되었다.
- 브라우저의 이벤트 루프와 유사하지만, setImmediate()나 process.nextTick()등 Node.js 고유 함수들이 존재하며, 특히 process.nextTick()은 현재 작업이 모두 완료되기 전에 실행되어 Promise와 유사한(그러나 더 높은 우선순위의) 역할을 한다.
- Node.js는 타이머, I/O 콜백, idle, poll, check, close callbacks 등의 단계를 거쳐 이벤트 루프를 개선해왔습니다.
Node.js v0.x ~ v10 전후
- 이벤트 루프 동작 단계(timer, I/O callbacks, idle, poll, check, close callbacks)가 지속적으로 개선되었다.
최근 버전(v14 이후)
- ES 모듈, Top-level await 등 새로운 ECMAScript 기능들이 도입되면서, 이들 기능이 어떻게 마이크로태스크 큐에서 처리되는지 정의되었습니다.
- 내부적으로 최적화된 I/O 스케쥴링, V8 업데이트 등에 맞춰 세부 사항이 개선되고 있습니다.
이벤트 루프와 함께 알아두면 좋은 개념
- 콜 스택(Call Stack)
- 자바스크립트 실행 컨텍스트가 어떻게 관리되는지 이해하면, 이벤트 루프가 언제 동작을 개시하는지 알 수 있다.
- 큐(Queue)와 스택(Stack)의 자료구조 원리
- First In First Out, Last In First Out의 차이를 이해하면, 비동기 작업이 처리되는 순서를 예측하기 쉽다.
- 비동기 프로그래밍 패턴
- 콜백(callback), Promise, async/await, Observable(RxJS)등 다양한 비동기 패턴을 익히면 이벤트 루프가 어떻게 각각의 콜백을 스케쥴링 하는지 이해할 수 있습니다.
렌더링 과정과 이벤트 루프의 관계
- 태스크 처리 후 렌더링
- 브라우저는 일반적으로 매크로태스크가 종료된 시점이나, 마이크로태스크 처리가 완료된 시점 등에 "렌더링 기회"를 가집니다.
- WHATWG 이벤트 루프 스펙에 따르면, 이벤트 루프 내에 "Update the rendering" 단계가 존재합니다. 이 단계에서 DOM 변화를 실제 화면에 반영합니다.
- requestAnimationFrame
- 화면에 애니메이션을 그려주고 싶다면,
requestAnimationFrame()
을 사용하면 브라우저 렌더링 주기에 맞춰 콜백이 실행되어 부드러운 프레임을 보장할 수 있습니다. - 일반적인 setTimeout보다 렌더링과 훨씬 밀접하게 동작합니다.
- 화면에 애니메이션을 그려주고 싶다면,
- 성능 측면
- 자바스크립트가 콜스택에서 오래 실행되고 있으면, 렌더링 단계에 지연이 발생하여 화면이 멈춘 것처럼 보일 수 있습니다. (메인 스레드 블로킹 현상)
- 따라서, 긴 연산을 Web Worker등에 위임하거나, 연산을 잘게 나누어
requestAnimationFrame
을 활용하는 전략이 중요합니다.
async/await과 타이머의 조합 예제
async function fetchData() {
console.log('1. 데이터 요청 시작');
setTimeout(() => {
console.log('4. 타이머 실행');
}, 0);
const promise = new Promise(resolve => {
console.log('2. Promise 생성');
resolve('데이터');
});
// process.nextTick()과 유사하게, Promise의 후속 처리가 마이크로태스크 큐에 등록됩니다.
await promise.then(data => {
console.log('3. 데이터 수신:', data);
});
console.log('5. 작업 완료');
}
fetchData();
// 예상 출력 순서:
// 1. 데이터 요청 시작
// 2. Promise 생성
// 3. 데이터 수신: 데이터
// 5. 작업 완료
// 4. 타이머 실행
출처
이벤트 루프 - JavaScript | MDN
JavaScript의 런타임 모델은 코드의 실행, 이벤트의 수집과 처리, 큐에 대기 중인 하위 작업을 처리하는 이벤트 루프에 기반하고 있으며, C 또는 Java 등 다른 언어가 가진 모델과는 상당히 다릅니다.
developer.mozilla.org
HTML Standard
html.spec.whatwg.org
Node.js — The Node.js Event Loop
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.
nodejs.org
The JavaScript Event Loop Explained with Examples
The event loop is a core concept in JavaScript that enables non-blocking, asynchronous behavior. Understanding how the event loop works is…
medium.com
반응형
'Server > Node' 카테고리의 다른 글
Javascript Promise에 대하여 (0) | 2025.01.05 |
---|---|
tRPC에서의 쿠키 설정 및 안전한 클라이언트-서버 간 쿠키 관리 방법 (2) | 2024.11.10 |
RPC의 이해와 tRPC 예제 소개 (10) | 2024.10.13 |
[Nest.js] TypeOrm, Postgresql 적용 (0) | 2023.08.15 |