Don't Panic
in JS ☕ 5분 읽기

[JS] 브라우저 렌더링과 DOM을 꿰뚫는 인터뷰 완전 정복

#JS
목차

DOM

BOM & DOM & Node

BOM(Browser Object Model)과 DOM(Document Object Model)은 브라우저 환경을 다루는 두 축이다.

구분대상대표 객체
BOM브라우저 창·환경window, screen, location, navigator
DOMHTML 문서 구조document, element, text, comment 노드

Window 객체

  • Global Context(전역공간)이자, 브라우저 창을 나타내는 객체
  • 전역변수나 전역 함수의 경우 window 프로퍼티처럼 작동하게 됨
  • 주요 프로퍼티: innerWidth, innerHeight, screenX, screenY, scrollBy(), scrollTo()

screen 객체

  • 사용자 환경의 디스플레이(모니터) 정보를 가지는 객체
  • 주요 프로퍼티: availHeight, availWidth, width, height, orientation

location 객체

  • 사용자가 보고 있는 페이지의 URL을 다루는 객체
  • 주요 프로퍼티: href, reload, replace
  • 웹브라우저 및 브라우저 환경 정보를 가지는 객체
  • 주요 프로퍼티: userAgent

DOM(Document Object Model)

자바스크립트의 계층화된 트리. 노드 종류는 document·element·text·comment 등이 있다.

document 노드

  • 웹 페이지마다 존재하는 객체. 웹 페이지 안의 모든 컨텐츠를 다루는 시작점
  • 주요 프로퍼티: title, url, doctype, documentElement, head, body, getElementById, createElement, querySelector, readyState
readyState 단계별 변화
loading      → 문서를 로딩(파싱)하는 중
interactive  → 파싱 완료, DOM 사용 가능 (이미지 등 하위 리소스는 로딩 중)
complete     → 문서와 모든 하위 리소스 로딩 완료

element 노드

  • 웹 페이지 안의 각 HTML 태그 요소를 의미
  • 주요 프로퍼티: querySelector, classList, dataset, id, innerHTML, parentNode, nextSibling, previousSibling

브라우저의 렌더링

웹 브라우저의 기본 구조

┌─────────────────────────────────────────────┐
│ 사용자 인터페이스 (주소창, 뒤로/앞으로 버튼 등)  │
├─────────────────────────────────────────────┤
│ 브라우저 엔진 (UI ↔ 렌더링 엔진 제어)           │
├──────────────────────┬──────────────────────┤
│ 렌더링 엔진          │ 자바스크립트 엔진       │
│ (HTML/CSS 파싱·표시) │ (JS 해석·실행)          │
├──────────────────────┴──────────────────────┤
│ 네트워킹 / UI 백엔드 / 데이터 저장소           │
└─────────────────────────────────────────────┘

단계 1. 파싱 — HTML → DOM

HTML 문자열                 DOM 트리
<html>                      document
  <head>...</head>    →     └── html
  <body>                          ├── head
    <div>...</div>               └── body
  </body>                             └── div
</html>
  • 오타·잘못된 문법은 예외처리로 보완
  • link, img 태그를 만나면 리소스를 다운로드
  • script 태그를 만나면 DOM 파싱을 중지하고 자바스크립트를 먼저 해석

단계 2. 스타일 계산 — CSS → CSSOM

CSS 규칙                       CSSOM 트리
body { font-size: 16px; }      body
  p { color: red; }       →    └── p (color: red, font-size: 16px 상속)
  • CSSOM 정보를 통해 DOM 노드에 대한 스타일을 결정
  • 결정된 스타일은 크롬 개발자 도구 Computed 항목에서 확인 가능

단계 3. 레이아웃 — 렌더 트리 생성

DOM 트리  +  CSSOM 트리  →  레이아웃(렌더) 트리
                              └── 화면에 그려질 노드 + 크기/좌표 정보
                                  (display:none 노드는 제외)
  • DOM과 계산된 스타일을 따라가며 요소의 크기·좌표 정보를 담은 레이아웃 트리 생성
  • display:none 요소는 제외되며, 가상 요소(::before 등)는 포함됨

단계 4. 페인트 & 컴포지팅

