본문으로 건너뛰기

브라우저는 웹 페이지를 어떻게 화면에 그릴까?

브라우저가 HTML 파일을 화면에 보여주기까지는 여러 단계가 필요합니다. HTML을 읽어 DOM을 만들고, CSS를 계산한 뒤 각 요소의 위치와 크기를 결정하고, 실제 화면에 그리는 과정을 거칩니다.

전체적인 흐름은 다음과 같습니다.

HTML
↓
HTML Parsing
↓
DOM Tree
↓
CSS Parsing
↓
CSSOM
↓
Render Tree
↓
Style 계산
↓
Layout
↓
Paint
↓
Composite
↓
화면에 표시

1. HTML을 파싱해 DOM Tree를 만든다​

브라우저는 전달받은 HTML을 위에서부터 파싱하면서 DOM(Document Object Model)을 만듭니다.

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

이 HTML은 다음과 같은 트리 구조로 표현됩니다.

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

이것이 DOM Tree입니다.

JavaScript에서 document.querySelector()처럼 DOM을 조작할 수 있는 것도 브라우저가 HTML을 DOM이라는 객체 구조로 만들어두기 때문입니다.


2. CSS를 파싱해 CSSOM을 만든다​

HTML과 함께 CSS도 파싱합니다.

h1 {
color: red;
}

p {
font-size: 20px;
}

브라우저는 CSS를 분석해 CSSOM(CSS Object Model)을 만듭니다.

즉,

HTML → DOM Tree
CSS → CSSOM

이라고 생각하면 됩니다.


3. DOM과 CSSOM을 이용해 Render Tree를 만든다​

브라우저는 DOM과 CSSOM을 결합해 화면에 실제로 표시할 요소들의 정보를 구성합니다.

이를 Render Tree라고 합니다.

여기서 중요한 점은 DOM에 있는 모든 요소가 Render Tree에 들어가는 것은 아니라는 것입니다.

예를 들어:

.hidden {
display: none;
}

인 요소는 화면에 표시되지 않기 때문에 Render Tree에서 제외됩니다.

DOM Tree CSSOM
│ │
└──────┬───────┘
↓
Render Tree

Render Tree는 이후 실제 화면의 위치와 크기를 계산하고 그리는 데 사용됩니다.


4. Style 계산​

브라우저는 각 요소에 어떤 CSS가 최종적으로 적용되는지 계산합니다.

예를 들어 여러 CSS 규칙이 존재한다면 상속, 우선순위, 캐스케이드 등을 고려해 최종 스타일을 결정합니다.

DOM + CSSOM
↓
최종 스타일 계산

이 결과를 바탕으로 다음 단계인 Layout을 진행합니다.


5. Layout: 요소의 크기와 위치를 계산한다​

이제 브라우저는 각 요소가 화면의 어디에, 얼마나 크게 위치할지 계산합니다.

예를 들어:

h1
x: 20px
y: 30px
width: 200px
height: 40px

처럼 각 요소의 geometry를 계산합니다.

이 과정을 Layout이라고 합니다.

과거에는 이 과정을 Reflow라고 부르기도 합니다.

예를 들어 부모 요소의 크기가 변경되어 자식 요소의 위치까지 다시 계산해야 한다면 Layout이 다시 발생할 수 있습니다.


6. Paint: 화면에 무엇을 그릴지 결정한다​

Layout이 끝나면 브라우저는 계산된 정보를 바탕으로 실제로 무엇을 그릴지 준비합니다.

예를 들어:

  • 배경색
  • 텍스트
  • 테두리
  • 그림자
  • 이미지

등을 그리는 작업입니다.

이 과정을 Paint라고 합니다.

요소의 색상이나 배경 등이 변경되어 다시 그려야 하는 경우 흔히 Repaint가 발생했다고 표현합니다.

Layout
↓
Paint
↓
"무엇을 그릴지 다시 그림"

7. Composite: 여러 레이어를 합친다​

Paint가 끝나면 브라우저는 만들어진 여러 레이어를 최종 화면으로 합칩니다.

이 과정을 Composite라고 합니다.

특히 transform이나 opacity와 같이 상황에 따라 이미 그려진 레이어를 이동하거나 합성하는 방식으로 처리할 수 있는 속성은 Layout과 Paint를 다시 수행하지 않고 Composite 단계에서 처리될 수 있습니다.

예를 들어:

.box {
transform: translateX(100px);
}

이 경우 브라우저가 최적화할 수 있다면 이미 그려진 .box를 새로운 위치에 합성하는 방식으로 처리할 수 있습니다.

따라서 transform은 애니메이션에서 자주 사용됩니다.

단, transform이 항상 Paint를 건너뛴다는 의미는 아닙니다. 실제 동작은 변경 내용과 브라우저의 최적화 여부에 따라 달라질 수 있습니다.


8. 그렇다면 requestAnimationFrame은 무엇일까?

