Brower 브라우저 구조
16/10/17/월
09:39
 
원본출처: <http://d2.naver.com/helloworld/59361>

글은 이스라엘 개발자 탈리 가르시엘(Tali Garsiel)이 html5rocks.com 게시한 "How Browsers Work: Behindthe scenes of modern web browsers"를 번역한 글입니다. 탈리 가르시엘은 년간 브라우저 내부와 관련된 공개 자료를 확인하고, C++ 코드 수백만 분량의 WebKit이나 Gecko 같은 오픈소스 렌더링 엔진의 소스 코드를 직접 분석하면서 어떻게 브라우저가 동작하는지 파악했습니다.


 
소개
브라우저는 아마도 가장 많이 사용하는 소프트웨어일 것이다. 이 글을 통해 브라우저가 어떻게 동작하는지 설명하려고 한다. 이 글을 읽고 나면, 브라우저 주소 창에 naver.com을 입력했을 어떤 과정을 거쳐 네이버 페이지가 화면에 보이게 되는지 알게 것이다.
 
 
글에서 설명하는 브라우저
최근에는 인터넷 익스플로러, 파이어폭스, 사파리, 크롬, 오페라 이렇게 다섯 개의 브라우저를 많이 사용하지만 나는 파이어폭스, 크롬, 사파리와 같은 오픈소스 브라우저를 예로 것이다. 사파리는 부분적으로 오픈소스이다. StatCounter 브라우저 통계 의하면 2012년 3월 현재 파이어폭스, 사파리, 크롬의 점유율은 62.57%에 달한다. 오픈소스 브라우저가 시장의 상당 부분을 차지하게 것이다.
 
브라우저의 주요 기능
브라우저의 주요 기능은 사용자가 선택한 자원을 서버에 요청하고 브라우저에 표시하는 것이다. 자원은 보통 HTML 문서지만 PDF나 이미지 또는 다른 형태일 있다. 자원의 주소는 URI(Uniform Resource Identifier)에 의해 정해진다.
브라우저는 HTML과 CSS 명세에 따라 HTML 파일을 해석해서 표시하는데 명세는 표준화 기구인 W3C(World Wide Web Consortium)에서 정한다. 과거에는 브라우저들이 일부만 명세에 따라 구현하고 독자적인 방법으로 확장함으로써 제작자가 심각한 호환성 문제를 겪었지만 최근에는 대부분의 브라우저가 표준 명세를 따른다.
브라우저의 사용자 인터페이스는 서로 닮아 있는데 다음과 같은 요소들이 일반적이다.

  • URI를 입력할 있는 주소 표시
  • 이전 버튼과 다음 버튼
  • 북마크
  • 새로 고침 버튼과 현재 문서의 로드를 중단할 있는 정지 버튼
  • 버튼

브라우저의 사용자 인터페이스는 표준 명세가 없음에도 불구하고 년간 서로의 장점을 모방하면서 현재에 이르게 되었다. HTML5 명세는 주소 표시줄, 상태 표시줄, 도구 모음과 같은 일반적인 요소를 제외하고 브라우저의 필수 UI를 정의하지 않았다. 물론 파이어폭스의 다운로드 관리자와 같이 브라우저에 특화된 기능도 있다.
 
 
브라우저의 기본 구조

  1. 사용자 인터페이스 - 주소 표시줄, 이전/다음 버튼, 북마크 메뉴 등. 요청한 페이지를 보여주는 창을 제외한 나머지 모든 부분이다.
  2. 브라우저 엔진 - 사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어.
  3. 렌더링 엔진 - 요청한 콘텐츠를 표시. 예를 들어 HTML을 요청하면 HTML과 CSS를 파싱하여 화면에 표시함.
  4. 통신 - HTTP 요청과 같은 네트워크 호출에 사용됨. 이것은 플랫폼 독립적인 인터페이스이고 플랫폼 하부에서 실행됨.
  5. UI 백엔드 - 콤보 박스와 같은 기본적인 장치를 그림. 플랫폼에서 명시하지 않은 일반적인 인터페이스로서, OS 사용자 인터페이스 체계를 사용.
  6. 자바스크립트 해석기 - 자바스크립트 코드를 해석하고 실행.
  7. 자료 저장소 - 이 부분은 자료를 저장하는 계층이다. 쿠키를 저장하는 것과 같이 모든 종류의 자원을 하드 디스크에 저장할 필요가 있다. HTML5 명세에는 브라우저가 지원하는 '웹 데이터 베이스'가 정의되어 있다.


