브라우저 렌더링 과정

브라우저는 어떻게 문서를 렌더링 할까?

2024-01-24
browserrendering

요약

이 글에서는 브라우저에 주소를 입력한 순간부터 웹페이지의 콘텐츠가 표시되는 순간까지의 과정을 다룬다. OS나 네트워크 단에서의 요청과 응답보다, 응답 받은 리소스를 파싱하고 렌더링하는 과정에 초점을 맞춘다.

웹을 통해 다른 컴퓨터에 있는 자원을 요청하고 이를 브라우저에 표시하는 과정은 다음과 같이 진행된다.

  1. 탐색
  2. 응답
  3. 구문 분석 (Parsing)
    • DOM 구축
    • CSSOM 구축
    • Javascript 컴파일 및 실행
  4. 렌더
    • 렌더 트리 생성
    • Layout (Reflow)
    • Paint (Repaint)
    • Composite (합성)

1) 탐색

사용자가 필요한 리소스는 주로 다른 컴퓨터(이 글에서는 웹 서버)에 존재하며, 브라우저에서는 주소창에 주소를 입력하거나, form 제출, 링크 클릭 등으로 서버에 자원을 요청한다. https://example.com을 주소창에 직접 입력해 콘텐츠를 요청하는 상황이라면, 해당 주소에 대응되는 IP 주소를 먼저 알아내야 한다.

https://example.com과 같은 주소는 사람이 편하게 쓰도록 만들어진 주소이고, 실제 클라이언트-서버 간 데이터 이동은 IP 주소에 의해서만 이루어지기 때문이다. 만약 이 사이트를 접근한 적이 없다면 DNS 조회를 통해 IP 주소를 알아낸다.

DNS 조회

DNS 조회는 브라우저가 직접 하지 않고, OS의 DNS 리졸버를 통해 수행한다. DNS 리졸버는 DNS 서버에 요청하기 전에 자체 캐시를 확인한다. 캐시에 해당 주소에 대한 IP 주소 맵핑이 존재한다면 IP 주소를 반환한다. 존재하지 않는다면 IP 주소를 얻을 때까지 여러 DNS 서버에 질의한다. Default DNS 서버 -> Root DNS 서버 -> TLD DNS 서버 -> Authoritative DNS 서버 등 계층 구조로 이루어진 여러 DNS 서버에 재귀적으로 혹은 순차적으로 질의하여 IP 주소를 얻어낸다.

웹페이지 내 이미지, 글꼴, 스크립트가 각각 다른 주소로 자원을 요청한다면 각 주소에 대해 DNS 조회를 수행해야 한다.

TCP Handshake

DNS 조회로 IP 주소를 알아냈다면, 해당 IP 주소를 가진 서버와 데이터를 주고받기 위해 TCP 연결을 맺어야 하며, 이 과정을 TCP Handshake라 한다. TCP Handshake는 데이터를 전송하기 전 두 주체가 TCP 연결을 위한 매개변수를 주고받는 과정이다. 이 경우 두 주체는 브라우저와 웹 서버가 된다. SYN(SYNchronize), SYN-ACK(SYNchronize-ACKnowledgement), ACK(ACKnowledge) 메시지를 주고받으면서 TCP 연결을 맺기 때문에 3-way-handshake라고도 한다.

Handshake 순서

  1. 연결을 맺고자 하는 클라이언트가 SYN bit를 1로 설정한 패킷을 보낸다.
  2. 서버가 SYN과 ACK bit 모두 1로 설정한 패킷을 보내 응답한다.
  3. 클라이언트가 마지막으로 ACK bit를 1로 설정한 패킷을 보내면서 연결이 설정된다.

이제 TCP 프로토콜을 통해 데이터를 주고 받을 수 있는 연결이 생겼다.

TLS Handshake (Negotiation)

TCP Handshake를 통해 TCP 연결을 맺었다면 HTTPS를 이용한 안전한 데이터 통신을 위해 TLS Handshake가 수행된다. 이를 TLS 협상이라고도 부른다. TLS handshake는 클라이언트와 서버가 서로를 검증하고, 암호화 통신을 위해 협상하는 과정이다.

DNS 조회부터 TCP handshake, TLS handshake를 모두 거치고 나서야 브라우저는 자원을 요청할 수 있다.

2) 응답

