브라우저는 어떻게 렌더링 되는가
1. 브라우저의 구조
- 사용자 인터페이스: 웹페이지 상단바의 뒤로 가기, 앞으로 가기, 새로고침 등 사용자가 어떤 페이지에 접속하더라도 바뀌지 않고 사용할 수 있는 UI를 말한다.
- 브라우저 엔진: 사용자 인터페이스와 렌더링 엔진을 이어주는 역할을 한다. 예를 들어 사용자가 사용자 인터페이스의 뒤로 가기 버튼을 누르면 렌더링 엔진이 이전 페이지를 렌더링 하도록 한다.
- 렌더링 엔진: 웹사이트를 그리는 엔진으로 우리가 보는 페이지를 화면으로 보여주는 엔진을 말한다
- 통신: 웹 브라우저의 네트워크를 담당하는 부분이다. 개발자 툴의 네트워크 탭에서 활동을 확인할 수 있다.
- 자바스크립트 해석기: 자바스크립트를 인식하고 실행하여 웹 페이지의 동적인 기능을 동작하도록 하는 부분이다. 크롬의 V8 엔진, 사파리의 Webkit, 파이어폭스의 Spider Monkey 등이 있다.
- UI 백엔드: 사용자의 입력, 클릭 이벤트 등을 핸들링하는 부분이다.
- 자료 저장소: 필요한 데이터를 저장하는 곳으로 로컬스토리지, 세션스토리지, 쿠키 등이 여기에 해당한다.
2. 브라우저의 렌더링 과정
서버로부터 리소스 받아오기
우리가 브라우저의 주소 검색창에 주소(예: www.google.com)를 입력하면 DNS(Domain Name System)를 통해 해당 주소의 IP(예:192.5.30.3)로 변환합니다. 여기서 DNS는 사람이 읽을 수 있는 형태의 도메인을 PC가 읽을 수 있는 형태로 변환시켜 주는 도메인 이름 시스템입니다. 변환된 주소는 DNS cache에 저장되어 다음번에는 더 빠르게 IP 주소를 찾아올 수 있게 됩니다.
브라우저가 DNS를 통해 변환된 주소로 리소스를 요청하면 서버는 0과 1로 이루어진 바이트 스트림으로 데이터를 전달합니다. 전달받은 바이트 스트림을 이해하기 위해서는 인코딩의 과정이 필요하게 되는데 가장 대표적인 방법이 UTF-8입니다.
HTML의 head에 meta 태그를 사용하여 HTML이 어떤 방식으로 인코딩 되었는지 명시합니다.
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
DOM 트리 만들기
토큰화는 인코딩을 통해 문서화된 HTML을 해석하며 토큰을 만드는 과정입니다. 문자열을 순차적으로 파싱 하여 형태별로 토큰을 만들게 됩니다. 이렇게 만들어진 토큰을 바탕으로 의미를 가진 객체 형태로 다시 해석하게 되는데 이것을 노드라고 합니다.
DOM(Document Object Model)은 이러한 노드들을 정의하고, 그들 사이의 관계를 설명해 주는 역할을 합니다.
CSSOM 트리 만들기
브라우저가 HTML을 파싱 하는 과정에서 link 태그의 CSS 파일을 확인하면 서버에 다시 CSS 파일을 요청하게 됩니다. 서버는 HTML과 마찬가지로 바이트 스트림의 형태로 CSS 파일을 전달합니다. 이후 HTML과 동일하게 문서화, 토큰화, 노드를 거쳐 최종적으로 CSSOM 트리를 생성합니다. CSSOM(CSS Object Model)는 DOM과 동일한 형태로 스타일 정보를 담은 계층적 구조입니다.
브라우저에 화면이 렌더링 되기 위해서는 DOM과 CSSOM이 모두 완성되어야 합니다. DOM 생성이 완료되었지만 CSSOM이 완성되지 않은 경우에는 CSSOM이 완전히 생성될 때까지 기다렸다가 CSSOM이 완성되면 그때 브라우저의 화면에 렌더링 됩니다. 따라서 CSS 파일이 지나치게 무거울 경우에는 브라우저의 렌더링 속도에 영향을 미칠 수 있습니다.
Render 트리 만들기
브라우저는 화면을 그리기 위해 DOM 트리와 CSSOM 트리를 합쳐 최종적으로 Render 트리를 만듭니다. Render 트리는 화면을 그리기 위한 트리이기 때문에 렌더링에 불필요한 노드(head 태그, meta 태그, display: none으로 설정된 노드 등)를 제외하고 생성됩니다.
Layout
각 노드들의 위치와 크기를 정확하게 계산하는 단계입니다. 예를 들어 화면의 50% 크기로 스타일 된 경우 화면 크기의 50%가 몇 px인지 계산하여 적용시키게 됩니다.
Paint
Render 트리와 Layout의 계산값을 바탕으로 화면의 픽셀을 그리는 단계입니다. 변경사항이 발생했을 때 효율적으로 렌더링 할 수 있도록 한 개의 레이어에 모든 화면을 렌더링 하지 않고 여러 개의 레이어로 나눠서 렌더링이 진행됩니다.
Composite
Paint 단계에서 만들어진 여러 개의 레이어를 하나로 합성하여 보여주는 단계입니다. Composite 과정을 통해 우리가 보는 브라우저 화면이 완성됩니다.
3. 브라우저의 렌더링 성능을 향상하는 방법
브라우저의 화면에 변경사항이 생길 때마다 브라우저는 위의 Layout, Paint, Composite 과정을 반복하게 됩니다. Layout과 Paint는 브라우저 렌더링에서 많은 연산을 필요로하는 무거운 과정이며 따라서 Layout부터 다시 진행되는 reflow와 Paint부터 다시 진행되는 repaint는 웹 성능에 큰 영향을 미치게 됩니다.
예시 코드 1
<script>
function onClick() {
const $ul = document.getElementById("ul");
for(let i = 0; i < 3000; i++) {
$ul.innerHTML += `<li>${i}</li>`;
}
}
</script>
<body>
<button onclick="onClick()">리스트 추가하기</button>
<ul id="ul"></ul>
</body>
위의 예시 코드는 버튼을 클릭하면 ul에 리스트를 하나씩 추가하는 코드입니다. onClick 함수의 내부를 보면 innerHTML을 통해 DOM을 수정하고 있는 것을 볼 수 있는데, 그 결과 3000번의 reflow와 repaint가 발생하게 됩니다. 실제로 성능 측정 결과 4,500ms(화면이 렌더링 되기까지 약 4.5초 소요)라는 매우 부정적인 결과를 얻을 수 있습니다.
예시 코드 2
<script>
function onClick() {
const $ul = document.getElementById("ul");
let list = "";
for(let i = 0; i < 3000; i++) {
list += `<li>${i}</li>`;
}
$ul.innerHTML = list;
}
</script>
<body>
<button onclick="onClick()">리스트 추가하기</button>
<ul id="ul"></ul>
</body>
위 예시 코드는 예시 코드 1과 기능은 동일하지만 list라는 변수에 리스트를 담아두고 반복문이 끝나면 딱 한번만 innerHTML을 수행하도록 하는 코드입니다. 성능 측정 결과 250ms로 예시 코드 1에 비해 엄청난 성능 향상을 확인할 수 있습니다.
그렇다면 어떤 경우에 reflow가 발생하게 될까요? reflow가 발생하는 시점은 다음과 같습니다.
- 페이지 초기 렌더링 시
- 윈도우 리사이징 시
- 노드 추가 또는 삭제 시
- 요소의 위치, 크기 변경 시
- 폰트 변경 시
- 이미지 크기 변경 시
따라서 reflow가 브라우저 렌더링 성능에 미치는 영향에 대한 이해를 바탕으로 필요에 따라 reflow를 효율적으로 제어함으로써 성능과 사용자 경험을 향상할 수 있습니다.