About→

← Studies

02. 브라우저 렌더링 — 브라우저는 HTML을 어떻게 화면으로 만드는가?

앞선 글에서는 브라우저가 서버와 통신하기까지의 과정을 살펴봤다.

DNS를 통해 서버를 찾고, IP를 통해 목적지를 확인하고, TCP/QUIC을 통해 통신 기반을 만들고, TLS를 통해 안전한 연결을 구성한 뒤 HTTP를 통해 필요한 리소스를 요청한다.

그 결과 서버에서는 HTML, CSS, JavaScript와 같은 리소스를 브라우저에게 전달한다.

그런데 여기서 또 하나의 과정이 필요하다.

서버에서 받은 HTML은 아직 우리가 보는 화면이 아니다.

text
서버
 ↓
HTML
CSS
JavaScript
 ↓
브라우저
 ↓
???
 ↓
화면

브라우저는 전달받은 리소스를 해석하고 계산한 뒤 실제 화면에 그려야 한다.

이 과정을 브라우저 렌더링(Browser Rendering)이라고 한다.

이번 글에서는 브라우저가 HTML과 CSS를 어떻게 화면으로 만들어내는지 살펴보고, 그 과정에서 JavaScript가 어떤 영향을 미치는지도 함께 알아본다.

1. 브라우저 렌더링은 무엇일까?

브라우저 렌더링을 아주 단순하게 표현하면 다음과 같다.

text
HTML
 ↓
DOM

CSS
 ↓
CSSOM

DOM + CSSOM
 ↓
Render Tree
 ↓
Layout
 ↓
Paint
 ↓
Composite
 ↓
화면

처음 보면 각각의 용어가 많아 보이지만 하나씩 보면 역할은 명확하다.

text
DOM
→ HTML을 브라우저가 이해할 수 있는 구조로 만든 것

CSSOM
→ CSS를 브라우저가 이해할 수 있는 구조로 만든 것

Render Tree
→ 실제 화면에 표시할 요소를 구성한 것

Layout
→ 요소가 어디에 얼마나 크게 배치될지 계산

Paint
→ 요소를 어떤 모습으로 그릴지 결정

Composite
→ 여러 레이어를 합쳐 최종 화면을 구성

이 과정을 순서대로 살펴보자.

2. HTML을 DOM으로 만든다

먼저 브라우저가 HTML을 받았다고 해보자.

html
<body>
  <h1>Hello</h1>
  <p>Hello World</p>
</body>

브라우저는 이 HTML을 그대로 화면에 그리는 것이 아니다.

먼저 HTML을 파싱(Parsing)한다.

파싱은 쉽게 말하면 문자열 형태의 HTML을 브라우저가 이해할 수 있는 구조로 분석하는 과정이다.

그 결과 만들어지는 것이 DOM(Document Object Model)이다.

개념적으로 보면 다음과 같다.

text
HTML

<body>
  <h1>Hello</h1>
  <p>Hello World</p>
</body>

        ↓ 파싱

DOM

body
 ├─ h1
 │   └─ "Hello"
 │
 └─ p
     └─ "Hello World"

DOM은 HTML 문서를 객체 형태의 트리 구조로 표현한다.

그래서 JavaScript를 통해 DOM을 직접 조작할 수도 있다.

jsx
document.querySelector('h1').textContent = 'Hello React';

JavaScript가 HTML 문자열 자체를 수정하는 것이 아니라, 브라우저가 만들어 놓은 DOM을 수정하는 것이다.

3. CSS는 CSSOM으로 만든다

HTML만 가지고는 요소의 모양과 위치를 결정할 수 없다.

다음과 같은 CSS가 있다고 해보자.

css
h1 {
  font-size: 32px;
  color: black;
}

p {
  font-size: 16px;
}

브라우저는 CSS 역시 파싱한다.

그 결과 만들어지는 것이 CSSOM(CSS Object Model)이다.

개념적으로 보면 다음과 같다.

text
CSS
 ↓
CSS Parser
 ↓
CSSOM

DOM이 HTML의 구조를 표현한다면,