레이아웃 트리 → 페인트 기록(그리기 순서) → 텍스트·색·이미지·보더·그림자 등을 그림
  • 페인트 기록에는 요소를 렌더링하는 순서가 저장됨
  • 한 페이지를 여러 레이어로 나눈 뒤 각 레이어에 시각적 요소를 그림
레이어 1 ┐
레이어 2 ├─ 래스터화(픽셀화) ─→ 합성(컴포지팅) ─→ 최종 화면
레이어 3 ┘

각 레이어를 픽셀로 변환(래스터화)한 뒤 합성하는 과정을 컴포지팅이라 한다.

렌더링 파이프라인 요약

단계입력출력
파싱HTMLDOM 트리
스타일 계산DOM + CSSCSSOM·확정 스타일
레이아웃DOM + CSSOM렌더 트리(크기·좌표)
페인트렌더 트리레이어별 페인트 기록
컴포지팅각 레이어최종 화면 픽셀

리페인트 & 리플로우

Reflow란

생성된 DOM 노드의 레이아웃이 변경될 때 영향받는 모든 노드(부모·자식)의 수치를 다시 계산하여 레이아웃 트리(렌더 트리)를 재생성하는 작업.

Reflow를 유발하는 속성

분류예시 속성
레이아웃 속성width, height, padding, margin, float, position
시각적 속성color, border-radius, background, box-shadow

Repaint란

Reflow 과정이 끝나고 재생성된 레이아웃 트리를 다시 레이어에 그리는 작업.

리플로우와 리페인트는 렌더링 파이프라인의 레이아웃·페인트 단계를 다시(Re) 거치는 과정이다.

왜 성능에 치명적인가

렌더링 과정은 순차적이므로, 앞 단계에 변화가 생기면 그 뒤 단계는 모두 다시 수행된다.

파싱 → 스타일 계산 → 레이아웃 → 페인트 → 컴포지팅
                     └ 앞 단계가 바뀌면 그 뒤 단계는 모두 다시 수행 ┘

60fps 기준으로 1프레임당 약 16.6ms 안에 리플로우 + 리페인트를 완료해야 부드러운 애니메이션을 얻을 수 있다.

60fps → 1프레임당 약 16.6ms 안에 리플로우 + 리페인트를 끝내야 함
[프레임1][프레임2][프레임3] ... 매 프레임 제때 그리면 부드럽게 보임

예산을 초과하면 프레임이 누락되어 버벅임이 발생한다.

프레임 예산 초과 시:
[프레임1][----지연----][프레임3] → 프레임 누락(드롭) → 애니메이션 버벅임

해결 방법

1. CSS transform 속성 사용

  • transform으로 만드는 애니메이션은 CPU 대신 GPU를 사용하여 렌더링을 처리하므로 레이아웃 단계를 건너뜀

2. requestAnimationFrame 함수 사용

  • 자바스크립트를 통한 애니메이션 정보를 매 프레임 시작 시점에 브라우저에 미리 알려줌
각 프레임 시작 시점에 콜백 실행:
[프레임 시작 → rAF 콜백 실행 → 리플로우/리페인트] [다음 프레임 시작 → rAF ...]

정리

  • Reflow: DOM 레이아웃 변경 시 렌더 트리를 재생성
  • Repaint: Reflow 후 레이어를 다시 그리는 작업
  • Reflow·Repaint가 많을수록 렌더링 성능 저하
  • 개선책: CSS transform + requestAnimationFrame

이벤트 흐름

브라우저가 사용자 입력을 받으면

  1. 이벤트 발생 — 마우스 클릭, 휠, 포인터 이동, 화면 터치 등 모든 사용자 제스처
  2. 이벤트 대상 탐색 — 페인트 기록을 통해 이벤트 발생 좌표에 위치한 요소 확인
  3. 캡처링 단계window에서 타겟 요소까지 하향 전파하며 캡처링 리스너 실행
캡처링 단계 (위 → 아래로 전파)
window → document → html → body → ... → 타겟 요소
  1. 버블링 단계 — 타겟에서 다시 window까지 상향 전파하며 버블링 리스너 실행
버블링 단계 (아래 → 위로 전파)
타겟 요소 → ... → body → html → document → window

