브라우저마다 다른 아키텍처로 이루어져 있지만 공통화시킬 수 잇는 부분들을 합쳐 간략하게 살펴보자.
모던 브라우저는 대부분 각각의 탭을 독립적인 프로세스로 처리하여 별도의 렌더링 인스턴스를 유지( process per tab ) 한다.
프로세스를 탭마다 분할함으로써 운영체제가 특정 기능을 제한하여 보안성을 높일 수 있다.
또한 한 탭의 렌더링 프로세스가 응답하지 못할 경우 다른 탭에 영향을 주지 않아 더 좋은 사용성을 제공할 수 있다.
사용자가 URL을 입력할 경우 브라우저 프로세스에서 제일 먼저 작업이 시작된다.
브라우저 프로세스는 브라우저의 UI를 그리는 UI 스레드, 네트워크 통신을 위한 네트워크 스레드, 파일에 접근하기 위한 스토리지 스레드 등이 존재한다.
최근에 등장한 브라우저는 대부분 주소창을 자사의 검색창과 동일하게 사용하고 있다.
검색창에 입력이 이뤄질 경우 UI 스레드는 해당 텍스트가 검색어인지 URL인지 확인한다.
검색어일 경우 검색 엔진 URL에 검색어를 결합해 페이지를 이동시키며 URL일 경우 네트워크 호출을 수행한다.
이때부터 브라우저에는 로딩이 되고 있다는 표시가 나타나게 된다.
동시에 네트워크 스레드는 적절한 프로토콜로 요청을 처리하기 시작한다.
네트워크 스레드는 응답이 도착하기 시작하면 응답이 어떤 타입인지 판단한다.
렌더러 프로세스가 다룰 수 있는 HTML 이나 PDF 같은 경우 렌더러 프로세스로 전달하며, 다운로드 파일일 경우 다운로드 매니저에게 데이터를 전달해 준다.
이 단계에서 해당 사이트가 악성 페이지인지 검사 또한 이뤄지며 악성 페이지일 경우 더 이상 처리하지 않고 작업을 중단한다.
모든 검사가 끝나면 네트워크 스레드는 UI 스레드에게 작업이 준비되었다고 알려준다.
UI 스레드는 웹 페이지를 렌더링한 렌더러 프로세스를 찾는다.
네트워크 요청이 오래 걸릴 수 있으므로 네트워크 스레드가 URL 요청을 보낼 때 UI 스레드는 렌더링을 수행할 프로세스를 미리 찾아 바로 사용하는 식으로 최적화가 되어 있기도 한다.
데이터와 렌더러 프로세스가 준비되면 본격적으로 페이지의 이동이 시작된다.
이 시점에 렌더러 프로세스는 HTML 데이터를 수신하여 문서를 로딩하며, 브라우저의 주소 표시줄, 사이트 제목 같은 관련 UI들도 갱신된다.
이후에 탭을 이동할 수 있도록 세션 기록이 저장된다.
렌더링이 완료되면 렌더러 프로레스는 브라우저 프로세스로 로딩이 완료되었음을 알리고 UI 스레드의 로딩 표시를 중지한다.
렌더러 프로세스에서 일어나는 일을 더 구체적으로 살펴보자.
렌더링 프로세스 작업은 다음과 같은 단계로 실행되며, 모든 HTML을 파싱할 때까지 기다리지 않고 레이아웃과 페인트를 우선적으로 처리해 내용의 일부를 나타내는 방식으로 수행된다.
HTML 파싱 ➡️ DOM Tree
CSS 파싱 ➡️ CSSOM Tree
렌더 트리 생성
레이아웃
페인트
합성
파싱은 일련의 문자열을 의미 있는 토큰으로 분해하고 토큰 간의 위계 관계를 분석해 구조를 결정하는 것이다.
파싱 결과는 주로 트리 형태를 나타내는데 이 트리는 parse 트리, parsing 트리, concrete syntax 트리 등으로 불린다.
변환 : 문서를 가져와서 지정된 인코딩 방식으로 읽는다.
토큰화 : 표준에 맞춰 지정된 태그들을 토큰화한다.
렉싱 ( lexical analysis ) : 토큰을 해당 속성 및 규칙을 정의하는 노드로 만든다.
트리 생성 : root 노드를 기준으로 생성된 노드들의 계층을 연결한다.
웹 사이트는 일반적으로 이미지나 CSS, js 같은 외부 리소스를 필요로 한다.
이런 파일들은 네트워크 혹은 캐시되어 있는 데이터를 통해 가져온다.
DOM 트리를 파싱하는 과정에서 데이터를 가져올 수도 있지만 모던 브라우저에서는 속도를 높이기 위해 <img>
나 <link>
같은 태그가 있다면 미리 브라우저 프로세스의 네트워크 스레드로 요청을 보낸다.
<script>
태그의 경우는 다르다.
자바스크립트에는 문서 전체의 구조를 바꿀 수 있는 메서드나 프로퍼티가 존재한다.
따라서 문서 중간에 자바스크립트 코드가 있는 경우 이를 로딩하고 파싱하기 위해 브라우저는 HTML파싱을 중단한다.
그리고 자바스크립트 파싱이 끝난 후 HTML 파싱이 재개된다.
만약 스크립트 파일에서 문서 조작이 이뤄지지 않는다면 async나 defer 속성을 지정하여 HTML 파싱을 중단하지 않고 자바스크립트 코드를 비동기적으로 로딩하고 실행할 수 있다.
DOMContentLoaded 이벤트는 모든 HTML 문서를 읽고 DOM 트리가 생성이 끝나는 즉시 실행되는 이벤트이다.
특이하게도, defer 속성을 갖는<script>
는 트리 생성이 끝나고 DOMContentLoaded 이벤트가 실행되기 전에 실행된다.
load 이벤트는 리소스와 그것에 의존하는 리소스들의 로딩이 완료되면 실행된다.
[그림 출처] : https://ko.javascript.info/script-async-defer
HTML 파싱 과정에서 <link>
태그를 만나면 해당 CSS 리소스를 가져온다.
HTML과 비슷하게 CSS 역시 토큰화, 노드 생성을 거쳐 CSSOM이라는 트리 구조를 생성한다.
CSSOM은 자바스크립트에서 CSS를 조작할 수 있게 해주는 API 집합이다.
DOM이 HTML을 조작할 수 있게 도와준다면, CSSOM은 CSS를 조작할 수 있게 도와준다.
CSSOM 트리의 노드는 DOM 엘리먼트의 선택자에 맞춰 적용될 CSS 스타일 정보가 포함되어 있다.
<link>
나 <script>
, <title>
, <meta>
와 같이 화면에 나타나지 않는 엘리먼트들에 대한 DOM 요소는 포함되지 않는다.
렌더 트리는 DOM 트리와 CSSOM 트리가 결합되어 생성된다.
Gecko 엔진의 경우 렌더 트리를 frame tree라 부르며 각 노드를 frame이라 부른다.
Webkit 엔진은 DOM 트리와 시각 정보를 결합하는 과정을 attachment라고 부른다.
각 노드는 렌더 객체 ( Render Object ) 로 이루어져 있으며 렌더 객체는 보이는 노드만을 포함한다.
렌더 트리의 생성 과정은 아래와 같이 요약할 수 있다.
<html>
태그와 <body>
태그를 처리하며 렌더 트리 루트를 구성한다.
DOM 트리를 순회하며 최상위 노드( <html>
)부터 보여지지 않는 노드를 생략한다.
display: none 처럼 CSS로 숨겨지는 노드 또한 트리에서 생략한다.
"숨겨지는"의 의미는 화면에서 해당 노드의 영역을 갖는지를 의미한다.
float나 position 같은 속성을 사용했을 경우 흐름에서 벗어나 실제 그려지는 위치로 렌더 객체가 이동한다.
화면에 나타나는 노드에 CSSOM 규칙을 찾아 일치하는 스타일을 적용한다.
렌더 트리의 모든 노드들은 자식 노드들의 레이아웃을 호출하는 layout ( Gecko 엔진에서는 reflow 라고 한다 )을 갖는다.
레이아웃은 렌더 트리의 최상위 노드인 <html>
부터 시작하며 이후 자식 렌더 객체들의 레이아웃을 배치며 반복적으로 발생한다.
초기 레이아웃 이후 DOM 노드가 추가 및 변경되었을 경우, 변경된 부분을 계산하기 위해 전체 노드를 대상으로 레이아웃이 발생한다면 너무 많은 낭비가 발생할 것이다.
레이아웃은 일부의 변경으로 인해 전체를 다시 배치하지 않기 위해 더티 비트 ( Dirty Bit )방식을 사용한다.
font-family나 font-size와 같은 전역 스타일이 변경되거나 창이 리사이즈 되었을 땐 글로벌 레이아웃이 일어나지만 일부만 변경이 이뤄지는 로컬 레이아웃은 더티 렌더 객체만을 대상으로 한다.
레이아웃의 과정은 다음과 같다.
부모 노드가 자식 노드의 너비를 결정한다.
너비는 블록의 너비나 렌더 객체의 스타일 속성, 박스 모델을 고려해 계산된다.
자식 렌더링 객체를 배치한다.
layout()
메서드를 호출해 높이를 계산한다.자식 렌더링 객체의 높이를 더해 부모 렌더 객체의 높이를 계산한다.
레이아웃 중인 렌더 객체의 "더티" 플래그를 제거한다.
레이아웃은 부하가 매우 큰 작업이기에 불필요한 레이아웃을 발생시키지 않는 것이 중요하다.
엘리먼트들의 크기, 위치는 알지만 같은 위치에 그려지는 여러 노드가 있다면 어떤 순서로 그려야 할까?
z-index나 float, position 같은 CSS 프로퍼티를 이용해 레이어의 순서를 변경한다면 렌더 트리의 순서가 레이어의 순서와 일치하지 않을 것이다.
페인트 단계에서는 렌더 트리를 순회하며 레이어를 만들고 레이어의 배경, 테두리, 텍스트, 그려지는 순서, 레이어 간의 순서 등 그려지는 과정을 기록한다.
그려질 엘리먼트들의 순서, 스타일, 위치 등을 모두 알고 있다.
이 정보를 화면에 픽셀로 변환하는 것을 래스터화 ( rasterizing ) 라고 한다.
합성은 각 레이어를 분리해서 레스터화한 뒤 브라우저에서 페이지의 크기, 뷰포트에 맞게 합성해 화면으로 나타낸다.
모던 브라우저의 경우 레이어를 래스터화한 뒤 조각화한 타일로 만들어 저장한다.
그리고 이후 뷰포트가 변경되었을 때 타일을 변경해가며 빠르게 보여준다.
지금까지 살펴본 HTML, CSS, 자바스크립트를 화면의 픽셀로 나타내기 위한 이런 과정을 주요 렌더링 경로 ( Critical Rendering Path, CRP )라고 부른다.
[참고] : 기초부터 완성까지, 프런트 엔드