그림 1 브라우저의 주요 구성 요소
 
 
렌더링 엔진
렌더링 엔진의 역할은 요청 받은 내용을 브라우저 화면에 표시하는 일이다.
렌더링 엔진은 HTML 및 XML 문서와 이미지를 표시할 있다. 물론 플러그인이나 브라우저 확장 기능을 이용해 PDF와 같은 다른 유형도 표시할 있다. 그러나 장에서는 HTML과 이미지를 CSS로 표시하는 주된 사용 패턴에 초점을 맞출 것이다.
 
 
렌더링 엔진들
글에서 다루는 브라우저인 파이어폭스와 크롬, 사파리는 종류의 렌더링 엔진으로 제작되었다. 파이어폭스는 모질라에서 직접 만든 게코(Gecko) 엔진을 사용하고 사파리와 크롬은 웹킷(Webkit) 엔진을 사용한다.
웹킷은 최초 리눅스 플랫폼에서 동작하기 위해 제작된 오픈소스 엔진인데 애플이 맥과 윈도우즈에서 사파리 브라우저를 지원하기 위해 수정을 가했다. 더 자세한 내용은 webkit.org 참조한다.
 
 
동작 과정
렌더링 엔진은 통신으로부터 요청한 문서의 내용을 얻는 것으로 시작하는데 문서의 내용은 보통 8KB 단위로 전송된다.
다음은 렌더링 엔진의 기본적인 동작 과정이다.

그림 2 렌더링 엔진의 동작 과정
렌더링 엔진은 HTML 문서를 파싱하고 "콘텐츠 트리" 내부에서 태그를 DOM 노드로 변환한다. 그 다음 외부 CSS 파일과 함께 포함된 스타일 요소도 파싱한다. 스타일 정보와 HTML 표시 규칙은 "렌더 트리"라고 부르는 다른 트리를 생성한다.
렌더 트리는 색상 또는 면적과 같은 시각적 속성이 있는 사각형을 포함하고 있는데 정해진 순서대로 화면에 표시된다.
렌더 트리 생성이 끝나면 배치가 시작되는데 이것은 노드가 화면의 정확한 위치에 표시되는 것을 의미한다. 다음은 UI 백엔드에서 렌더 트리의 노드를 가로지르며 형상을 만들어 내는 그리기 과정이다.
일련의 과정들이 점진적으로 진행된다는 것을 아는 것이 중요하다. 렌더링 엔진은 나은 사용자 경험을 위해 가능하면 빠르게 내용을 표시하는데 모든 HTML을 파싱할 때까지 기다리지 않고 배치와 그리기 과정을 시작한다. 네트워크로부터 나머지 내용이 전송되기를 기다리는 동시에 받은 내용의 일부를 먼저 화면에 표시하는 것이다.
 
 
동작 과정

그림 3 웹킷 동작 과정
 

그림 4 모질라의 게코 렌더링 엔진 동작 과정(3.6)
 
웹킷과 게코가 용어를 약간 다르게 사용하고 있지만 동작 과정은 기본적으로 동일하다는 것을 그림 3과 그림 4에서 있다.
게코는 시각적으로 처리되는 렌더 트리를 "형상 트리(frame tree)"라고 부르고 요소를 형상(frame)이라고 하는데 웹킷은 "렌더 객체(render object)"로 구성되어 있는 "렌더 트리(render tree)"라는 용어를 사용한다. 웹킷은 요소를 배치하는데 "배치(layout)" 라는 용어를 사용하지만 게코는 "리플로(reflow)" 라고 부른다. "어태치먼트(attachment)"는 웹킷이 렌더 트리를 생성하기 위해 DOM 노드와 시각 정보를 연결하는 과정이다. 게코는 HTML과 DOM 트리 사이에 "콘텐츠 싱크(content sink)"라고 부르는 과정을 두는데 이는 DOM 요소를 생성하는 공정으로 웹킷과 비교하여 의미있는 차이점이라고 보지는 않는다.
 
 
HTML 파서
HTML 파서는 HTML 마크업을 파싱 트리로 변환한다.
 
