브라우저 렌더링 동작 이해하기

Jihye Hong
6 min readJun 27, 2019

--

HTML, CSS, 자바스크립트로 작성된 코드가 실제 브라우저 화면에 어떻게 보여지는지 궁금하신 적 있으신가요? 브라우저 성능을 높이기 위한 기술들이 왜 제안되었으며 어떻게 설계되었는지 자세히 알고 싶으신가요? 그런 의문들을 파헤치고자 여러분이 브라우저 주소창에 URL을 입력하는 순간부터 웹 페이지가 화면에 보이기까지 브라우저가 어떤 일들을 하는지 알아봅니다. 웹 브라우저를 구현하는 방법에 대한 표준은 없습니다. 다양한 구현 방법이 있겠지만, 본 글에서는 크롬 브라우저 기준으로 설명 드립니다.

우선 브라우저가 어떻게 구성되는지 간단히 살펴보면, 브라우저는 IPC를 통해 통신하는 다양한 프로세스로 구성되며 각 프로세스는 서로 다른 여러 개의 스레드를 가집니다. 아래의 그림에서는 각 기능별로 구분되어 브라우저를 구성하는 프로세스들을 보여줍니다.

이 중에서 브라우저 프로세스와 렌더러 프로세스가 “웹 페이지가 보이는 과정”의 핵심 역할을 합니다. 크롬 브라우저가 실행되면, 브라우저 창 하나 당 한 개의 브라우저 프로세스가 생성되며, 그 안의 탭 하나 당 한 개의 렌더러 프로세스가 생성됩니다. 즉, 탭 밖에 있는 것들은 모두 브라우저 프로세스가 담당하며 렌더러 프로세스는 탭 안에서 일어나는 모든 일들을 담당합니다. 브라우저 프로세스는 버튼이나 입력창을 그리며 그에 대한 사용자 입력을 처리하는 UI 스레드, 인터넷에서 데이터를 송수신하기 위한 네트워크 스레드, 리소스 파일 및 캐쉬 등을 다루는 스토리지 스레드 등을 가지고 있습니다. 렌더러 프로세스 안에서는 개발자들이 구현한 코드 대부분을 실행하는 메인 스레드, 웹 워커 혹은 서비스 워커를 실행하는 워커 스레드가 있습니다. 컴포지터와 레지스터 스레드 또한 렌더러 프로세스 내부에서 페이지를 효율적이며 매끄럽게 렌더링하기 위해 실행됩니다. 렌더러 프로세스의 핵심 역할은 HTML, CSS 그리고 자바스크립트를 사용자가 인터렉션할 수 있는 웹 페이지로 보여주는 것입니다.

그럼 이제 하나의 페이지를 화면에 보여주기 위해 이런 프로세스들, 또 그 안의 스레드들이 어떤 일을 하는지 알아보겠습니다. 주소창에 URL을 입력하는 순간 브라우저 프로세스의 UI 스레드가 실행되면서 우선 입력된 내용이 검색어인지 URL인지 판단합니다. 크롬에서는 주소창이 검색창도 겸하기에 입력 내용을 검색 엔진에 보낼지, 요청한 페이지로 연결할지 결정합니다. URL인 경우, 해당 URL의 리소스를 가지고 오도록 네트워크 스레드에 요청합니다. 이 시점에 안전한 브라우징인지 확인을 하게 되는데, URL에 해당하는 페이지가 악성 페이지이면 네트워크 스레드는 경고 문구를 보여줍니다. 또한 Cross Origin Read Blocking (CORB) 확인을 통해 cross-site 데이터는 렌더러 프로세스에 전달 하지 않습니다. 다행히 리소스가 안전한 HTML 파일이면 브라우저 프로세스는 렌더러 프로세스에게 웹 페이지를 그리도록 요청합니다. 렌더러 프로세스가 실행되면, 브라우저 프로세스의 네트워크 스레드는 계속해서 렌더러 프로세스에게 해당 페이지에 대한 데이터 스트림을 보내기 시작합니다. 그리고 이 때부터 해당 브라우저 탭의 파비콘(favbicon)에서 로딩 애니메이션이 돌기 시작합니다.

이제 렌더러 프로세스 차례입니다. 렌더러 프로세스가 하는 일들은 아래의 그림으로 요약됩니다. 그리고 이 일련의 과정은 브라우저 렌더링 파이프라인이라고 합니다.