https://example.com에 대한 자원 요청을 위해 브라우저는 유저 대신 HTTP GET 요청을 수행한다. 웹 서버는 https://example.com와 같이 root 주소로 GET 요청이 들어왔을 때 보통 index.html을 요청한 것으로 간주하고 이에 따른 응답을 보낸다.

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>My simple page</title>
    <link rel="stylesheet" href="style.css" />
    <script src="myscript.js"></script>
  </head>
  <body>
    <h1 class="heading">My Page</h1>
    <p>A paragraph with a <a href="https://example.com/about">link</a></p>
    <div>
      <img src="myimage.jpg" class="dog" alt="image description" />
    </div>
    <script src="anotherscript.js"></script>
  </body>
</html>

3) 구문 분석 (Parsing)

구문 분석은 브라우저가 응답 받은 데이터를 DOM과 CSSOM으로 만들어내는 과정이다. 브라우저는 서버로부터 응답 받은 HTML 문서와 스타일시트를 읽어들이기 시작한다. HTML과 CSS를 DOM과 CSSOM으로 만들기 위해 공통적으로 아래와 같은 과정을 거친다.

파싱 과정

바이트 형태의 데이터를 위와 같은 단계로 변환하여 최종적으로 DOM과 CSSOM을 만들어낸다.

먼저 HTML 문서를 파싱하는 과정부터 살펴보자.

DOM 구축

HTML 문서는 바이트의 형태로 브라우저에게 전달된다. 하지만 바이트 자체로는 브라우저가 콘텐츠를 렌더링 하는 데 사용할 수 없다. 따라서 바이트 형태의 데이터를 브라우저가 활용할 수 있는 형태로 파싱해야 하는데, 이 결과물이 문서 객체 모델, DOM(Document Object Model)이다. DOM은 문서 구조와 정보를 표현하여 문서 구조, 스타일, 내용 등을 변경할 수 있도록 하는 프로그래밍 인터페이스이자 자료구조다.

DOM을 만들기 위해서는 다음과 같은 과정을 거친다.

  1. 바이트 형태의 응답을 응답 헤더의 charset에 명시된 인코딩 방식으로 문자열로 변환한다.
  2. 문자열을 토큰화한다.
  3. 토큰을 노드로 변환한다.
  4. 노드를 트리 구조로 연결한다.
    • HTML 문서는 다음과 같이 중첩구조를 가지는데, 이와 같은 중첩 구조는 트리에서 부모 자식 관계로 표현된다.
    • <body><h1 class="heading">My Page</h1></body>

위 과정을 거쳐 구축된 DOM은 다음과 같이 표현할 수 있다.

DOM

사진 출처: MDN

Render Blocking resources

HTML을 파싱하여 DOM을 구축하는 과정을 blocking하는 리소스가 있다. 직접적으로 HTML 파싱을 막는 Parser blocking resource와 전체 렌더링 프로세스를 막는 Render blocking resource로 나눌 수 있다.

async, defer가 적용되지 않은 <script>Parser blocking resource다. HTML 파싱이 스크립트에 의해 멈추는 이유는 스크립트에서 DOM이나 CSSOM에 변경을 가할 수 있고, 변경이 가해지면 DOM이나 CSSOM을 다시 구축해야 하기 때문이다. 따라서 스크립트를 만나면 HTML 파싱을 이어갈 수 없다.

CSS는 Render blocking resource다. 추후 살펴보겠지만 렌더 트리를 구축하기 위해서는 CSSOM이 필요하다. 따라서 CSS 파싱은 렌더를 blocking한다.

프리로드 스캐너

HTML을 파싱해 DOM을 구축하는 과정은 메인 스레드를 점유한다. 이 과정을 더 빠르게 하기 위해 프리로드 스캐너는 HTML 파서가 필요한 자원을 우선순위에 따라 미리 요청한다. 뒤에서 CSS, JS 등의 자원을 미리 요청하여 메인 HTML 파서의 작업을 더 빨리 끝낼 수 있도록 도와준다. HTML 파서가 해당 리소스를 파싱해야 할 때는 이미 자원에 대한 응답을 받았거나, 응답을 기다리는 중일 것이고, 이 덕분에 파싱 과정의 blocking을 줄일 수 있다.

우리의 예제에서 프리로드 스캐너는 다음과 같은 리소스를 미리 요청한다.

<link rel="stylesheet" href="styles.css" />
<script src="myscript.js" async></script>
<img src="myimage.jpg" class="dog" alt="image description" />
<script src="anotherscript.js" async></script>