HTML 문법 정의
HTML의 어휘와 문법은 W3C에 의해 명세로 정의되어 있다. 현재 버전은 HTML4와 초안 상태로 진행 중인 HTML5 이다.
 
문맥 자유 문법(Context Free Grammar)이 아님
파싱 일반 소개를 통해 알게 것처럼 문법은 BNF와 같은 형식을 이용하여 공식적으로 정의할 있다. 
안타깝게도 모든 전통적인 파서는 HTML에 적용할 없다. 그럼에도 불구하여 지금까지 파싱을 설명한 것은 그냥 재미 때문은 아니다. 파싱은 CSS와 자바스크립트를 파싱하는 사용된다. HTML은 파서가 요구하는 문맥 자유 문법에 의해 쉽게 정의할 없다.
HTML 정의를 위한 공식적인 형식으로 DTD(문서 형식 정의)가 있지만 이것은 문맥 자유 문법이 아니다.
이것은 언뜻 이상하게 보일 수도 있는데 HTML이 XML과 유사하기 때문이다. 사용할 있는 XML 파서는 많다. HTML을 XML 형태로 재구성한 XHTML도 있는데 무엇이 차이점일까?
차이점은 HTML이 더 "너그럽다"는 점이다. HTML은 암묵적으로 태그에 대한 생략이 가능하다. 가끔 시작 또는 종료 태그 등을 생략한다. 전반적으로 뻣뻣하고 부담스러운 XML에 반하여 HTML은 "유연한" 문법이다.
이런 작은 차이가 차이를 만들어 낸다. 웹 제작자의 실수를 너그럽게 용서하고 편하게 만들어주는 이것이야 말로 HTML이 인기가 있었던 이유다. 다른 한편으로는 공식적인 문법으로 작성하기 어렵게 만드는 문제가 있다. 정리하자면 HTML은 파싱하기 어렵고 전통적인 구문 분석이 불가능하기 때문에 문맥 자유 문법이 아니라는 것이다. XML 파서로도 파싱하기 쉽지 않다.
 
HTML DTD
HTML의 정의는 DTD 형식 안에 있는데 SGML 계열 언어의 정의를 이용한 것이다. 이 형식은 허용되는 모든 요소와 그들의 속성 그리고 중첩 구조에 대한 정의를 포함한다. 앞서 한대로 HTML DTD는 문맥 자유 문법이 아니다. 
DTD는 여러 변종이 있다. 엄격한 형식은 명세만을 따르지만 다른 형식은 낡은 브라우저에서 사용된 마크업을 지원한다. 낡은 마크업을 지원하는 이유는 오래된 콘텐츠에 대한 하위 호환성 때문이다. 현재의 엄격한 형식 DTD는www.w3.org/TR/html4/strict.dtd 에서 확인할 있다.
 
DOM
"파싱 트리"는 DOM 요소와 속성 노드의 트리로서 출력 트리가 된다. DOM은 문서 객체 모델(Document Object Model)의 준말이다. 이것은 HTML 문서의 객체 표현이고 외부를 향하는 자바스크립트와 같은 HTML 요소의 연결 지점이다. 트리의 최상위 객체는 문서이다.
DOM은 마크업과 1:1의 관계를 맺는다. 예를 들면 이런 마크업이 있다.
<html>  <body>   <p>Hello World</p>   <div><img src="example.png" /></div>  </body></html>
 
이것은 아래와 같은 DOM 트리로 변환할 있다.

그림 8 예제 마크업의 DOM 트리
 
