이벤트 루프와 렌더링 파이프 라인

호우·2025년 11월 25일

이벤트 루프

이벤트 루프 큰 그림

JS Engine

call stack (= 실행 컨텍스트)

  • JS 코드가 실행되는 곳이다.
  • 함수가 호출될 때마다 스택에 push, 함수가 끝나면 pop된다.
  • 한 시점에 콜 스택 가장 위에 있는 함수 하나만 실행한다.
  • 콜 스택이 비어야만 이벤트 루프가 큐에 있는 콜백을 실행할 수 있다.

heap

  • 객체, 배열 함수 같은 참조 타입 값들이 저장되는 메모리 영역이다.
  • 구조화된 데이터들이 저장되고, 콜 스택에서는 이 데이터들의 주소만 가지고 있다.
  • JS가 GC로 더 이상 쓰이지 않는 객체들을 청소하는 공간이다.

Web API

  • 브라우저가 제공하는 기능 묶음
  • 타이머를 재거나, 네트워크 요청을 보내거나, 클릭을 감지
  • Web API는 비동기 작업을 실제 수행하는 곳
  • 작업이 끝났을 때 실행되어야 할 콜백은 큐에 넣는다.

microtask queue

  • 어떤 것들이 들어가는가
    - Promise
    - await 뒷 부분
    - queueMicrotask(...)
    - MutationObserver 콜백
  • 콜 스택에 비어졌을때 실행
  • 마이크로태스크 큐에 있는 것들을 전부 실행한다.
  • 한 번 스택이 비면, 마이크로태스크 큐를 완전히 비울 때까지 계속 실행한다.

macrotask queue

  • 어떤 것들이 들어가는가
    - setTimeout, setInterval 콜백
    - DOM 이벤트 콜백
    - 네트워크 요청 완료 콜백 (ex: fetch then 내부의 첫 task)
  • Web API가 일을 다 끝낸 후에 이제 이거 실행해 하고 넣는 함수들이 모인 곳
  • 매크로 태스크 큐 콜백 딱 하나를 꺼내서 콜 스택에서 실행.

렌더링 파이프 라인

  • HTML text, CSS text -> 파싱 -> Dom tree, CSSOM tree
  • Render tree
    - DOM tree와 CSSOM tree를 합쳐 화면에 실제 그릴 노드들만 모은 트리를 만든다.
    - display: none과 같은 실제 보이지 않는 노드들 제외
  • Layout (Reflow)
    - render tree의 각 노드가 화면 어디에, 어떤 크기로 배치될지 계산
  • Painting (Rasterization)
    - 레이아웃 계산을 바탕으로 실제 픽셀을 칠하는 단계
    - 색, 테두리, 그림자, 이미지, 텍스트 등이 그려짐
  • 왜 JS 실행 후, 매크로태스크 전에 페인팅을 끼워 넣나?
    - 최종 상태 한 번만 그리기 위해
    - JS 코드가 DOM을 막 변경하고 업데이트 함
    - JS task 실행 -> microtask로 자잘한 후속작업 마저 처리 -> DOM/CSS 안정 -> paint -> 다음 task

Event Loop

  • 이벤트 루프가 이 흐름 전체를 관리
  1. 콜 스택이 비었는지 확인
  2. 비어 있다면
    1. 마이크로 태스큐에 있는 것들 전부 실행
    2. 렌더링 단계(페인트) 진행 -> 필요한 경우
    3. 매크로 태스큐에서 task 하나를 꺼내서 콜 스택에 올리고 실행
  3. 다시 1번으로 돌아감

전체 흐름

console.log("1");

button.addEventListener("click", () => {
  console.log("click");
});

setTimeout(() => {
  console.log("timeout");

  Promise.resolve().then(() => {
    console.log("promise in timeout");
  });
}, 0);

console.log("2");

1단계: 초기 스크립트 실행 (콜 스택)

  • console.log("1") → 출력: 1
  • addEventListener → Web API(DOM)에 콜백 등록만 하고 끝
  • setTimeout(...) → Web API 타이머에 작업을 맡기고 스택에서 빠짐
  • console.log("2") → 출력: 2
  • 초기 스크립트 끝 → 콜 스택 비어짐

2단계: microtask queue

  • 아직 Promise 관련 코드가 없음 → microtask queue는 비어 있음
    → 아무 일도 안 함

3단계: 렌더링

  • 필요하면 렌더링 한 번 할 수 있음 (화면 업데이트)

4단계: macrotask queue에서 task 하나 실행

  • 0ms 지난 후 Web API의 타이머가 끝나면
    setTimeout 콜백이 macrotask queue에 들어간다.
  • 이벤트 루프가 이 콜백 하나를 꺼내서 콜 스택에 올리고 실행:
    • console.log("timeout") → 출력: timeout
    • Promise.resolve().then(...) 실행 → then 콜백이 microtask queue에 들어감
  • setTimeout 콜백 함수 끝 → 콜 스택 비어짐

5단계: 다시 microtask queue 비우기

  • 방금 등록된 promise in timeout 콜백 실행 → 출력: promise in timeout
  • microtask queue 비어짐

6단계: 렌더링

  • 필요 시 레이아웃/페인트

7단계: 이후

  • 사용자가 버튼을 클릭하면, Web API(DOM)가 그 순간
    click 핸들러를 macrotask queue에 넣는다.
  • 이벤트 루프는 다시 위 순서대로 돌다가, macrotask queue에서 click 콜백을 꺼내 실행한다.
    console.log("click") 출력

0개의 댓글