브라우저 내부 구조: 시니어 엔지니어의 심층 탐구

발행: (2026년 1월 12일 오전 03:52 GMT+9)
9 min read
원문: Dev.to

Source: Dev.to

멀티‑프로세스 아키텍처

┌─────────────────────────────────────────────────────────────┐
│                     Browser Process                        │
│  (UI, bookmarks, network, storage)                          │
└─────────────────────────────────────────────────────────────┘
         │              │              │              │
         ▼              ▼              ▼              ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│  Renderer   │ │  Renderer   │ │  Renderer   │ │    GPU      │
│  Process    │ │  Process    │ │  Process    │ │  Process    │
│  (Tab 1)    │ │  (Tab 2)    │ │  (Tab 3)    │ │             │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
이점설명
보안각 탭이 샌드박스화되어 있어 악성 사이트가 다른 탭에 접근할 수 없습니다.
안정성하나의 탭이 충돌해도 다른 탭은 계속 작동합니다.
성능CPU 코어를 활용한 병렬 처리.

핵심 요점: 이것은 프런트엔드 성능에 있어 가장 중요한 개념입니다.

Rendering Pipeline

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│   HTML   │───▶│   DOM    │───▶│  Render  │───▶│  Layout  │───▶│  Paint   │
│  Parse   │    │   Tree   │    │   Tree   │    │          │    │          │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘
                     │                │
                     │                │
               ┌─────▼─────┐          │
               │   CSSOM   │──────────┘
               │   Tree    │
               └───────────┘

Example HTML

<!DOCTYPE html>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <div id="app">
      <p>Hello</p>
    </div>
  </body>
</html>

DOM Tree (simplified)

document

 html

 body

 div#app

  p

 "Hello"

Key Point: 파서는 동기적으로 동작합니다. “ 태그를 만나면 스크립트가 실행될 때까지 파싱을 중단합니다.

Sample CSS

body { font-size: 16px; }
#app { color: blue; }
p { margin: 10px; }

CSSOM (simplified)

CSSOM

 ┌────┴────┐
body      #app
(font:16) (color:blue)

  p
(margin:10)

Key Point: CSSOM을 구성하는 과정은 렌더링을 차단합니다. 그래서 critical CSS를 인라인으로 삽입합니다.

Render Tree (only visible elements)

Render Tree:
  body (font: 16px)
    └─ div#app (color: blue)
         └─ p (margin: 10px)
              └─ "Hello"

렌더 트리에 포함되지 않음:

  • “ 및 그 자식 요소
  • display: none인 요소
  • , , “

Layout (calculates exact positions & sizes)

┌────────────────────────────────────────┐
│ body: 0,0 – 1920x1080                 │
│  ┌──────────────────────────────────┐ │
│  │ div#app: 8,8 – 1904x500          │ │
│  │  ┌────────────────────────────┐ │ │
│  │  │ p: 8,18 – 1904x20          │ │ │
│  │  └────────────────────────────┘ │ │
│  └──────────────────────────────────┘ │
└────────────────────────────────────────┘

Expensive operation: 너비, 높이, 위치를 변경하면 모든 하위 요소에 대한 reflow가 발생합니다.

Paint (fills in pixels)

Paint order

  1. 배경 색상
  2. 배경 이미지
  3. 테두리
  4. 자식 요소
  5. 외곽선

GPU는 레이어들을 합성해 최종 이미지를 만든다. 별도 레이어에 있는 요소는 재페인팅 없이 애니메이션이 가능하다.

JavaScript 실행 모델

┌─────────────────────────────────────────────────────────────┐
│                         HEAP                                 │
│                   (Object Storage)                           │
└─────────────────────────────────────────────────────────────┘

┌─────────────┐     ┌─────────────────────────────────────────┐
│   CALL      │     │              WEB APIs                  │
│   STACK     │     │ (setTimeout, fetch, DOM events, etc.) │
│             │     └──────────────────┬──────────────────┘
│ function()  │                        │
│ function()  │                        ▼
│ main()      │     ┌─────────────────────────────────────────┐
└─────────────┘     │           CALLBACK QUEUES                │
       ▲            │  ┌─────────────────────────────────────┐ │
       │            │  │ Microtask Queue (Promises, queueMicrotask) │ │
       │            │  └─────────────────────────────────────┘ │
       │            │  ┌─────────────────────────────────────┐ │
       └────────────│  │ Macrotask Queue (setTimeout, I/O)   │ │
     Event Loop    │  └─────────────────────────────────────┘ │
     picks next    └─────────────────────────────────────────────┘

예시

console.log('1');                     // Sync
setTimeout(() => console.log('2'), 0); // Macrotask
Promise.resolve().then(() => console.log('3')); // Microtask
console.log('4');                     // Sync

// Output: 1, 4, 3, 2

규칙

  1. 모든 동기 코드를 실행한다 (콜 스택이 비워질 때까지).
  2. 모든 마이크로태스크를 실행한다 (Promise 콜백, queueMicrotask).
  3. 하나의 매크로태스크를 실행한다 (setTimeout, setInterval, I/O 등).
  4. 2단계부터 다시 반복한다.

작업 유형 개요

마이크로태스크매크로태스크
Promise.then / catch / finallysetTimeout
queueMicrotask()setInterval
MutationObserversetImmediate (Node)
process.nextTick (Node)
I/O 콜백
requestAnimationFrame*

* requestAnimationFrame재페인팅 전에, 마이크로태스크 이후에 실행됩니다.

이벤트 루프에 양보하기