HTML과 마찬가지로 DOM은 W3C에 의해 명세(www.w3.org/DOM/DOMTR)가 정해져 있다. 이것은 문서를 다루기 위한 일반적인 명세인데 부분적으로 HTML 요소를 설명하기도 한다. HTML 정의는 www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html에서 찾을 있다.
트리가 DOM 노드를 포함한다고 말하는 것은 DOM 접점의 하나를 실행하는 요소를 구성한다는 의미다. 브라우저는 내부의 다른 속성들을 이용하여 이를 구체적으로 실행한다.
 
 
파싱 알고리즘
앞서 말한대로 HTML은 일반적인 하향식 또는 상향식 파서로 파싱이 안되는데 이유는 다음과 같다.

  1. 언어의 너그러운 속성.
  2. 알려져 있는 HTML 오류에 대한 브라우저의 관용.
  3. 변경에 의한 재파싱. 일반적으로 소스는 파싱하는 동안 변하지 않지만 HTML에서 document.write을 포함하고 있는 스크립트 태그는 토큰을 추가할 있기 때문에 실제로는 입력 과정에서 파싱이 수정된다.

 
일반적인 파싱 기술을 사용할 없기 때문에 브라우저는 HTML 파싱을 위해 별도의 파서를 생성한다.
파싱 알고리즘은 HTML 자세히 설명되어 있다. 알고리즘은 토큰화와 트리 구축 이렇게 단계로 되어 있다. 
토큰화는 어휘 분석으로서 입력 값을 토큰으로 파싱한다. HTML에서 토큰은 시작 태그, 종료 태그, 속성 이름과 속성 값이다.
토큰화는 토큰을 인지해서 트리 생성자로 넘기고 다름 토큰을 확인하기 위해 다음 문자를 확인한다. 그리고 입력의 마지막까지 과정을 반복한다.

그림 9 HTML 파싱 과정(HTML5 명세에서 가져옴)
 
 
 
CSS 파싱
소개 글에서 설명했던 파싱의 개념을 기억하는가? HTML과는 다르게 CSS는 문맥 자유 문법이고 소개 글에서 설명했던 파서 유형을 이용하여 파싱이 가능하다. 실제로 CSS 명세는 CSS 어휘와 문법을 정의하고 있다.
가지 예제를 보자. 어휘 문법은 토큰을 위한 정규 표현식으로 정의되어 있다.
 

omment   \/[]([/][^]*)\/ 


num        [0-9]|[0-9]"."[0-9] 


nonascii    [\200-\377] 


nmstart    [_a-z]|{nonascii}|{escape} 


nmchar    [_a-z0-9-]|{nonascii}|{escape} 


name        {nmchar}+ 
ident        {nmstart}{nmchar}
 
"ident"는 클래스 이름처럼 식별자(identifier)를 줄인 것이다. "name"은 요소의 아이디("#"으로 참조하는) 이다.
구문 문법은 BNF로 설명되어 있다.

Ruleset : selector [ ',' S* selector ]'{' S declaration [ ';' S* declaration ]* '}' S*;Selector : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?;simple_selector : element_name [ HASH | class | attrib | pseudo ]| [ HASH | class | attrib | pseudo ]+;Class : '.' IDENT;element_name : IDENT | '';


Attrib : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*[ IDENT | STRING ] S* ] ']';Pseudo : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ];


룰셋(ruleset)은 다음과 같은 구조를 나타낸다.
div.error, a.error {    color: red;   font-weight: bold;}
div.error와 a.error 는 선택자(selector)이다. 중괄호 안쪽에는 룰셋에 적용된 규칙이 포함되어 있다. 이 구조는 공식적으로 다음과 같이 정의되어 있다.

Ruleset    : selector [ ',' S* selector ]       '{' S declaration [ ';' S* declaration ]* '}' S*   ;


룰셋은 쉼표와 공백(S가 공백을 의미함)으로 구분된 하나 또는 여러 개의 선택자라는 것을 의미한다. 룰셋은 중괄호 내부에 하나 또는 세미 콜론으로 구분된 여러 개의 선언을 포함한다. "선언"과 "선택자"는 이어지는 BNF에 정의되어 있다.
 
 
웹킷 CSS 파서
웹킷은 CSS 문법 파일로부터 자동으로 파서를 생성하기 위해 플렉스와 바이슨 파서 생성기를 사용한다. 파서 소개에서 언급했던 것처럼 바이슨은 상향식 이동 감소 파서를 생성한다. 파이어폭스는 직접 작성한 하향식 파서를 사용한다. 두 경우 모두 각 CSS 파일은 스타일 시트 객체로 파싱되고 객체는 CSS 규칙을 포함한다. CSS 규칙 객체는 선택자와 선언 객체 그리고 CSS 문법과 일치하는 다른 객체를 포함한다.
 