캡처링·버블링 전체 흐름

            ┌── 캡처링 단계 ──┐        ┌── 버블링 단계 ──┐
  window ───┤ (위에서 아래로) ├─ 타겟 ─┤ (아래에서 위로) ├─── window
            └─────────────────┘        └─────────────────┘

이벤트 위임

개념

요소에 이벤트를 등록하는 일반적인 방법은 addEventListener()다. 100개 요소에 각각 리스너를 붙이는 대신, 이벤트 버블링을 활용하면 단 1개의 리스너로 처리할 수 있다.

<div>  ← 이벤트 리스너 등록
  └── <button>  ← 실제 클릭(이벤트 발생 지점)
클릭 → button에서 발생 → 버블링으로 부모 div의 리스너 실행

event.target vs event.currentTarget

event.currentTarget → 리스너가 등록된 요소 (= 리스너 안의 this)
event.target        → 이벤트가 최초로 발생한 요소

이벤트 위임 예제


<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
    <div class="parent">
        <!-- "generate item" 버튼을 클릭하면 새로운 아이템을 생성하는 버튼 -->
        <button type="button">generate item</button>
        <ul>
            <!-- 초기 아이템으로 화면에 표시될 리스트 아이템 -->
            <li>initial item</li>
        </ul>
    </div>

    <script>

        // .parent 클래스를 가진 요소를 찾아 변수 parent에 할당
        var parent = document.querySelector(".parent");

        // .parent 요소에 클릭 이벤트 리스너 추가
        parent.addEventListener('click', function (event) {
            // 클릭된 요소의 태그 이름이 "button"인 경우
            if (event.target.tagName.toLowerCase() === "button") {
                // 새로운 리스트 아이템 생성하고 텍스트를 "hello world~"로 설정
                const item = document.createElement("li");
                item.innerText = "hello world~";
                // 부모 요소(.parent)의 하위 ul 요소에 새로운 아이템 추가
                parent.querySelector("ul").appendChild(item);
            }

            // 클릭된 요소의 태그 이름이 "li"인 경우
            if (event.target.tagName.toLowerCase() === "li") {
                // 콘솔에 "hit!!" 메시지 출력
                console.log('hit!!');
            };
        });

    </script>
</body>
</html>

이벤트를 받을 요소를 리스너가 붙은 부모의 자식으로 배치하면 개수에 상관없이 이벤트를 처리할 수 있다. 동적으로 추가되는 요소도 동일하게 동작한다.

정리

  • 이벤트 위임이란, 이벤트 버블링을 활용해 단일 리스너로 여러 자식 요소의 이벤트를 처리하는 패턴
  • event.target으로 실제 발생 지점을 식별하여 분기 처리

DocumentFragment — DOM의 경량 조각

기본 패턴과 문제점

처음 자바스크립트로 DOM을 생성할 때 document.createElement()로 만들고 appendChild()로 바로 DOM에 등록하면, 반복할 때마다 Reflow가 발생한다.

<script>
for (let i = 0; i < 10; i++) {
    let divEl = document.createElement("div");
    divEl.innerText = "hello~ this is " + i;
    document.body.appendChild(divEl);
}
</script>

DocumentFragment를 사용하면 메모리 상에만 존재하는 경량 DOM을 생성하여 한 번에 등록할 수 있다.

const docFrag = document.createDocumentFragment();

for (let i = 0; i < 10; i++) {
    var divEl = document.createElement("div");
    divEl.innerText = "hello~ this is " + i;
    docFrag.appendChild(divEl);
}

document.body.appendChild(docFrag);

div 래퍼 요소와의 차이점: div는 DOM에 추가될 때 자신도 함께 삽입되지만, DocumentFragment자식 노드만 삽입되고 자신은 사라진다.

//const docFrag = document.createDocumentFragment();
const test = document.createElement('div');

for (let i = 0; i < 10; i++) {
    var divEl = document.createElement("div");
    divEl.innerText = "hello~ this is " + i;
    //docFrag.appendChild(divEl);
    test.appendChild(divEl);
}

//document.body.appendChild(docFrag);
document.body.appendChild(test);

DocumentFragment의 특징

// 비어 있는 DocumentFragment를 생성하고 docFrag 변수에 할당합니다.
const docFrag = document.createDocumentFragment();