// BAD: Blocks for 5 seconds
function processLargeArray(items) {
  items.forEach(item => {
    // Heavy computation
    heavyWork(item);
  });
}

// GOOD: Yield to the event loop
async function processLargeArray(items) {
  for (let i = 0; i < items.length; i++) {
    heavyWork(items[i]);
    await new Promise(r => setTimeout(r, 0));
  }
}

각 유형의 작업을 트리거하는 요인을 이해하는 것이 성능에 중요합니다.

레이아웃 트리거 vs. 페인트 트리거 CSS 변경

페인트 전용 변경 (레이아웃 없음):

element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.visibility = 'hidden'; // Still takes space
element.style.opacity = 0.5;

레이아웃 트리거 변경 (리플로우):

element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';
element.style.position = 'absolute';

CSS 및 레이아웃

le.padding = '10px';
element.style.margin = '20px';
element.style.display = 'none';          // Removed from layout
element.style.position = 'absolute';
element.style.fontSize = '20px';          // Text reflow!

최악의 성능 안티‑패턴

BAD – 100개의 레이아웃 재연산을 강제

// BAD: Forces 100 reflows!
elements.forEach(el => {
  const height = el.offsetHeight;               // READ → forces layout
  el.style.height = height + 10 + 'px';         // WRITE → invalidates layout
});

GOOD – 읽기를 한 번에 수행하고, 그 다음에 쓰기를 한 번에 수행

// GOOD: Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // All reads

elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px';          // All writes
});

레이아웃‑트리거 Getter (즉시 재배치 강제)

  • element.offsetTop / offsetLeft / offsetWidth / offsetHeight
  • element.scrollTop / scrollLeft / scrollWidth / scrollHeight
  • element.clientTop / clientLeft / clientWidth / clientHeight
  • element.getBoundingClientRect()
  • window.getComputedStyle(element)

GPU‑Friendly 애니메이션

/* These animate on the GPU — 60 fps guaranteed */
transform: translateX(100px);
transform: scale(1.5);
transform: rotate(45deg);
opacity: 0.5;

현대적인 방법

.animated-element {
  will-change: transform;
}

레거시 폴백

.animated-element {
  transform: translateZ(0);  /* “Null transform hack” */
}

will-change 남용 피하기

/* BAD: Creates too many layers */
* {
  will-change: transform;
}

/* GOOD: Only elements that will animate */
.card:hover {
  will-change: transform;
}
.card {
  will-change: auto;  /* Release after animation */
}

타이밍 & 페인트 사이클

// BAD: 타이머가 디스플레이 새로 고침과 동기화되지 않음
setInterval(() => {
  element.style.left = x++ + 'px';
}, 16);  // 60 fps 목표
// GOOD: 브라우저의 페인트 사이클과 동기화됨
function animate() {
  element.style.left = x++ + 'px';
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

한 프레임 (~16.67 ms)

┌────────────────────────────────────────────────────────────┐
│                    One Frame (~16.67ms)                     │
├──────────┬──────────┬──────────┬──────────┬───────────────┤
│   JS     │   rAF    │  Style   │  Layout  │     Paint     │
│ (이벤트) │ 콜백     │  계산    │          │   합성        │
└──────────┴──────────┴──────────┴──────────┴───────────────┘

무거운 연산 오프로드

main.js

const worker = new Worker('worker.js');

worker.postMessage({ data: largeArray });

worker.onmessage = (event) => {
  console.log('Result:', event.data);
};

worker.js

self.onmessage = (event) => {
  const result = heavyComputation(event.data);
  self.postMessage(result);
};

API 접근 매트릭스

API접근 가능접근 불가
fetch
DOM
setTimeout/setInterval
window
WebSockets
document
IndexedDB
UI‑related APIs
postMessage
localStorage (use IndexedDB)

일반적인 메모리 누수 패턴

  1. 잊힌 이벤트 리스너

    element.addEventListener('click', handler);
    // element removed from DOM, but handler still references it
  2. 클로저가 참조를 유지

    function createHandler() {
      const largeData = new Array(1_000_000);
      return () => console.log(largeData.length);
    }
  3. 분리된 DOM 트리

    const div = document.createElement('div');
    div.innerHTML = 'Hello';
    // div never added to DOM, but JavaScript holds reference

팁: Chrome DevTools → MemoryTake Heap Snapshot을 사용하고 의심되는 누수 전후의 스냅샷을 비교하세요.

가비지 컬렉션 개요

  1. Mark Phase – “루트”(전역 객체, 스택)에서 시작하여 도달 가능한 모든 객체를 표시합니다.
  2. Sweep Phase – 표시되지 않은 모든 객체를 삭제합니다.

Summary (in my own words)

저는 브라우저를 다단계 파이프라인으로 이해합니다: HTML/CSS를 트리 구조로 파싱하고, 이를 렌더 트리로 병합한 뒤 레이아웃을 계산하고, 픽셀을 그리며, 레이어를 합성합니다. 레이아웃 스러싱을 피하기 위해 (읽기를 먼저 배치하고 쓰기를 수행) 최적화하고, 애니메이션에 컴포지터 친화적인 속성(transform, opacity)을 사용하며, requestAnimationFrame을 활용해 부드러운 60 fps를 구현합니다. 무거운 연산은 Web Workers에 오프로드하여 메인 스레드가 응답성을 유지하도록 합니다. 이벤트 루프—특히 마이크로‑task와 매크로‑task의 구분—을 이해하면 예측 가능한 비동기 코드를 작성하는 데 도움이 됩니다.

Back to Blog

관련 글

더 보기 »