CSSOM은 CSS의 스타일 정보를 브라우저가 이해할 수 있는 구조로 표현한다.

4. DOM과 CSSOM을 결합한다

이제 브라우저에게 두 가지 정보가 생겼다.

text
DOM
→ 어떤 요소가 존재하는가?

CSSOM
→ 각각의 요소가 어떤 스타일을 가지는가?

브라우저는 이 두 정보를 이용해서 실제로 화면에 표시할 대상을 구성한다.

이때 등장하는 것이 Render Tree다.

text
DOM + CSSOM
      ↓
Render Tree

5. Render Tree

Render Tree는 실제로 화면에 렌더링할 요소들의 구조라고 이해할 수 있다.

여기서 중요한 점이 있다.

DOM에 존재한다고 해서 모든 요소가 Render Tree에 포함되는 것은 아니다.

예를 들어 다음과 같은 요소가 있다고 해보자.

html
<div style="display: none">
  Hello
</div>

DOM에는 존재한다.

하지만 display: none인 요소는 화면에 표시되지 않기 때문에 Render Tree에는 포함되지 않는다.

즉,

text
DOM
→ 문서에 존재하는 구조

Render Tree
→ 실제 화면에 표시할 대상

라는 차이가 있다.

이 차이를 이해하면 나중에 display: none, visibility: hidden 같은 스타일이 렌더링에 어떤 영향을 주는지도 자연스럽게 연결된다.

6. Layout — 어디에 배치할 것인가?

Render Tree가 만들어졌다고 해서 아직 화면에 그려진 것은 아니다.

브라우저는 각 요소가 어디에 있고, 얼마나 큰지 계산해야 한다.

예를 들어

html
<div class="container">
  <h1>Hello</h1>
  <p>Hello World</p>
</div>

라는 구조가 있다면 브라우저는 다음과 같은 정보를 계산해야 한다.

text
container
→ x: 0
→ y: 0
→ width: 500px
→ height: 200px

h1
→ x: 20px
→ y: 20px
→ width: ...
→ height: ...

p
→ x: 20px
→ y: ...
→ width: ...
→ height: ...

이처럼 요소의 크기와 위치를 계산하는 과정이 Layout이다.

Layout은 흔히 Reflow라는 용어와 함께 설명되기도 한다.

7. Paint — 어떻게 그릴 것인가?

이제 브라우저가 각 요소의 위치와 크기를 알게 됐다.

다음으로는 실제로 어떤 모습으로 그릴지 결정해야 한다.

예를 들어

css
button {
  width: 100px;
  height: 40px;
  background: blue;
  border-radius: 8px;
}

라면 브라우저는

  • 100 × 40 크기의 영역
  • 배경색
  • 테두리
  • 텍스트
  • 모서리

등을 실제로 그리기 위한 작업을 수행한다.

이 과정이 Paint다.

쉽게 구분하면:

text
Layout
→ 어디에 그릴까?

Paint
→ 어떤 모습으로 그릴까?

라고 이해하면 된다.

8. Composite — 레이어를 합친다

브라우저는 화면을 하나의 덩어리로만 처리하지 않는다.

상황에 따라 여러 요소를 별도의 Layer로 분리해서 처리할 수 있다.

그리고 마지막에는 이러한 레이어들을 합쳐 최종 화면을 구성한다.

이 과정을 Composite라고 한다.

개념적으로 보면:

text
Layer 1
Layer 2
Layer 3
   ↓
Composite
   ↓
최종 화면

특히 CSS Animation이나 Transform처럼 특정 요소를 별도의 레이어에서 처리할 수 있는 경우 Composite 단계에서 효율적으로 화면을 업데이트할 수 있다.

9. 브라우저 렌더링 전체 과정

지금까지의 내용을 하나로 연결하면 다음과 같다.

text
HTML
 ↓
HTML Parsing
 ↓
DOM

CSS
 ↓
CSS Parsing
 ↓
CSSOM

DOM + CSSOM
 ↓
Render Tree
 ↓
Layout
 ↓
Paint
 ↓
Composite
 ↓
화면

이것이 브라우저 렌더링을 이해하는 가장 기본적인 구조다.