그림 12 CSS 파싱
 
 
 
 
스크립트와 스타일 시트의 진행 순서
 
스크립트
웹은 파싱과 실행이 동시에 수행되는 동기화(synchronous) 모델이다. 제작자는 파서가 < script > 태그를 만나면 즉시 파싱하고 실행하기를 기대한다. 스크립트가 실행되는 동안 문서의 파싱은 중단된다. 스크립트가 외부에 있는 경우 우선 네트워크로부터 자원을 가져와야 하는데 또한 실시간으로 처리되고 자원을 받을 때까지 파싱은 중단된다. 이 모델은 년간 지속됐고 HTML4와 HTML5의 명세에도 정의되어 있다. 제작자는 스크립트를 "지연(defer)"으로 표시할 있는데 지연으로 표시하게 되면 문서 파싱은 중단되지 않고 문서 파싱이 완료된 이후에 스크립트가 실행된다. HTML5는 스크립트를 비동기(asynchronous)로 처리하는 속성을 추가했기 때문에 별도의 맥락에 의해 파싱되고 실행된다.
 
예측 파싱
웹킷과 파이어폭스는 예측 파싱과 같은 최적화를 지원한다. 스크립트를 실행하는 동안 다른 스레드는 네트워크로부터 다른 자원을 찾아 내려받고 문서의 나머지 부분을 파싱한다.  이런 방법은 자원을 병렬로 연결하여 받을 있고 전체적인 속도를 개선한다. 참고로 예측 파서는 DOM 트리를 수정하지 않고 메인 파서의 일로 넘긴다. 예측 파서는 외부 스크립트, 외부 스타일 시트와 외부 이미지와 같이 참조된 외부 자원을 파싱할 뿐이다.
 
스타일 시트
한편 스타일 시트는 다른 모델을 사용한다. 이론적으로 스타일 시트는 DOM 트리를 변경하지 않기 때문에 문서 파싱을 기다리거나 중단할 이유가 없다. 그러나 스크립트가 문서를 파싱하는 동안 스타일 정보를 요청하는 경우라면 문제가 된다. 스타일이 파싱되지 않은 상태라면 스크립트는 잘못된 결과를 내놓기 때문에 많은 문제를 야기한다. 이런 문제는 흔치 않은 것처럼 보이지만 매우 빈번하게 발생한다. 파이어폭스는 아직 로드 중이거나 파싱 중인 스타일 시트가 있는 경우 모든 스크립트의 실행을 중단한다. 한편 웹킷은 로드되지 않은 스타일 시트 가운데 문제가 될만한 속성이 있을 때에만 스크립트를 중단한다.
 
 
렌더 트리 구축
DOM 트리가 구축되는 동안 브라우저는 렌더 트리를 구축한다. 표시해야 순서와 문서의 시각적인 구성 요소로써 올바른 순서로 내용을 그려낼 있도록 하기 위한 목적이 있다.
파이어폭스는 구성 요소를 "형상(frames)" 이라고 부르고 웹킷은 "렌더러(renderer)" 또는 "렌더 객체(render object)"라는 용어를 사용한다.
렌더러는 자신과 자식 요소를 어떻게 배치하고 그려내야 하는지 알고 있다.
웹킷 렌더러의 기본 클래스인 RenderObject 클래스는 다음과 같이 정의되어 있다.
 
