브라우저가 화면을 그리기까지의 과정 정리
주소창에 URL을 치고 엔터를 누르면 화면이 뜬다. 그사이에 브라우저 안에서 어떤 일이 벌어지는지 한 번 짚어보고 싶었다. 면접 준비 겸 짚어두기에도 좋은 주제라, 흐름을 두 덩어리로 나눠 정리했다. 데이터를 받아 Render Tree를 만들기까지(Construction), 그 트리를 실제 픽셀로 옮기기까지(Operation).
Construction — DNS 조회부터 Render Tree까지
웹사이트에 접속하면 브라우저는 가장 먼저 DNS에 호스트의 IP를 묻는다. 사람이 외우기 쉬운 도메인 이름을 IP 주소로 바꿔주는 단계다.
IP를 알아내면 서버와 TCP 연결을 맺는다. 3-way handshake로 부르는 절차인데, SYN과 ACK를 주고받으며 양쪽이 서로의 시작 시퀀스 번호를 확인한다.
연결이 열리면 그 위로 HTTP Request를 보낸다. 서버는 HTTP Response로 HTML·CSS·JS 같은 자원을 돌려준다. 여기까지가 네트워크 구간이다.
자원이 도착하면 브라우저는 W3C 명세에 따라 데이터를 해석한다. 렌더링 엔진은 HTML을 파싱해 DOM Tree를 만든다. 문서 구조 자체를 노드로 풀어둔 트리다.
파싱 도중 <style> 태그나 <link rel="stylesheet">를 만나면 CSS를 별도로 파싱해 CSSOM Tree를 만든다. HTML 파싱 자체는 멈추지 않고 계속 진행되지만, CSS가 완전히 파싱되기 전까지는 Render Tree를 만들 수 없어 화면 렌더링이 차단된다.
<script> 태그를 만나면 이야기가 달라진다. HTML Parsing을 중지하고 JS 엔진에 제어권을 넘긴다. 자바스크립트 엔진은 코드를 파싱해 추상 구문 트리(Abstract Syntax Tree, AST)를 만들고, AST를 바이트코드로 바꿔 인터프리터로 실행한다. 실행이 끝나면 HTML Parsing을 마저 이어간다.
HTML Parsing이 끝나면 DOM Tree와 CSSOM Tree를 합쳐 Render Tree를 만든다. Render Tree는 실제로 화면에 그려질 노드들만 모은 트리다. display: none처럼 화면에서 빠지는 노드는 여기 포함되지 않는다.
Operation — Layout, Paint, Composition
Render Tree가 만들어지면 본격적인 그리기 단계로 넘어간다.
먼저 Layout이다. Render Tree의 노드들이 화면의 어느 위치에 어떤 크기로 자리잡을지 계산한다. 이 단계가 끝나야 각 노드의 박스가 어디에 놓일지 확정된다.
그다음은 Paint다. UI 백엔드가 Render Tree의 노드를 돌면서 픽셀로 그린다. 이때 stacking context 안의 요소들은 z-index가 낮은 것부터 높은 것 순서로 그려진다. painter's algorithm으로 부르는 방식이다.
마지막은 Composition이다. Paint된 결과를 별도의 레이어로 묶고, GPU에서 그 레이어들을 합성해 최종 프레임을 만든다. 레이어 단위로 분리해두면 일부만 바뀌었을 때 전체를 다시 그리지 않아도 된다.
데이터를 다 받기 전에 그리기 시작한다
흐름을 단계별로 적어두니 "서버 응답을 다 받고 나서 한꺼번에 그린다"처럼 읽히기 쉽다. 실제로는 그렇지 않다.
브라우저는 사용자에게 화면을 빨리 보여주기 위해, 서버로부터 일부 데이터를 받자마자 화면에 표시하고, 또 데이터를 받으면 다시 화면에 표시하는 과정을 반복한다. Parsing, Layout, Paint는 응답이 도착하는 동안에도 계속 돌아간다. 그 덕분에 큰 페이지여도 위쪽부터 점차 보이기 시작한다.
흐름 자체는 짧게 적으면 한 줄짜리지만, 그 안에 DNS, TCP, HTTP, Parsing, Layout, Paint, Composition이 줄줄이 끼어 있다. 한 군데가 느려지면 결국 사용자에게 흰 화면이 길어진다는 점이, 이 파이프라인을 굳이 외워두는 이유였다.