10. 그렇다면 JavaScript는 어디에 있을까?

여기서 프론트엔드 개발자에게 중요한 질문이 하나 생긴다.

우리가 작성하는 JavaScript는 이 과정에서 어디에 들어갈까?

예를 들어 HTML에 다음과 같은 코드가 있다고 해보자.

html
<h1 id="title">Hello</h1>

<script>
  document.getElementById('title').textContent = 'Hello World';
</script>

브라우저가 HTML을 읽다가 JavaScript를 만나면 JavaScript를 실행할 수 있다.

그리고 JavaScript가 DOM을 변경하면 브라우저는 변경된 결과를 화면에 반영해야 한다.

text
JavaScript 실행
      ↓
DOM 변경
      ↓
스타일/레이아웃 등에 영향
      ↓
필요한 렌더링 작업
      ↓
화면 업데이트

즉 JavaScript는 브라우저 렌더링과 완전히 별개의 영역이 아니다.

JavaScript가 DOM과 스타일을 변경하면 브라우저의 렌더링 과정에 영향을 줄 수 있다.

11. JavaScript가 화면을 멈추게 할 수 있는 이유

여기서 우리가 흔히 말하는 프론트엔드 성능 문제가 등장한다.

다음과 같은 JavaScript가 있다고 해보자.

jsx
for (let i = 0; i < 10_000_000_000; i++) {
  // 매우 오래 걸리는 작업
}

브라우저에서 이 코드가 실행되는 동안 JavaScript 실행이 계속된다.

그렇다면 사용자가 버튼을 클릭하거나 화면을 스크롤하면 어떻게 될까?

브라우저가 아무것도 하지 못하는 것처럼 느껴질 수 있다.

왜 그럴까?

브라우저에서 JavaScript가 실행되는 대표적인 실행 공간인 Main Thread가 다른 작업을 처리하는 데 영향을 받기 때문이다.

개념적으로 보면:

text
Main Thread

JavaScript 실행
████████████████████████████

화면 업데이트
        대기

사용자 입력
        대기

렌더링 작업
        대기

그래서 JavaScript에서 너무 오래 걸리는 작업을 수행하면 화면이 멈추거나 입력 반응이 늦어지는 현상이 발생할 수 있다.

12. 그렇다면 JavaScript는 왜 하나의 흐름으로 실행될까?

여기서 다음 글에서 다룰 JavaScript 실행으로 자연스럽게 이어진다.

JavaScript의 실행을 이해하려면 다음 개념들이 필요하다.

text
Execution Context
      ↓
Call Stack
      ↓
JavaScript 실행

그리고 비동기 작업을 만나면 이야기가 조금 더 복잡해진다.

text
JavaScript
      ↓
Web API
      ↓
Task Queue / Microtask Queue
      ↓
Event Loop
      ↓
Call Stack

즉 브라우저 렌더링을 이해하다 보면 자연스럽게 JavaScript의 실행 구조까지 연결된다.

13. 렌더링과 JavaScript의 관계

여기서 하나 구분해야 하는 것이 있다.

브라우저 렌더링과 React 렌더링은 같은 의미가 아니다.

예를 들어 React에서 컴포넌트가 다시 렌더링된다는 것은 React가 컴포넌트 함수를 다시 실행하고 새로운 결과를 계산하는 과정이다.

반면 브라우저 렌더링은 최종적으로 브라우저가 DOM과 CSS 등의 정보를 기반으로 실제 화면을 구성하는 과정이다.

따라서 다음과 같이 구분하는 것이 좋다.

text
React Render
→ React가 UI 변경 사항을 계산하는 과정

Browser Rendering
→ 브라우저가 실제 화면을 그리는 과정

React에서 상태가 변경되면

text
State 변경
 ↓
React Render
 ↓
DOM 변경
 ↓
Browser Rendering
 ↓
화면 업데이트

와 같은 흐름으로 연결될 수 있다.

이 차이는 나중에 React의 re-render, memo, useMemo, useCallback 등을 공부할 때 매우 중요해진다.

14. 브라우저 렌더링 성능에서 중요한 것