class RenderObject { virtual void layout(); virtualvoid paint(PaintInfo); virtualvoid rect repaintRect();Node * node; //the DOM nodeRenderStyle * style; // the computed styleRenderLayer * containgLayer; //the containing z-index layer}
렌더러는 CSS2 명세에 따라 노드의 CSS 박스에 부합하는 사각형을 표시한다. 렌더러는 너비, 높이 그리고 위치와 같은 기하학적 정보를 포함한다. 
 
DOM 트리와 렌더 트리의 관계
렌더러는 DOM 요소에 부합하지만 1:1로 대응하는 관계는 아니다. 예를 들어 "head" 요소와 같은 비시각적 DOM 요소는 렌더 트리에 추가되지 않는다. 또한 display 속성에 "none" 값이 할당된 요소는 트리에 나타나지 않는다(visibility 속성에 "hidden" 값이 할당된 요소는 트리에 나타난다).
여러 개의 시각 객체와 대응하는 DOM 요소도 있는데 이것들은 보통 하나의 사각형으로는 묘사할 없는 복잡한 구조다. 예를 들면 "select" 요소는 '표시 영역, 드롭다운 목록, 버튼' 표시를 위한 3개의 렌더러가 있다. 또한 줄에 충분히 표시할 없는 문자가 여러 줄로 바뀔 줄은 별도의 렌더러로 추가된다. 여러 렌더러와 대응하는 다른 예는 깨진 HTML이다. CSS 명세에 의하면 인라인 박스는 블록 박스만 포함하거나 인라인 박스만을 포함해야 하는데 인라인과 블록 박스가 섞인 경우 인라인 박스를 감싸기 위한 익명의 블록 렌더러가 생성된다.
어떤 렌더 객체는 DOM 노드에 대응하지만 트리의 동일한 위치에 있지 않다. float 처리된 요소 또는 position 속성 값이 absolute로 처리된 요소는 흐름에서 벗어나 트리의 다른 곳에 배치된 상태로 형상이 그려진다. 대신 자리 표시자가 원래 있어야 곳에 배치된다.
 

그림 13 렌더 트리와 DOM 트리 대응(3.1). "뷰포트"는 최초의 블록이다. 웹킷에서는 "RenderView" 객체가 역할을 한다.
 
트리를 구축하는 과정
파이어폭스에서 프레젠테이션은 DOM 업데이트를 위한 리스너로 등록된다. 프레젠테이션은 형상 만들기를 FrameConstructor에 위임하고 FrameConstructor는 스타일(스타일 계산 참고)을 결정하고 형상을 만든다.
웹킷에서는 스타일을 결정하고 렌더러를 만드는 과정을 "어태치먼트(attachment)" 라고 부른다. 모든 DOM 노드에는 "attach" 메서드가 있다. 어태치먼트는 동기적인데 DOM 트리에 노드를 추가하면 노드의 "attach" 메서드를 호출한다.
html 태그와 body 태그를 처리함으로써 렌더 트리 루트를 구성한다. 루트 렌더 객체는 CSS 명세에서 포함 블록(다른 모든 블록을 포함하는 최상위 블록)이라고 부르는 그것과 일치한다. 파이어폭스는 이것을 ViewPortFrame이라 부르고 웹킷은 RenderView라고 부른다. 이것이 문서가 가리키는 렌더 객체다. 트리의 나머지 부분은 DOM 노드를 추가함으로써 구축된다.
 
 
 
다단계 순서에 따라 규칙 적용하기
스타일 객체는 모든 CSS 속성을 포함하고 있는데 어떤 규칙과도 일치하지 않는 일부 속성은 부모 요소의 스타일 객체로부터 상속 받는다. 그 다른 속성들은 기본 값으로 설정된다.
문제는 하나 이상의 속성이 정의될 시작되고 다단계 순서가 문제를 해결하게 된다.
스타일 시트 다단계 순서
스타일 속성 선언은 여러 스타일 시트에서 나타날 있고 하나의 스타일 시트 안에서도 여러 나타날 있는데 이것은 규칙을 적용하는 순서가 매우 중요하다는 것을 의미한다. 이것을 "다단계(cascade)" 순서라고 한다. CSS2 명세에 따르면 다단계 순서는 다음과 같다(아래에서 5가 가장 우선 순위가 높다).


  1. 브라우저 선언 (browser declarations)
  2. 사용자 일반 선언 (user normal declarations)
  3. 저작자 일반 선언 (author normal declarations)
  4. 저작자 중요 선언 (author important declarations)
  5. 사용자 중요 선언 (user important declarations)

브라우저 선언의 중요도가 가장 낮으며 사용자가 저작자의 선언을 덮어 있는 것은 선언이 중요하다고 표시한 경우뿐이다. 같은 순서 안에서 동일한 속성 선언은 특정성(specificity)에 의해 정렬이 되고 순서는 특정성이 된다. HTML 시각 속성은 CSS 속성 선언으로 변환되고 변환된 속성들은 저작자 일반 선언 규칙으로 간주된다.