Browser Anatomy - Rendering Engine
렌더링 엔진의 관점에서, 그러니까 우리가 보는 화면에 HTML, CSS, JavaScript 코드가 시각화되는 관점에서 브라우저의 동작을 살펴보도록하자. 렌더링 엔진이 작동하는 방식은 대략 다음과 같이 도식화할 수 있다.
첨부된 이미지는 여러 렌더링 엔진들 가운데서 WebKit의 도식을 보여주고 있는데, WebKit이 아니라 다른 렌더링도 용어의 차이는 있으나 큰 틀의 작동방식은 동일하다. 각각의 단계가 무엇을 의미하는지 하나하나 살펴보도록 하자.
Construction
이 때 구축(construction)이란 [Figure 1]에 나오는 DOM(Document Object Model) Tree, CSSOM(CSS Object Model) Tree, Render Tree와 같은 자료구조를 구축하는 것을 의미한다. 구축은
- DOM Tree Construction
- CSSOM Tree Construction
- Render Tree Construction
세 부분으로 나눠볼 수 있다.
DOM Tree
DOM Tree의 인터페이스를 사용해 JavaScript 등을 이용해 외부에서 HTML을 다룰 수 있게된다.
Mechanics
HTML 파일을 파싱해서 DOM Tree로 만드는 과정은 다음과 같다:
- 바이트(byte) → 문자 코드(character) 변환:
브라우저는 디스크 또는 네트워크 요청에 대한 응답으로부터 받은 HTML 파일의 바이너리 값을 파일에 지정된 인코딩 방식에 따라 문자 코드로 변환한다. - 문자 코드 -> 토큰 변환 (Tokenization):
구문 분석을 통해 문자 코드들로부터 여는 태그(<td>
), 닫는 태그(</td>
), 속성 이름(attribute name; e.g.colspan
) 및 속성 값(attribute value; e.g.2
) 등 HTML 문법에서 유의미한 요소들을 구별해낸다. 이 때 각 요소들을 토큰이라고 부른다. - 토큰 → 객체 구문 변환 (Lexing):
속성 이름 - 속성 값(attribute name - attribute value)으로 이루어진 HTML 토큰을 키 - 값(key - value)으로 이루어진 JavaScript 객체로 변환한다. - DOM Tree 구축 (Construction):
문서 구조에 따라 HTML 토큰들을 트리 구조로 위계화한다. 최종적으로 DOM Tree(Output Tree, Parser Tree)가 만들어진다.
문서 최상단에 선언한 HTML DTD(🔗 Document Type Definition; e.g. HTML5의 경우 <!DOCTYPE html>
)이 파싱 구문 및 규칙의 기준이 된다. HTML DTD를 생략한 HTML 파일의 경우 웹 브라우저마다 HTML을 파싱하는 방식이 조금씩 달라진다.
💡 HTML document parsing 도중에 CSS, JavaScript를 만났을 때
<script src="abc.js" />
: HTML document를 위에서 아래로 읽어 내려가면서 parsing하다가 JavaScript 파일을 포함한<script>
태그를 만나면 (<script deferred>
처럼 별도의 플래그가 붙지 않은 경우) HTML parsing은 잠시 중단되고 abc.js 파일을 다운로드 및 실행시킨다.<link href="def.css" ... />
,<style>
: 그러나 외부 CSS 파일은 HTML parsing을 중단시키지 않는다. 일반적으로 CSS 자체는 DOM의 구조를 변경시킬 수 없기 때문이다. 또한 CSS의 스타일 규칙은 뒤늦게 업데이트되더라도 다시 화면을 그리기만 하면(repaint) 된다. HTML parsing을 하다가 외부 CSS 파일을 포함한<link>
태그를 만나면 CSS 파일에 대해 다운로드 요청만 해놓고 계속 DOM Tree를 만든다. 그러다가 파일이 다운로드 되면 CSSOM 트리를 만들고 다시 HTML parsing이 중단된 지점으로 돌아와 HTML parsing을 재개한다.
이렇듯 JavaScript 파일과 CSS 파일을 처리하는 순서가 각각 다르기때문에 문서 내 <script>
태그와 <link>
태그의 위치도 달라진다. HTML은 문서 내에 시각적으로 보이는 모든 요소를 <body>
태그 안에 넣고 문서 자체에 대한 메타데이터, 즉 문서에 적용될 스타일 스크립트, 문서 탐색에 필요한 데이터들을 <head>
태그 내에 위치시킨다. 따라서 일반적으로 CSS 파일의 링크를 담은 <link>
태그를 <head>
태그의 맨 아래에 위치시킨다. <script>
태그 역시 <head>
태그 안에 넣을 수 있지만 앞서 말했듯 <script>
태그는 HTML parsing을 블록시키므로 이를 방지하기 위해 <body>
태그의 맨 아래에 위치시킨다. DOM 노드가 모두 로드한 후에 스크립트를 다운로드하고 실행시키기 위함이다. 이를 통해 사용자에게 먼저 정적인 시각적 요소를 제공하고 이를 제어하는 정적인 스크립트는 나중에 불러옴으로써 사용자 경험을 보다 향상시키기위함이다.
💡 Parsing이 끝난 브라우저는
- 클릭, 스크롤과 같은 사용자 인터랙션에 대해 반응이 가능해진다
deferred
플래그가 붙은<script>
들을 파싱하기 시작한다.load
이벤트를 실행시킨다
CSSOM Tree
Mechanics
우리가 작성한 CSS 스타일을 브라우저가 알아들을 수 있는 형태로 만들려면 HTML과 마찬가지로 CSS 구문을 파싱해서 CSSOM Tree로 만들어야한다.
- 바이트(byte) → 문자 코드(character) 변환:
- 문자 코드 -> 토큰 변환 (Tokenization):
- 토큰 → 객체 구문 변환(Lexing):
- CSSOM Tree 구축(Construction):
CSS 파싱은 CSS 명세(🔗 CSS specification)에 따라 이루어진다.
Style Computation
CSSOM을 트리 구조로 만들어 놓으면 HTML의 각 요소에 적용할 스타일을 계산할 때 유리하다. 위의 예시처럼 우리가 <p>
에 p { font-weight: bold; }
스타일을 지정해줬다고 하자. 우리는 <p>
에 대해서 font-weight
속성만 지정해줬지만 브라우저는 <p>
에 적용할 최종 스타일을 계산할 때 <p>
의 부모 Element들을 모두 찾아간다. 그 결과 최상위 Element인 <body>
의 스타일({ font-size: 16px }
)이 <body>
의 자식인 <p>
에도 적용된다. <p>
가 가지는 고유의 스타일 { font-weight: bold }
은 <body>
가 물려주는 스타일({ font-size: 16px }
)과 상충되지 않으므로 최종적으로 <p>
에 적용될 스타일은 둘을 합친 { font-size: 16px; font-weight: bold }
이 된다. 이렇듯 CSS의 스타일 규칙은 위에서부터 아래로 폭포수처럼 흘러 내려오고(cascade) 또 이를 덮어쓰는 방식으로 계산되는데, CSSOM을 트리로 만들어 놓으면 이러한 위계관계를 보다 쉽게 파악할 수 있다.
Render Tree
DOM의 내용물이 CSSOM의 스타일과 결합되어야 우리가 사용하는 내용도 풍부하고 UI도 유려한 웹사이트가 만들어진다. DOM Tree와 CSSOM Tree의 결합으로 만들어지는 새로운 Tree를 Render Tree라고 부른다.
Attachment
DOM Tree의 DOM Node와 Style Rules의 정보들을 결합하는 과정을 attachment라 부른다.
Render Object
DOM의 내용물과 CSSOM의 스타일이 합쳐진 객체를 renderer, render object(WebKit), 또는 frame(Firefox) 등의 이름으로 부른다. Renderer들이 모여서 Render Tree를 구성한다.
WebKit render object의 인터페이스를 살펴보면 다음과 같다:
class RenderObject{
virtual void layout(); virtual
void paint(PaintInfo); virtual
void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}
node
: 대응하는 DOM 노드에 대한 정보를 담고있다style
: 계산된 스타일 규칙들을 담고있다containingLayer
layout()
: 레이아웃 단계에서 실행된다paint()
: Painting 단계에서 실행된다repaintRect()
Mechanics
- DOM Tree를 순회해서 실제로 화면에 보여줄 노드를 찾는다.
💡 DOM Tree - Render Tree 대응 관계
i. 1:1 대응
ii. 1:0 대응: Render Tree에 Render Object를 추가하지 않는다
-<head>
,<meta>
처럼 실제로 눈에 보이는 태그가 아닌 경우
-{ display: none }
속성이 적용된 태그
iii. 1:N 대응
- DOM Node의 구조가 복합적인 경우 (ex.<select>
)
- multi-line: 텍스트가 줄바꿈을 하는 줄 개수만큼 Render Tree에 Render Object가 여러개 추가된다.
- HTML 구문 오류: CSS 명세에 따르면 inline box type은 같은 inline box type 또는 block box type만을 포함해야한다. 그런데 다른 box type이 섞인 경우 인라인 박스를 감싸기 위한 익명의 Render Object가 Render Tree에 추가된다. - 실제로 화면에 보여지는 DOM 노드인 경우 CSSOM Tree에서 해당 노드에 적용될 스타일 규칙을 가진 노드를 찾아서 스타일을 적용한다.
- Render Tree 구축 (Construction):
문서 구조에 따라 Render Object들을 트리 구조로 위계화한다. 최종적으로 DOM Tree(Output Tree, Parser Tree)가 만들어진다. Render Tree의 루트 노드는 자식 노드들인 블록(block)들을 모두 포함한다는 의미에서 컨테이너 블록(containing block)이라 부른다. 화면 내 시각적 요소들을 모두 포함하는 컨테이너 블록은 브라우저 창이 보여주고있는 범위인 Viewport에 해당한다(Webkit의 RenderView 객체, Firefox의 ViewportFrame에 해당한다). 이 루트 노드 아래에 다른 자식 노드를 추가해가면서 Render Tree가 구축된다.
Operation
operation은
- Layout (Reflow)
- Painting (Repainting)
- Composition
으로 나눠볼 수 있다.
Layout (Reflow)
Render Tree를 이루는 Render Object들은 화면에 그려져야하는 순서대로 구조화되어있지만 자신이 정확히 화면 내 어디에 위치하게될지를 알지 못한다. Render Object들의
- 화면 내 위치와
- 크기
를 결정하는 것은 Layout 단계에 속한다.
Mechanics
- Render Tree의 루트 노드부터 순회
- 노드의 정확한 크기와 위치를 계산한다:
크기: 크기가 % 등 상대적인 값으로 선언된 크기가 있으면 모두 절대적인 px 값으로 바꾼다.
위치: 루트 노드의 맨 왼쪽 최상단을 (0,0)로 설정한다 - 2의 과정을 재귀적으로 진행한다:
1) 부모가 자기 자신의 너비를 판단한다
2) 자식 노드에게 가서
2 - 1) 자식 노드의 좌표값을 판단한다
2 - 2) 필요시 자식 노드의layout()
메소드를 호출해서 자식 노드의 높이를 판단한다
3) 부모는 자식 노드의 높이, margin, padding을 합쳐서 자기 자신의 높이를 판단한다 - CSS Box Model 산출: 레이아웃의 결과물로 HTML Element를 구체적인 크기와 위치를 가진 박스로 변환한 Box Model을 산출한다.
CSS Flow Layout
레이아웃은 CSS Flow Model에 기초해 이루어진다.
🚧 Coming Soon...
Triggers
Layout 이벤트는 Render Tree가 구축된 시점, 그리고 크기와 위치가 다시금 계산될 때에 발생한다.
Global Layout
Render Tree 전체에 대해 레이아웃 작업이 이루어는 경우를 global layout이라 부른다. 다음과 같은 경우에 발생할 수 있다:
font-size
와 같이 모든 render object에 영향을 끼치는 속성이 바뀌는 경우- 브라우저 창 크기가 줄어들거나 늘어나는 경우
global layout은 동기적(synchronous)으로 이루어진다.
Incremental Layout
바뀐 부분만 레이아웃을 재조정하는 것을 incremental layout이라 부른다.
incremental layout은 비동기적으로 이루어진다. 다만 JavaScript 코드가 offsetHeight
와 같은 스타일 정보에 접근하고자하는 경우에는 incremental layout도 동기적으로 이루어질 수 있다.
Painting (=Repaint)
레이아웃이 완료되면 브라우저는 Paint Setup
과 Paint
이벤트를 전파하고 Painting 단계가 시작된다. Painting 단계에서는 최종 Render Tree를 기반으로 색상, 투명도 등 위치와 관계 없는 CSS 속성들을 적용하고 또한 화면에 실제 픽셀을 그린다. 이 때 픽셀로 변환된 결과물은 포토샵의 레이어처럼 서로 다른 레이어들로 관리된다. 단, 모든 Element가 독자적인 레이어가 되는 것은 아니다. 다만 transform
등의 속성을 적용한 Element는 별도의 레이어를 형성한다.
Mechanics
- 구축이 완료된 Render Tree를 순회한다
- 각각 노드의
paint()
메소드를 실행시킨다. UI Backend Layer를 사용해 각각 노드를 화면에 그린다.
Triggers
Painting도 Layout과 마찬가지로 Global Painting과 Incremental Painting으로 나뉜다.
Global Painting
Render Tree 전체를 화면에 (다시) 그린다
Incremental Painting
renderer 하나에 변화가 발생해도 Render Tree 내 다른 renderer들에 영향을 주지 않는 경우에 Incremental Painting이 일어난다
Composition
합성(composition) 단계에서 생성된 레이어를 합성한다. CSS2 명세는 render object들이 그려지는 순서를 지정하고 있다. Element들은 stacking context에 쌓이는 순서대로 화면의 뒤쪽에서부터 앞쪽으로 그려지는데, 이 때 z-index가 높은 순에서 높은 순으로 쌓인다. stacking context에 레이어가 쌓이는 순서는 다음과 같다:
1. background color
2. background image
3. border
4. children
5. outline
Rendering
위의 모든 과정들을 마치고 비로소 사용자가 화면에서 웹 페이지를 볼 수 있는 상태가 된다.