브라우저 렌더링에서 성능을 생각할 때는 단순히 "렌더링이 느리다"라고 생각하면 안 된다.

브라우저가 화면을 만들기 위해서는 여러 작업이 필요하다.

text
JavaScript
   ↓
Style
   ↓
Layout
   ↓
Paint
   ↓
Composite

JavaScript가 너무 오래 걸릴 수도 있고,

Layout 계산이 너무 많이 발생할 수도 있고,

Paint해야 할 영역이 너무 커질 수도 있으며,

레이어를 합치는 과정에서 비용이 발생할 수도 있다.

그래서 실제 성능 문제를 분석할 때는 어떤 단계에서 시간이 많이 사용되는지 확인해야 한다.

15. 한 가지 예로 이해해보자

다음과 같은 코드가 있다고 해보자.

html
<div id="box"></div>
css
#box {
  width: 100px;
  height: 100px;
}

브라우저에서는 대략 다음과 같은 정보가 만들어진다.

text
HTML
 ↓
DOM
 ↓

CSS
 ↓
CSSOM
 ↓

DOM + CSSOM
 ↓
Render Tree
 ↓
Layout
 ↓
"box는 x=0, y=0, width=100, height=100"
 ↓
Paint
 ↓
"이 영역을 실제로 그린다"
 ↓
Composite
 ↓
화면

여기서 JavaScript가 실행되어

jsx
document.getElementById('box').style.width = '500px';

라고 한다면 기존의 화면 상태와 달라진다.

브라우저는 변경된 스타일을 바탕으로 필요한 렌더링 작업을 다시 수행해야 한다.

text
JavaScript
 ↓
Style 변경
 ↓
필요한 Layout 계산
 ↓
Paint
 ↓
Composite
 ↓
화면 변경

이런 이유로 DOM과 스타일을 어떻게 변경하느냐가 브라우저 성능에 영향을 줄 수 있다.

16. 브라우저 렌더링을 한 문장으로 정리하면

브라우저 렌더링은 단순히 HTML을 화면에 보여주는 과정이 아니다.

text
HTML
 ↓
DOM

CSS
 ↓
CSSOM

DOM + CSSOM
 ↓
Render Tree
 ↓
Layout
 ↓
Paint
 ↓
Composite
 ↓
화면

브라우저는 전달받은 리소스를 분석하고, 화면에 표시할 요소를 결정하고, 각 요소의 위치와 크기를 계산한 뒤 실제로 그려 최종 화면을 만든다.

그리고 JavaScript가 DOM이나 스타일을 변경하면 이 과정에 다시 영향을 줄 수 있다.

마무리

이제 네트워크와 브라우저 렌더링을 연결해서 볼 수 있다.

text
사용자가 URL 입력
      ↓
DNS
      ↓
IP
      ↓
TCP / QUIC
      ↓
TLS
      ↓
HTTP
      ↓
HTML / CSS / JavaScript 수신
      ↓
브라우저 렌더링
      ↓
DOM / CSSOM
      ↓
Render Tree
      ↓
Layout
      ↓
Paint
      ↓
Composite
      ↓
화면

그런데 여기서 아직 하나의 중요한 영역이 남아 있다.

JavaScript가 실제로 어떻게 실행되는가?

JavaScript가 실행되면 DOM을 변경할 수도 있고, 네트워크 요청을 만들 수도 있으며, 사용자 이벤트에 반응할 수도 있다.

그런데 JavaScript 코드는 어디에 쌓이고, 어떤 순서로 실행될까?

함수를 호출하면 무엇이 생기고, 실행이 끝나면 어떻게 사라질까?

그리고 우리가 흔히 말하는 호이스팅(Hoisting)은 이 과정에서 어디에 등장할까?

이 질문을 이해하려면 이제 브라우저 렌더링에서 한 단계 더 들어가 JavaScript의 실행 구조를 살펴봐야 한다.

다음 글에서는

text
Execution Context
        ↓
생성 단계 / 실행 단계
        ↓
Hoisting
        ↓
Call Stack
        ↓
Heap

을 중심으로 JavaScript 코드가 실제로 실행되는 과정을 살펴본다.