브라우저는 웹 페이지를 어떻게 화면에 그릴까?
브라우저가 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라고 이해하면 됩니다.