// 0부터 9까지 총 10번 반복하는 루프를 시작합니다.
for (let i = 0; i < 10; i++) {
    // 새로운 div 요소를 생성하고 divEl 변수에 할당합니다.
    var divEl = document.createElement("div");

    // div 요소의 텍스트 내용을 설정합니다.
    divEl.innerText = "hello~ this is " + i;

    // 생성한 div 요소를 DocumentFragment에 추가합니다.
    docFrag.appendChild(divEl);

    // DocumentFragment에 현재까지 추가된 자식 노드들을 출력합니다.
    console.log(docFrag.childNodes);
}

// DocumentFragment에 추가된 모든 div 요소들을 문서의 body에 추가합니다.
document.body.appendChild(docFrag);

// DocumentFragment가 비어 있음을 확인하기 위해 다시 자식 노드들을 출력합니다.
console.log(docFrag.childNodes);
  1. DocumentFragment를 DOM 노드에 추가해도 Fragment 노드 자체는 등록되지 않고 자식 노드들만 추가된다.
  2. DOM에 추가된 후 Fragment의 자식 노드들은 더 이상 메모리에 존재하지 않는다.

이 특징 덕분에 여러 부모 요소에 동일 구조를 삽입할 때 cloneNode(true)를 활용하면 이중 반복문 없이 깔끔하게 구성할 수 있다.

<script>
    // 비어 있는 DocumentFragment를 생성하고 frag 변수에 할당합니다.
    const frag = document.createDocumentFragment();

    // 빈 배열 elements를 선언합니다.
    let elements = [];

    // 100번 반복하는 루프를 시작합니다.
    for (let i = 0; i < 100; i++) {
        // div 요소를 생성하고 el 변수에 할당합니다.
        const el = document.createElement("div");

        // 생성된 div 요소를 배열 elements에 추가합니다.
        elements.push(el);
    }

    // 클래스 이름이 "container"인 모든 요소를 선택하여 cont 변수에 할당합니다.
    const cont = document.querySelectorAll(".container");

    // "container" 클래스를 가진 각 요소에 배열 elements의 복제된 요소들을 추가합니다.
    for (let i = 0; i < cont.length; i++) {
        // 배열 elements의 모든 요소를 복제하여 현재 "container" 요소에 추가합니다.
        for(let j = 0; j < elements.length; j++){
            cont[i].appendChild(elements[j].cloneNode(true));
        }
    }
</script>

cloneNode(true)를 활용하면 아래처럼 이중 반복문 없이 동일하게 구성할 수 있다.

<body>
    <!-- 3개의 동일한 클래스 "container"를 가진 div 요소가 있습니다. -->
    <div class="container">
    </div>
    <div class="container">
    </div>
    <div class="container">
    </div>
    <script>
        // 비어 있는 DocumentFragment를 생성하고 frag 변수에 할당합니다.
        const frag = document.createDocumentFragment();

        // 100번 반복하는 루프를 시작합니다.
        for (let i = 0; i < 100; i++) {
            // div 요소를 생성하고 el 변수에 할당합니다.
            const el = document.createElement("div");

            // 이미지 요소를 생성하고 img 변수에 할당합니다.
            const img = document.createElement("img");

            // 이미지의 소스(src)를 'koreanFlag.png'로 설정합니다.
            img.src = 'koreanFlag.png';

            // div 요소에 이미지 요소를 자식으로 추가합니다.
            el.appendChild(img);

            // DocumentFragment에 현재 생성된 div 요소를 추가합니다.
            frag.appendChild(el);
        }

        // 클래스 이름이 "container"인 모든 요소를 선택하여 cont 변수에 할당합니다.
        const cont = document.querySelectorAll(".container");

        // "container" 클래스를 가진 각 요소에 복제된 DocumentFragment를 추가합니다.
        for (let i = 0; i < cont.length; i++) {
            // 복제된 DocumentFragment를 현재 "container" 요소에 추가합니다.
            cont[i].appendChild(frag.cloneNode(true));
        }

    </script>
</body>

정리

  • DocumentFragment는 오직 메모리상에만 존재하는 경량화 DOM
  • DOM에 추가 시 Fragment 자체는 등록되지 않고 자식 노드만 삽입됨
  • 삽입 후 자식 노드는 Fragment에서 사라짐 — 재사용이 필요하면 cloneNode(true)로 복제