CSS를 얻는 과정은 HTML 파싱이나 다운로드를 막지 않는다. 하지만 Javascript의 실행은 막을 수 있다. 스크립트가 CSS 속성을 활용할 수 있기 때문이다.

CSSOM 구축

CSSOM은 CSS를 파싱하여 생성되는 객체 모델이다. CSSOM 파싱 과정은 DOM 파싱 과정과 유사하며, 아래와 같은 과정을 거쳐 CSSOM을 구축한다.

  1. 바이트 형태의 응답을 응답 헤더의 charset에 명시된 인코딩 방식으로 문자열로 변환한다.
  2. 문자열을 토큰화한다.
  3. 토큰을 노드로 변환한다.
  4. 노드를 트리 구조로 연결한다.

CSSOM은 DOM과 유사한 트리구조이나, 두 자료구조는 독립적이다. 아래는 예제 CSS와, CSS를 CSSOM으로 변환한 그림이다.

body {
  font-size: 16px;
}
p {
  font-weight: bold;
}
span {
  color: red;
}
p span {
  display: none;
}
img {
  float: right;
}
CSSOM

사진 출처: Constructing the Object Model

위 그림에서는 나타나지 않았지만 CSSOM은 user agent의 기본 스타일을 포함한다. Chromium 소스 코드의 html.css를 보면 user agent의 기본 스타일을 확인할 수 있다.

스타일 속성은 부모 노드에서 자식 노드로 상속된다. 위 그림에서 bodyfont-sizepspan, img를 포함해 그 아래 모든 노드에게 상속되는 것을 볼 수 있는데, 위에서 아래로 스타일 속성이 흐르기 때문에 cascade 된다고도 한다. CSSOM을 구성할 때는 user agent가 제공하는 가장 일반적인 CSS 규칙부터, 더 구체적인 규칙을 재귀적으로 적용하며 계산된 스타일을 확정한다.

Javascript 컴파일 및 실행

HTML을 파싱하다가 <script>를 만나면 파싱이 blocking 된다고 했다. 이때 제어권은 Javascript 엔진에게 넘어가며, Javascript 엔진은 다음과 같이 스크립트를 컴파일하고 실행한다.

먼저 Javascript 엔진은 스크립트를 파싱해 추상 구문 트리 (AST)를 만든다. 그리고 인터프리터가 이 추상 구문 트리를 실행 가능한 바이트코드로 변환하여 실행한다. Javascript 엔진에 따라 이 과정에서 추가적으로 최적화를 수행하는 컴파일러가 존재하기도 한다.

4) 렌더

응답 받은 문서를 파싱까지 완료했다면 실제 화면에 콘텐츠를 렌더해야 한다. 렌더링은 렌더 트리 생성 (Style), Layout, Paint, Composite로 나눌 수 있다.

렌더 트리 생성 (Style)

브라우저의 렌더러는 DOM과 CSSOM을 조합하여 렌더 트리, 다른 말로 계산된 스타일 트리를 구성한다. DOM 루트 노드부터 '보이는' 모든 노드의 계산된 스타일을 포함한 렌더 트리를 구성한다. 이때 <script>, <head>와 그 자식 요소처럼 display: none이 설정된 보이지 않는 요소는 렌더 트리에서 제외된다. 반면 div::before { content: 'hello' }와 같은 의사 클래스의 콘텐츠는 DOM에는 포함되지 않지만 렌더 트리에 포함된다.

Chromium 소스 코드에서도 기본적으로 아래 요소는 렌더 트리에 포함하지 않음을 확인할 수 있다.

/* https://html.spec.whatwg.org/multipage/rendering.html#hidden-elements */
/* TODO(crbug.com/1231263): <area> should be display:none. */
area {
  display: inline;
}
base,
basefont,
datalist,
head,
link,
meta,
noembed,
noframes,
param,
rp,
script,
style,
template,
title {
  display: none;
}
input[type="hidden" i] {
  display: none !important;
}

visibility: hidden vs opacity: 0 vs display: none