requestAnimationFrame은 브라우저에게 다음 화면 업데이트 시점에 맞춰 콜백을 실행해달라고 요청하는 API입니다.

requestAnimationFrame(() => {
// 다음 프레임에 실행할 코드
});

예를 들어 애니메이션을 구현할 때:

function animate() {
box.style.transform = `translateX(${x}px)`;
x++;

requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

브라우저의 화면 업데이트에 맞춰 JavaScript를 실행할 수 있기 때문에 애니메이션에 적합합니다.

개념적으로는 다음과 같이 이해할 수 있습니다.

JavaScript
↓
requestAnimationFrame()
↓
다음 렌더링 기회
↓
rAF Callback 실행
↓
필요한 렌더링 작업
↓
Style
↓
Layout
↓
Paint
↓
Composite
↓
화면 업데이트

다만 실제 브라우저의 렌더링 과정은 이보다 복잡하며, 모든 프레임에서 Style → Layout → Paint → Composite가 전부 다시 실행되는 것은 아닙니다.


9. setTimeout보다 requestAnimationFrame을 사용하는 이유

애니메이션을 구현할 때 다음과 같이 setTimeout을 사용할 수도 있습니다.

function animate() {
box.style.transform = `translateX(${x}px)`;
x++;

setTimeout(animate, 16);
}

하지만 setTimeout은 시간을 기준으로 다음 실행을 예약할 뿐, 화면이 업데이트되는 시점과 직접 연결되어 있지는 않습니다.

반면 requestAnimationFrame은 브라우저의 화면 업데이트에 맞춰 콜백을 실행합니다.

이 차이를 이해하려면 디스플레이의 주사율(Refresh Rate)을 알아야 합니다.

주사율과 프레임​

모니터의 주사율이 60Hz라면 1초에 최대 60번 화면을 업데이트할 수 있습니다.

1000ms ÷ 60 ≈ 16.7ms

따라서 한 프레임을 업데이트하는 데 사용할 수 있는 시간이 약 16.7ms입니다.

120Hz 디스플레이라면:

1000ms ÷ 120 ≈ 8.3ms

로 프레임 간격이 더 짧아집니다.

즉, 화면의 주사율에 따라 브라우저가 화면을 업데이트하는 타이밍이 달라집니다.

setTimeout의 문제​

setTimeout(animate, 16);

은 단순히 약 16ms 이후에 Task를 실행해달라는 의미입니다.

화면 업데이트가 실제로 언제 발생하는지는 고려하지 않습니다.

반면:

requestAnimationFrame(animate);

은 브라우저가 다음 화면 업데이트를 준비하는 시점에 맞춰 콜백을 실행합니다.

따라서 애니메이션을 다음과 같이 화면의 프레임과 맞춰 실행할 수 있습니다.

60Hz 디스플레이

Frame 1 Frame 2 Frame 3
│ │ │
▼ ▼ ▼
rAF rAF rAF
│ │ │
▼ ▼ ▼
화면 업데이트 화면 업데이트 화면 업데이트

그래서 requestAnimationFrame은 모니터의 주사율과 브라우저의 렌더링 타이밍에 맞춰 애니메이션을 실행할 수 있다는 점에서 setTimeout보다 적합합니다.

또한 브라우저는 화면이 보이지 않는 백그라운드 탭 등에서는 requestAnimationFrame의 실행을 일시 중지하거나 빈도를 낮출 수 있어, 화면에 실제로 표시되는 애니메이션에 맞춰 작업할 수 있습니다.

단, requestAnimationFrame이 정확히 16.7ms 또는 8.3ms마다 실행된다는 의미는 아닙니다. 브라우저가 다음 렌더링 업데이트를 수행할 수 있는 시점에 맞춰 실행한다는 것이 핵심입니다.


정리

브라우저가 처음 HTML을 화면에 표시하는 과정은 다음과 같이 정리할 수 있습니다.

HTML
↓
DOM Tree
+
CSS
↓
CSSOM
↓
Render Tree
↓
Style
↓
Layout
↓
Paint
↓
Composite
↓
🖥️ 화면

그리고 애니메이션처럼 화면을 계속 변경해야 하는 경우:

requestAnimationFrame()
↓
다음 렌더링 기회
↓
rAF Callback 실행
↓
필요한 렌더링
↓
화면 업데이트
↓
requestAnimationFrame()
↓
...

setTimeout이 시간을 기준으로 실행을 예약한다면, requestAnimationFrame은 브라우저의 화면 업데이트 타이밍에 맞춰 실행을 예약합니다.

따라서 requestAnimationFrame은 주사율이 다른 디스플레이에서도 브라우저의 프레임 업데이트에 맞춰 애니메이션을 실행할 수 있어 화면 애니메이션에 적합합니다.

결국 requestAnimationFrame은 브라우저의 다음 화면 업데이트에 맞춰 JavaScript를 실행하기 위한 API라고 이해하면 됩니다.