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()은 매우 높은 우선순위를 가지며, 사용 시 주의가 필요합니다.

이벤트 루프의 동작 과정

  1. 콜 스택(Call Stack) 확인
    • 자바스크립트는 단일 스레드로 동작하며, 현재 실행 중인 함수(또는 실행 컨텍스트)가 콜 스택에 쌓여 있습니다.
    • 이벤트 루프는 콜 스택이 비어 있는지를 지속적으로 관찰합니다.
  2. 콜 스택이 비어 있지 않으면
    • 콜 스택이 비어 있지 않다면, 스택의 맨 위에 있는 함수를 실행합니다. (동기 작업은 실행이 완료될 때까지 블로킹됩니다.)
      • 여기에는 Promise.then(), async/await, MutationObserver 등이 포함된다.
      • Node.js의 process.nextTick()은 기술적으로 별도의 nextTick 큐에서 관리되지만, 동일 tick 내에서 항상 마이크로태스크보다 먼저 실행되며, 마이크로태스크 큐(및 Node.js의 경우 nextTick 큐)에 대기 중인 모든 작업이 실행될 때까지 계속 처리한다..
  3. 콜 스택이 비어 있는 순간, 먼저 마이크로태스크 큐(Microtask Queue) 를 확인
    • Promise.then(), async/await, MutationObserver, Node.js의 process.nextTick() 등이 여기에 해당합니다.
    • 마이크로태스크 큐에 대기 중인 모든 작업(콜백)을 전부 실행 완료할 때까지 비우고, 그 과정에서 새로 추가된 마이크로태스크가 있어도 계속 처리합니다.
    • 즉, "마이크로태스크 큐가 빌 때까지" 반복 실행합니다.
  4. 마이크로태스크가 모두 처리된 후, 태스크 큐(매크로 태스크 큐, Callback Queue)를 확인
    • setTimeout, setInterval, I/O 콜백, DOM 이벤트, requestAnimationFrame(콜백 등록 시점에 따라 다름) 등이 매크로태스크 큐에 들어갑니다.
    • 이 큐에서 맨 앞에 있는 작업을 하나 가져와 콜 스택에 푸시하고 실행합니다.
  5. 실행(Execution) 및 콜백 처리
    • 위에서 스택에 푸시된 함수가 실행되면서, 추가적인 비동기 작업이 발생할 수 있습니다(예: 다음 타이머 등록, 네트워크 요청 등).
    • 함수 실행이 종료되어 콜 스택이 다시 비면, 이벤트 루프는 다시 마이크로태스크 -> 매크로태스크 순으로 확인하며 필요한 작업을 처리합니다.
  6. 이 과정을 계속 반복(Repeat)
    • 결과적으로 자바스크립트 엔진은 콜 스택이 빌 때마다 마이크로태스크 큐, 그 다음에 매크로태스크 큐 순으로 작업들을 하나씩(마이크로태스크는 모두) 실행하면서 무한히 순환(Event Loop)합니다.

예제

console.log('시작');

setTimeout(() => {
    console.log('매크로 태스크(태스크 큐에 들어갑니다.)');
}, 0);

Promise.resolve().then(() => {
    console.log('마이크로 태스크(마이크로 태스크 큐에 들어갑니다.)');
});

console.log('끝');

// 출력 결과
// 시작
// 끝
// 마이크로 태스크(마이크로 태스크 큐에 들어갑니다.)
// 매크로 태스크(태스크 큐에 들어갑니다.)
  1. 동기 코드 실행
    • console.log("시작") → "시작" 출력
    • setTimeout(...) 등록 (비동기) → 타이머 콜백은 매크로태스크 큐에 등록
    • Promise.resolve().then(...) 등록 (비동기) → then() 콜백은 마이크로태스크 큐에 배치될 예정
    • console.log("끝") → "끝" 출력
    • 이제 콜 스택이 비어짐
  2. 마이크로태스크 큐 확인
    • Promise의 then() 콜백이 즉시 실행되어 "마이크로 태스크(마이크로태스크 큐 실행)" 출력
  3. 매크로태스크 큐 확인
    • setTimeout 콜백이 실행되어 "매크로 태스크(태스크 큐 실행)" 출력
  4. 이후 반복
    • 다시 콜 스택 비면 마이크로태스크 → 매크로태스크 순으로 확인. 현재는 추가 작업이 없으므로 종료.

역사와 엔진별 특징 변화

브라우저 환경

초기(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

 

반응형
코드플리