개발을 하다보면 간혹 요소가 보이지 않게 해야하는 요구사항을 마주한다. 이러한 요구사항은 세 가지 CSS 속성으로 구현할 수 있는데, 각 속성이 렌더링에 미치는 영향은 다르다. 간략하게 세 속성의 차이를 요약하자면 display: none이 설정된 요소는 렌더 트리에서 제외되지만 visibility: hiddenopacity: 0이 설정된 요소는 렌더 트리에 포함된다. 또 opacity가 1 이하이면 새로운 쌓임 맥락을 생성한다.

display: none

visibility: hidden

opacity: 0

Layout (Reflow)

레이아웃은 렌더 트리의 모든 요소의 위치와 크기를 계산하는 과정이다. 렌더 트리를 생성하며 각 요소의 스타일은 계산되었지만 실제 위치와 크기는 결정되지 않았다. 브라우저는 레이아웃을 결정하기 위해 렌더 트리의 루트부터 순회하여 뷰 포트에 따라 요소의 위치와 크기를 계산한다. 이때 width: 50%와 같은 상대적인 값이 실제 픽셀로 변환된다.

처음 요소의 위치와 크기를 결정하는 것을 레이아웃이라고 하고, 이후에 변경된 위치와 크기를 재계산하는 것을 리플로우(reflow)라고 한다.

리플로우는 다양한 이유에 의해 트리거될 수 있다. 화면 크기가 변경된다거나, 위치를 차지하는 새로운 DOM 요소가 추가된다거나, 각종 CSS 속성의 값이 변경되어 요소의 위치와 크기가 변경되는 등 다양한 이유로 발생한다.

아래 두 사이트에서 레이아웃이 트리거되는 CSS 속성을 확인할 수 있다.

  1. https://csstriggers.com/
  2. https://lab.skk.moe/css-triggers

Paint (Repaint)

페인트는 레이아웃 과정에서 계산된 위치와 크기에 따라 실제 픽셀을 채우는 과정이다. 이 과정에서 텍스트, 이미지, 색상, 경계, 그림자 등을 화면에 표시한다.

렌더 트리 생성, 레이아웃, 페인팅은 모두 메인 스레드에서 실행되며, 각 작업은 이전 작업이 완료된 후에 실행된다는 점에 유의하자. 부드러운 스크롤이나 애니메이션을 위해서 위 작업은 16.67ms 이내에 수행되어야 한다. 보통 초당 프레임 수가 60fps이기 때문에 1초인 1000ms를 60으로 나눈 값인 16.67ms 이내에 작업이 완료되어야 유저가 느끼기에 부드러운 화면을 제공할 수 있다.

만약 작업이 16.67ms 이내에 완료되지 못하면 웹 페이지가 버벅이는 것 처럼 보인다. 이 작업은 메인 스레드에서 실행되기 때문에 Javascript가 실행되는 동안 중단될 수 있으며, 이를 해결하기 위해 requestAnimationFrame()을 통해 프레임마다 실행될 작업을 스케줄 하거나, 웹 워커에서 Javascript를 실행하는 방법이 있다.

페인트 과정은 일반적으로 몇개의 레이어로 구분되어 수행된다.

Composite (합성)

페인트 과정에서 구분된 레이어는 합성 과정을 거쳐 최종 화면에 표시된다. opacity, 3D transform, will-change 등의 CSS 속성과 <video>, <canvas> 요소가 레이어 생성을 유발한다. 콘텐츠를 CPU의 메인 스레드에서 GPU 레이어로 격상하면 일반적으로 페인트와 리페인트 성능을 높일 수 있다. 하지만 레이어는 메모리 관리 측면에서는 비싼 작업이기 때문에 과도하게 쓰이지는 않아야 한다.

레이어를 합성하여 화면에 표현하면 큰 틀에서의 렌더링 과정이 마무리된다.

참고자료


인터넷 프로토콜
모던 자바스크립트 Deep Dive 38장, 브라우저 렌더링 과정

공식 문서
브라우저는 어떻게 동작하는가

커뮤니티
How browsers work (원문)
브라우저는 어떻게 동작하는가? (번역)
성능 최적화
Constructing the Object Model
Render-tree Construction, Layout, and Paint
Rendering performance
최신 브라우저의 내부 살펴보기 3 - 렌더러 프로세스의 내부 동작
Understanding the critical path

영상
[10분 테코톡] ☕️ 체프의 브라우저 렌더링 웹개발자면서 이것도 모름? | DOM과 CSSOM, 렌더링 과정


비슷한 글이 더 있어요.

web
CSR, SSR, SSG
2023/09/27

CSR, SSR, SSG