critical rendering path란?
“어떠한 과정을 통해 사용자에게 웹페이지가 보여지게 되는 것일까?”
웹개발을 하면서 위 질문에 대해 깊게 생각해보지 않았던 것 같다. 개발을 하다보면, 내가 개발한 화면을 다른 유저가 보게 되는 일이 많은데, 해당 화면이 어떠한 과정을 통해 사람들에게 보여지는지에 대해서는 별 다른 의문을 가져본 적이 없는 것 같다. 그래서 내가 만든 화면이 어떻게 사용자에게 보여지는 것인지에 알고 싶어 공부하고 정리해보았다.
🎯 인터넷 브라우저의 구조
설명에 앞서, 먼저 인터넷 브라우저의 구조에 대해 살펴보자.
인터넷 브라우저란 웹 서버와 통신하여 인터넷 사이트 및 다양한 컨텐츠를 볼 수 있도록 지원해주는 소프트웨어 프로그램으로, Chrome을 포함하여 Safari, Firefox, 오페라 등 여러 종류가 있다. 각 브라우저마다 전반적인 구조는 조금씩 다르지만 큰 틀은 아래의 그림으로 동일하다.
위 구조에 대해 설명하면 아래와 같다
✅ 사용자 인터페이스 (User Interface / UI)
위 첨부한 이미지와 같이, 페이지를 보여주는 창 위에 주소 표시줄, 이전, 이후 버튼, 홈버튼, 북마크 버튼 등이 존재하는 영역을 말한다.
✅ 브라우저 엔진
사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어하는 부분이다. 예를 들어 이전 버튼을 눌렀을때, 이전 화면을 보여주게 될텐데 이전 버튼이 있는 부분은 사용자 인터페이스, 이전 화면을 보여주는 부분은 렌더링 엔진, 그리고 그 사이의 동작을 제어하는 곳은 브라우저 엔진이라고 이해할 수 있다.
✅ 렌더링 엔진
브라우저 엔진과 밀접히 관련된 엔진으로, 웹 페이지가 표시되는 모든 영역을 제어하는 역할을 한다. 렌더링 엔진은 요청한 콘텐츠(HTML, CSS 등)를 파싱하고, 화면에 나타내는 일을 수행한다.
✅ 자료 저장소 / 데이터 저장소
localStorage, sessionStorage, 쿠키와 같이 로컬에 저장되어 좀 더 오래 유지되어야 하는 데이터들을 보관할 수 있도록 지원하는 영역이다.
✅ 통신 (Networking)
플랫폼과 독립적인 인터페이스로, 각 플랫폼의 하부에서 실행되며, HTTP/HTTPS 네트워크 처리를 한다.
✅ 자바스크립트 해석기 (JS Interpreter)
스크립트(JS 코드)를 파싱할 때 사용하는 JS 엔진으로, HTML을 파싱 중 script 태그를 만나면, JS 엔진이 제어 권한을 넘겨받는다. 즉 DOM트리를 만들다, script 태그를 만나면 DOM트리를 만드는 과정을 잠시 중단하고, JS 엔진이 작업을 마칠때 까지 기다린다.
✅ UI 백엔드 (UI Backend)
기본 위젯을 그릴 때 이용하는 부분으로 OS의 방법을 사용한다.
🎯 주소창에 www.google.com을 치면 일어나는 일
그렇다면 본격적으로 주소창에 www.google.com을 검색했을 때 어떤일이 일어나는 지 알아보자!
주소창에 www.google.com을 검색했을때는 아래와 같은 과정이 일어나게 된다.
google.com -> 입력한 텍스트 정보 확인 -> 네트워크 호출 -> 렌더링 작업 -> google
1️⃣ 입력한 텍스트 정보 확인
브라우저는 사용자가 주소창에 어떤 텍스트를 입력했을 때, 입력한 텍스트가 검색어인지 URL인지 확인한다.
만일 입력한 텍스트가 검색어이면, 브라우저는 검색 엔진의 URL에 검색어를 포함한 주소로 페이지를 이동시킨다.
만일 입력한 텍스트가 URL이면, 브라우저 엔진에서 네트워크 호출을 수행한다.
따라서 www.google.com 라는 텍스트를 입력하고 엔터를 치면, 해당 값은 URL이므로, 네트워크 호출을 수행하게 된다.
2️⃣ 네트워크 호출
다음은 네트워크 호출이다.
사용자에게 구글 사이트를 화면에 보여주려면 브라우저는 구글의 HTML, CSS, script, 이미지 등의 데이터를 가지고 있어야 한다. 하지만 해당 데이터들은 구글 서버 컴퓨터에 존재한다.
따라서, 브라우저는 구글 서버와의 네트워크 통신을 통해 이러한 데이터들을 가져와야 해당 과정은 아래의 두 과정으로 설명할 수 있다.
1️⃣ 구글 서버의 주소를 알기 위해 **도메인 네임 서버(Domain Name Server)**와 통신
2️⃣ DNS를 통해 알아낸 주소를 바탕으로 구글 서버와 통신하여 필요한 데이터를 수집
1️⃣ 구글 서버의 주소를 알기 위해 **도메인 네임 서버(Domain Name Server)**와 통신
클라이언트는 구글 서버의 주소를 알아내기 위해, 아래의 과정을 수행한다.
- host 파일에서 도메인 네임에 대응하는 IP 주소가 있는지 확인한다.
- 만일 없다면, 도메인 네임 서버(Domain Name Server)에 ‘구글 IP 주소를 알려주세요’ 라는 요청을 보낸다.
여기서 도메인 네임이란 URL www.google.com에서 google.com 에 해당하는 부분이다.
인터넷은 컴퓨터의 주소인 IP 주소를 기반으로 동작한다. 하지만 우리가 인터넷을 사용할 땐, IP 주소 대신 사용하기 쉽도록 문자로 이루어진 도메인 네임을 사용한다.
따라서 도메인 네임을 IP 주소로 변환해 주는 환경인 DNS (Domain Name Server)가 반드시 필요한데, 이 DNS를 운영하는 장치를 DNS 서버라 한다.
즉, DNS 서버는 도메인 주소에 대응하는 IP 주소를 찾아주는 역할을 수행하는 것이다.
클라이언트는 일반적으로 DNS 서버의 IP 주소를 이미 가지고 있다. 따라서 클라이언트는 DNS 서버와 통신이 가능하고, google.com에 해당하는 IP 주소를 요청 및 응답 받을 수 있다.
2️⃣ DNS를 통해 알아낸 주소를 바탕으로 구글 서버와 통신하여 필요한 데이터 수집
-
이제 클라이언트는 구글 서버의 IP 주소를 알게 되어 구글 서버와 통신할 수 있게 되었다. 클라이언트의 브라우저는 구글 서버에 데이터를 요청하는 HTTP Request를 보낸다.
-
HTTP Request를 받은 구글 서버는 클라이언트가 요청한 문서를 찾아 읽고 이를 0과 1로 이루어진 바이트 형태 (바이트 스트림) 로 변환한 후, 클라이언트로 HTTP Response를 보낸다.
3️⃣ 렌더링 작업
위 과정에서 구글 서버와 통신하여 얻은 HTTP Response는 0과 1로 이루어진 바이트 형태로, 브라우저 엔진이 읽을 수 없다. 따라서 브라우저 엔진은 렌더링 엔진에게 해당 데이터를 해석하고, 웹 페이지를 화면에 띄울 것을 요청한다.
요청을 받은 렌더링 엔진은 받은 데이터를 바탕으로 렌더링 프로세스를 수행하고, 이 과정이 끝나면 브라우저 엔진에게 작업 완료를 알리고 최종적으로 화면에 구글 페이지가 보여지게 된다.
그렇다면 렌더링 프로세스는 어떠한 과정으로 이루어 지는 것일까?
🎯 렌더링 프로세스
렌더링 프로세스는 아래와 같은 과정으로 이루어진다.
-
HTML을 파싱하여 DOM 트리 구축, CSS를 파싱하여 CSSOM 트리 구축 (+ JS 파싱)
-
DOM 트리와 CSSOM 트리를 통한 랜더 트리 (Render Tree) 구축
-
랜더 트리 배치 (Layout)
-
랜더 트리 그리기 (Paint)
다음은 웹킷과 Blink의 렌더링 프로세스 과정과, 게코의 렌더링 프로세스 과정이다.
브라우저 별로 사용하는 렌더링 엔진이 다른데, 파이어폭스의 경우 모질라에서 직접 만든 게코(Gecko) 엔진을 사용하고, 사파리는 웹킷(Webkit) 엔진을 사용한다.
참고로 크롬은 웹킷(Webkit) 엔진을 사용했다가, 웹킷을 Fork하여 자체적으로 구현한 블링크(Blink) 엔진을 현재 사용하고 있다고 한다.
웹킷과 Blink의 렌더링 프로세스 과정
게코의 렌더링 프로세스 과정
자 그렇다면 위의 1번부터 4번의 렌더링 프로세스과정에 대해서 자세하게 알아보자!
👉🏻 HTML을 파싱하여 DOM 트리 구축, CSS를 파싱하여 CSSOM 트리 구축 (+ JS 파싱)
📌 HTML 파싱
HTML 파서는 HTML문서를 위에서부터 읽어 내려가며 파싱을 진행하고, 그 결과물로 DOM 트리를 생성한다. 자세한 과정은 아래와 같다.
-
변환 (Bytes -> Characters) 브라우저가 HTML의 원시 바이트를, 해당 파일에 대해 지정된 인코딩(예: UTF-8)에 따라 개별 문자로 변환한다.
-
토큰화 (Character -> Tokens) 브라우저가 문자열을 W3C HTML5 표준에 지정된 고유 토큰으로 변환한다 (예:
<html>
,<body>
및 꺽쇠괄호로 묶인 기타 문자열) -
렉싱 (Tokens -> Nodes) 변환된 토큰은 해당 속성 및 규칙을 정의하는 ‘객체’로 변환된다.
-
DOM 트리 생성 (Nodes -> DOM) 마지막으로, HTML 마크업이 여러 태그 간의 관계를 정의하기 때문에 생성된 객체는 트리 데이터 구조 내에 연결된다. 이 트리 데이터 구조에는 원래 마크업에 정의된 상위-하위 관계도 포함된다.
이때 렌더링 엔진은 사용자의 만족도를 높이기 위해 HTML 문서가 모두 파싱될 때까지 기다리지 않고 파싱 이후의 과정인 배치와 그리기를 미리 진행한다.
📌 CSS 파싱
CSS파싱도 HTML파싱과 비슷한 과정으로 진행된다.
HTML 파싱 중 CSS 문서를 가져오는 link 태그를 만나면, DOM 생성이 잠시 중단되고 해당 CSS의 파싱 과정이 시작된다. (DOM생성과정은 CSSOM이 만들어 지는 것을 기다린다. 따라서 CSS파일이 크다면 CSSOM트리를 만드는 과정이 오래걸리며, 이는 DOM트리 생성에도 영향을 끼치기 때문에, CSS파일의 크기를 줄이는 방법으로 시간을 줄일 수 있다.)
CSS도 HTML을 파싱하여 DOM트리를 과정을 만드는 과정과 동일하게 변환, 토큰화, 렉싱 과정을 거치고 CSSOM트리를 생성한다.
이렇게 만들어진 CSSOM 트리의 노드는 DOM 트리 요소의 선택자에 맞춰 적용될 CSS 스타일 정보가 포함되어 있다.
📌 JS 파싱
HTML 파싱 과정에서 script 태그를 만나면, 렌더링 엔진은 DOM 생성을 잠시 중지하고 서버에서 해당 JavaScript 리소스를 브라우저 엔진으로부터 받아온다. 그리고 JavaScript 엔진에게 제어권을 넘겨준다.
JavaScript 엔진은 받아온 JS 리소스를 파싱하여 AST (추상 구문 트리) 를 생성하고 이를 바이트코드로 변환해 실행한다.
JavaScript 파싱이 종료되면 렌더링 엔진은 다시 제어권을 돌려받고 DOM 생성을 이어나간다.
만일 script 태그를 body 태그의 중간에 작성할 경우, HTML 파싱이 끝나지 않은 상태에서 JavaScript로 인해 DOM이 조작되어 에러가 발생할 위험이 생긴다. 따라서 script 태그는 반드시 body 태그 내부의 최하단에 위치해야 한다.
👉🏻 DOM 트리와 CSSOM 트리를 통한 랜더 트리 (Render Tree) 생성
다음은 렌더 트리 생성이다.
DOM은 컨텐츠, CSSOM은 스타일 규칙을 설명하는 독립적인 객체이다. 각각의 객체를 가지고 화면에 픽셀을 찍기위해서는 두 객체를 합쳐 Render Tree 를 만들어야 한다.
따라서 앞선 과정의 HTML과 CSS의 파싱 과정에서 나온 결과물인 DOM 트리와 CSSSOM 트리를 서로 결합하여 **랜더 트리 (Render Tree)**를 생성한다.
렌더 트리 생성과정은 아래와 같다.
- DOM 트리의 루트에서 시작하여 표시되는 노드 각각을 순회한다.
이때,
스크립트 태그
,메타 태그
,display: none
속성등 화면에 보여지지 않는 노드는 렌더 트리의 구성에서 제외된다. - 표시된 각 노드에 대해 적절하게 일치하는 CSSOM 규칙을 찾아 적용한다.
- 표시된 노드를 콘텐츠 및 계산된 스타일과 함께 내보낸다.
👉🏻 랜더 트리 배치 (Layout)
Render Tree 에는 노드와 노드의 스타일만 계산되어 있다. 따라서 Layout 에서는 화면에 표시될 노드의 정확한 위치 및 크기를 계산하는 과정을 진행한다.
이때 노드의 위치는 (x, y) 좌표계를 사용하는데, 랜더 트리의 루트부터 아래로 내려가면서 계산을 진행하게 되며 %
, rem
, vh
등의 상대적인 값들이 절대적인 값인 px
로 변하게 된다.
👉🏻 랜더 트리 그리기 (Paint)
Layout 과정을 거쳐 화면에 UI를 화면에 표현하기 위한 계산이 끝나면 Paint 과정이 진행된다.
Layout 과정에서 Render Layer가 2개 이상 생성되면 각각의 Layer 를 Painting 한 뒤 하나의 이미지로 Composite하는 과정을 추가로 거쳐 브라우저에 표현한다.
자세한 과정에 대한 설명은 아래와 같다.
Paint / Rasterize Paint 는 Render Tree 를 화면의 픽셀로 변환하는 프로세스이다. 이는 텍스트, 색, 이미지, 경계 및 그림자 등 요소의 모든 시각적 부분을 그리는 작업을 포함한다. 이때, 픽셀로 변환하는 이 과정을 래스터화 (Rasterizing)라고 한다.
Composite Composite이란, 사용된 HTML이나 CSS 속성에 따라 여러개의 Layer 가 생성된 경우, 생성된 Layer 들을 합성하여 한장의 bitmap 으로 만드는 과정을 말한다. 이는 각 Layer 별로 paint 되기 때문에 불필요한 painting 을 줄여 효율 적으로 그릴 수 있다.
🎨 Reflow와 Repaint
여기서 끝난 것은 아니다.
브라우저에 특정 변경이 생긴다면 이를 화면에 다시 그려주어야 한다. 즉 특정 액션이나 이벤트에 따라 HTML요소의 크기나 위치 등의 레이아웃이 변하게 될 수 있으며 이 경우 Layout(Reflow) 또는 페인트 과정이 다시 일어나는 것을 리페인트 (Repaint)가 발생한다. (기본적으로 리플로우가 발생하면 리페인트도 함께 발생한다. 또한 화면의 구조가 변하지 않더라도 요소의 색깔이 변한다면 리페인트가 일어난다.)
1️⃣ Reflow
렌더 트리와 각 요소들의 크기와 위치를 다시 계산해주는 과정을 Reflow
라고 한다.
이러한 Reflow
는 아래와 같은 경우에 발생한다.
- DOM 노드를 추가하거나 제거하는 경우
- DOM 노드의 위치가 변경되는 경우
- DOM 노드의 크기가 변경되는 경우 (margin, padding, border 등)
- 폰트가 변경되는 경우 (font-weight, font-size)
- 페이지 초기 렌더링
- 윈도우 리사이징
2️⃣ Repaint
Reflow만 수행되며 실제 화면에는 반영되지 않기 때문에 다시 Painting이 일어나야 한다. 이 과정을 Repaint라고 한다.
화면의 구조가 변경되었을 때에는 Reflow 과정을 거쳐 화면 구조를 다시 계산한 후 Repaint 과정을 통해 화면을 다시 그린다. 즉 화면의 구조가 변경되었을 때에는 Reflow와 Repaint 모두 발생한다.
화면의 구조가 변경되지 않는 경우면 변화의 경우 Repaint만 발생한다. 예를 들면 opacity
, background-color
, visibility
, outline
등의 스타일 변경 시에는 Repaint만 동작한다.
이러한 Reflow와 Repaint의 경우 비용이 높은 연산이므로 이를 최소화하는 것을 통해 성능을 향상시킬 수 있다.
Reflow가 일어나는 대표적인 속성
position, width, height, left, top, right, bottom, margin, padding, border, border-width, clear, display, float, font-family, font-size, font-weight, line-height, min-height, overflow, text-align, vertical-align, white-space…
Repaint가 일어나는 대표적인 속성
background, background-image, background-position, background-repeat, background-size, border-radius, border-style, box-shadow, color, line-style, outline, outline-color, outline-style, outline-width, text-decoration, visibility…
🤔 마치며…
브라우저 렌더링 과정에 대해 조금이나마 알 수 있었던 시간이었다. 또한 해당 내용을 찾아보는 과정에서 CS의 중요성을 다시 한번 느끼게 되었다. 해당 내용을 공부하면서 모르는 부분을 채워가는 과정에서 어려움을 느끼기도 하였지만, 오히려 이러한 경험을 통해 성장하고 있는 나 자신의 모습을 발견할 수 있어서 뿌듯하기도 하였다. 아직 해당 내용에 대해서 깊게 안다고는 할 수 없겠지만 처음부터 모든것을 완벽하게 알 수 없듯, 해당 부분에 대해 계속해서 공부해 나가며 부족한 부분을 채워나가야겠다.