렌더러 프로세스가 데이터를 받기 시작하면, 렌더러 프로세스의 메인 스레드는 HTML 파일의 텍스트 문자열을 파싱하기 시작하고 이를 Document Object Model (DOM)으로 변환합니다. 혹시 에러가 있는 HTML 파일을 브라우저에서 실행했을 때 항상 잘 실행되었다는 것을 알고 계셨나요? 예를 들어, </p> 태그로 닫지 않은 부분이 있는 파일도 정상 실행됩니다. 그 이유는 HTML 표준에 이런 에러들을 적절히 처리하는 내용이 있기 때문입니다. 이와 더불어, CSS도 파싱되며 각 DOM 노드에 대한 스타일 정보가 얻어집니다. 이러한 정보는 개발자 도구의 Computed 섹션에서 볼 수 있습니다.

이제 렌더러 프로세스는 문서의 구조와 각 노드에 대한 스타일을 알게 되었습니다. 하지만, 아직 페이지를 렌더하기에 충분하지 않아 요소들의 기하학적인 구조를 찾는 레이아웃 단계가 진행됩니다. 메인 스레드는 DOM 트리를 기반으로 각 노드의 2차원 좌표 및 요소의 박스 모델 크기와 같은 정보를 가지는 레이아웃 트리를 생성합니다. 레이아웃 트리는 DOM 트리와 유사하지만 페이지에 보이는 정보만을 담고 있습니다. 만약 어떤 요소에 display: none이 적용되면, 해당 요소는 레이아웃 트리에 포함되지 않습니다. 하지만, visibility: hidden이 명시된 요소는 레이아웃 트리에 있습니다. 또 다른 경우, p::before{content:"Hi!"}와 같이 가상 요소(pseudo element)가 있는 컨텐츠는 DOM 트리에는 없지만 레이아웃 트리에는 포함됩니다.

그 다음, 페인트 단계에서 메인 스레드는 하나의 페이지를 여러 층의 레이어로 분리하고 그 레이어들을 어떠한 순서로 그릴지 결정합니다. 이 과정에서, 레이아웃 트리를 기반으로 레이어 트리가 생성됩니다. 이 과정은 다른 요소와 겹치는 요소가 있는 경우에 실수로 한 요소가 다른 요소 위에 잘못 나타날 수 있기 때문에 중요합니다. 이렇게 레이어 트리가 생성되면 래스터 스레드가 텍스트, 색, 이미지, 경계 및 그림자 등 요소의 모든 시각적 부분을 그리기 위해 각 레이어의 픽셀을 채우게 됩니다.

이제 마지막 과정입니다! 오래 기다린 컴포지터 스레드가 픽셀이 채워진 레이어를 순서대로 화면에 보여줍니다. 드디어 여러분이 브라우저 주소창에 URL을 입력하고 엔터를 누른 결과 화면이 보이게 됩니다! 여기서 렌더러 프로세스는 본인이 맡은 바 소임을 다하였다는 메시지를 브라우저 프로세스에게 보냅니다. 그리고 이 때까지 돌던 브라우저 탭의 파비콘(favbicon) 로딩 애니메이션도 멈추게 됩니다.

지금까지 설명 드린 내용은 단순한 브라우저 동작 과정이었습니다. 일반적인 웹 페이지들은 이보다 더 복잡한 방법으로 동작하고 자바스크립트를 통하여 동적으로 변하는 경우가 많습니다. 특히 자바스크립트로 페이지 내 요소들의 스타일을 변경하거나 애니메이션을 적용하는 경우, 브라우저 렌더링 파이프라인 과정을 처리하기 위해 렌더러 프로세스의 메인 스레드가 바빠지는데 이것이 브라우저 성능에 큰 영향을 미칩니다. 따라서 브라우저 성능 개선을 하고 싶은 경우, 구현된 코드의 어떤 부분이 어떻게 렌더링 파이프라인에 접근하는지 알면 그 해결 방법을 찾기 수월합니다. 브라우저 성능 최적화를 위한 다양한 방법들이 연구되고 있습니다.

* 본 주제에 대한 상세 내용은 구글에서 4부작으로 작성한 포스트 (#1, #2, #3, #4)를 참고해주세요.

--

--