NHN Ajax UI랩 장정환, 웹표준개발2팀 남덕현, 오픈퍼블리싱팀 정찬명
이 글은 이스라엘 개발자 탈리 가르시엘(Tali Garsiel)이 html5rocks.com에 게시한 "How Browsers Work: Behind the scenes of modern web browsers"를 번역한 글입니다. 탈리 가르시엘은 몇 년간 브라우저 내부와 관련된 공개 자료를 확인하고, C++ 코드 수백만 줄 분량의 WebKit이나 Gecko 같은 오픈소스 렌더링 엔진의 소스 코드를 직접 분석하면서 어떻게 브라우저가 동작하는지 파악했습니다.
소개
이 글에서 설명하는 브라우저
최근에는 인터넷 익스플로러, 파이어폭스, 사파리, 크롬, 오페라 이렇게 다섯 개의 브라우저를 많이 사용하지만 나는 파이어폭스, 크롬, 사파리와 같은 오픈소스 브라우저를 예로 들 것이다. 사파리는 부분적으로 오픈소스이다. StatCounter 브라우저 통계에 의하면 2012년 3월 현재 파이어폭스, 사파리, 크롬의 점유율은 62.57%에 달한다. 오픈소스 브라우저가 시장의 상당 부분을 차지하게 된 것이다.
브라우저의 주요 기능
브라우저의 사용자 인터페이스는 서로 닮아 있는데 다음과 같은 요소들이 일반적이다.
브라우저의 기본 구조
- 사용자 인터페이스 - 주소 표시줄, 이전/다음 버튼, 북마크 메뉴 등. 요청한 페이지를 보여주는 창을 제외한 나머지 모든 부분이다.
- 브라우저 엔진 - 사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어.
- 렌더링 엔진 - 요청한 콘텐츠를 표시. 예를 들어 HTML을 요청하면 HTML과 CSS를 파싱하여 화면에 표시함.
- 통신 - HTTP 요청과 같은 네트워크 호출에 사용됨. 이것은 플랫폼 독립적인 인터페이스이고 각 플랫폼 하부에서 실행됨.
- UI 백엔드 - 콤보 박스와 창 같은 기본적인 장치를 그림. 플랫폼에서 명시하지 않은 일반적인 인터페이스로서, OS 사용자 인터페이스 체계를 사용.
- 자바스크립트 해석기 - 자바스크립트 코드를 해석하고 실행.
그림 1 브라우저의 주요 구성 요소
크롬은 대부분의 브라우저와 달리 각 탭마다 별도의 렌더링 엔진 인스턴스를 유지하는 것이 주목할만하다. 각 탭은 독립된 프로세스로 처리된다.
렌더링 엔진
렌더링 엔진의 역할은 요청 받은 내용을 브라우저 화면에 표시하는 일이다.
렌더링 엔진들
웹킷은 최초 리눅스 플랫폼에서 동작하기 위해 제작된 오픈소스 엔진인데 애플이 맥과 윈도우즈에서 사파리 브라우저를 지원하기 위해 수정을 가했다. 더 자세한 내용은 webkit.org를 참조한다.
동작 과정
렌더링 엔진은 통신으로부터 요청한 문서의 내용을 얻는 것으로 시작하는데 문서의 내용은 보통 8KB 단위로 전송된다.
렌더링 엔진은 HTML 문서를 파싱하고 "콘텐츠 트리" 내부에서 태그를 DOM 노드로 변환한다. 그 다음 외부 CSS 파일과 함께 포함된 스타일 요소도 파싱한다. 스타일 정보와 HTML 표시 규칙은 "렌더 트리"라고 부르는 또 다른 트리를 생성한다.
렌더 트리는 색상 또는 면적과 같은 시각적 속성이 있는 사각형을 포함하고 있는데 정해진 순서대로 화면에 표시된다.
렌더 트리 생성이 끝나면 배치가 시작되는데 이것은 각 노드가 화면의 정확한 위치에 표시되는 것을 의미한다. 다음은 UI 백엔드에서 렌더 트리의 각 노드를 가로지르며 형상을 만들어 내는 그리기 과정이다.
일련의 과정들이 점진적으로 진행된다는 것을 아는 것이 중요하다. 렌더링 엔진은 좀 더 나은 사용자 경험을 위해 가능하면 빠르게 내용을 표시하는데 모든 HTML을 파싱할 때까지 기다리지 않고 배치와 그리기 과정을 시작한다. 네트워크로부터 나머지 내용이 전송되기를 기다리는 동시에 받은 내용의 일부를 먼저 화면에 표시하는 것이다.
동작 과정 예
그림 4 모질라의 게코 렌더링 엔진 동작 과정(3.6)
웹킷과 게코가 용어를 약간 다르게 사용하고 있지만 동작 과정은 기본적으로 동일하다는 것을 그림 3과 그림 4에서 알 수 있다.
게코는 시각적으로 처리되는 렌더 트리를 "형상 트리(frame tree)"라고 부르고 각 요소를 형상(frame)이라고 하는데 웹킷은 "렌더 객체(render object)"로 구성되어 있는 "렌더 트리(render tree)"라는 용어를 사용한다. 웹킷은 요소를 배치하는데 "배치(layout)" 라는 용어를 사용하지만 게코는 "리플로(reflow)" 라고 부른다. "어태치먼트(attachment)"는 웹킷이 렌더 트리를 생성하기 위해 DOM 노드와 시각 정보를 연결하는 과정이다. 게코는 HTML과 DOM 트리 사이에 "콘텐츠 싱크(content sink)"라고 부르는 과정을 두는데 이는 DOM 요소를 생성하는 공정으로 웹킷과 비교하여 의미있는 차이점이라고 보지는 않는다.
파싱과 DOM 트리 구축
파싱 일반
파싱은 렌더링 엔진에서 매우 중요한 과정이기 때문에 더 자세히 다룰 필요가 있다. 파싱에 대한 간단한 소개로 시작한다.
예를 들면 2+3-1과 같은 표현식은 다음과 같은 트리가 된다.
문법
파싱은 문서에 작성된 언어 또는 형식의 규칙에 따르는데 파싱할 수 있는 모든 형식은 정해진 용어와 구문 규칙에 따라야 한다. 이것을 문맥 자유 문법이라고 한다. 인간의 언어는 이런 모습과는 다르기 때문에 기계적으로 파싱이 불가능하다.
파서-어휘 분석기 조합
파싱은 어휘 분석과 구문 분석이라는 두 가지로 구분할 수 있다.
어휘 분석은 자료를 토큰으로 분해하는 과정이다. 토큰은 유효하게 구성된 단위의 집합체로 용어집이라고도 할 수 있는데 인간의 언어로 말하자면 사전에 등장하는 모든 단어에 해당된다.
변환
파싱 예
그림 5에서는 수학 표현식을 파싱 트리로 만들어 보았다. 간단한 수학 언어를 정의하고 파싱 과정을 살펴 보자.
어휘: 수학 언어는 정수, 더하기 기호, 빼기 기호를 포함한다.
어휘와 구문에 대한 공식적인 정의
어휘는 보통 정규 표현식으로 표현한다. 예를 들면 언어는 다음과 같이 정의될 것이다.
1 2 3 | INTEGER : 0|[1-9][0-9]* PLUS : + MINUS : - |
보시다시피 정수는 정규 표현식으로 정의한다.
구문은 보통 BNF 라고 부르는 형식에 따라 정의한다. 언어는 다음과 같이 정의될 것이다.
1 2 3 | expression := term operation term operation := PLUS | MINUS term := INTEGER | expression |
문법이 문맥 자유 문법이라면 언어는 정규 파서로 파싱할 수 있다. 문맥 자유 문법을 쉽게 말하면 완전히 BNF로 표현 가능한 문법이다. 공식적인 정의는 위키백과의 문맥 자유 문법을 참조한다.
파서의 종류
상향식 파서는 입력 값이 규칙에 맞을 때까지 찾아서 맞는 입력 값을 규칙으로 바꾸는데 이 과정은 입력 값의 끝까지 진행된다. 부분적으로 일치하는 표현식은 파서 스택에 쌓인다.
스택 | 입력 값 |
2+3-1 | |
항 | +3-1 |
항 연산자 | 3-1 |
표현식 | -1 |
표현식 연산자 | 1 |
표현식 |
파서 자동 생성
웹킷은 잘 알려진 두 개의 파서 생성기를 사용한다. 어휘 생성을 위한 플렉스(Flex)와 파서 생성을 위한 바이슨(Bison)이다. 렉스(Lex)와 약(Yacc)이라는 이름과 함께 들어본 적이 있을지도 모르겠다. 플렉스는 토큰의 정규 표현식 정의를 포함하는 파일을 입력 받고 바이슨은 BNF 형식의 언어 구문 규칙을 입력 받는다.
HTML 파서
HTML 파서는 HTML 마크업을 파싱 트리로 변환한다.
HTML 문법 정의
HTML의 어휘와 문법은 W3C에 의해 명세로 정의되어 있다. 현재 버전은 HTML4와 초안 상태로 진행 중인 HTML5 이다.
문맥 자유 문법이 아님
파싱 일반 소개를 통해 알게 된 것처럼 문법은 BNF와 같은 형식을 이용하여 공식적으로 정의할 수 있다.
HTML 정의를 위한 공식적인 형식으로 DTD(문서 형식 정의)가 있지만 이것은 문맥 자유 문법이 아니다.
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의 관계를 맺는다. 예를 들면 이런 마크업이 있다.
<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은 일반적인 하향식 또는 상향식 파서로 파싱이 안되는데 그 이유는 다음과 같다.
일반적인 파싱 기술을 사용할 수 없기 때문에 브라우저는 HTML 파싱을 위해 별도의 파서를 생성한다.
파싱 알고리즘은 HTML 자세히 설명되어 있다. 알고리즘은 토큰화와 트리 구축 이렇게 두 단계로 되어 있다.
토큰화는 어휘 분석으로서 입력 값을 토큰으로 파싱한다. HTML에서 토큰은 시작 태그, 종료 태그, 속성 이름과 속성 값이다.
토큰화는 토큰을 인지해서 트리 생성자로 넘기고 다름 토큰을 확인하기 위해 다음 문자를 확인한다. 그리고 입력의 마지막까지 이 과정을 반복한다.
그림 9 HTML 파싱 과정(HTML5 명세에서 가져옴)
토큰화 알고리즘
다음은 HTML 토큰화를 설명하기 위한 기본적인 예제이다.
<body>
Hello world
</body>
</html>
트리 구축 알고리즘
<body>
Hello world
</body>
</html>
상태는 "head 이전" 모드로 바뀌었고 "body" 토큰을 받았다. "head" 토큰이 없더라도 HTMLHeadElement는 묵시적으로 생성되어 트리에 추가될 것이다.
"Hellow world" 문자열의 문자 토큰을 받았다. 첫 번째 토큰이 생성되고 "본문" 노드가 추가되면서 다른 문자들이 그 노드에 추가될 것이다.
body 종료 토큰을 받으면 "body 다음" 모드가 된다. html 종료 태그를 만나면 "body 다음 다음" 모드로 바뀐다. 마지막 파일 토큰을 받으면 파싱을 종료한다.
파싱이 끝난 이후의 동작
이번 단계에서 브라우저는 문서와 상호작용할 수 있게 되고 문서 파싱 이후에 실행되어야 하는 "지연" 모드 스크립트를 파싱하기 시작한다. 문서 상태는 "완료"가 되고 "로드" 이벤트가 발생한다. 보다 자세한 내용은 HTML5의 토큰화 알고리즘과 트리 구축에서 볼 수 있다.
브라우저의 오류 처리
HTML 페이지에서 "유효하지 않은 구문" 이라는 오류를 본 적이 없을 것이다. 이는 브라우저가 모든 오류 구문을 교정하기 때문이다. 아래 오류가 포함된 HTML 예제를 보자.
<mytag></mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
HTML5 명세는 이런 요구 사항 일부를 정의했다. 웹킷은 이것을 HTML 파서 클래스의 시작 부분에 주석으로 잘 요약해 두었다.
<br> 대신 </br>
어떤 사이트는 <br> 대신 </br>을 사용한다. 인터넷 익스플로러, 파이어폭스와 호환성을 갖기 위해 웹킷은 이것을 <br> 으로 간주한다. 코드는 다음과 같다.
1 2 3 4 | if (t->isCloseTag(brTag) && m_document->inCompatMode()) { reportError(MalformedBRError); t->beginTag = true ; } |
오류는 내부적으로 처리하고 사용자에게는 표시하지 않는다.
어긋난 표
어긋난 표는 표 안에 또 다른 표가 th 또는 td 셀 내부에 있지 않은 것을 의미한다. 아래 예제와 같은 경우를 말한다.
이런 경우 웹킷은 표의 중첩을 분해하여 형제 요소가 되도록 처리한다.
1 2 | if (m_inStrayTableContent && localName == tableTag) popBlock(tableTag); |
웹킷은 이런 오류를 처리하는데 스택을 사용한다. 안쪽의 표는 바깥쪽 표의 외부로 옮겨져서 형제 요소가 된다.
중첩된 폼 요소
폼 안에 또 다른 폼을 넣은 경우 안쪽의 폼은 무시된다. 코드는 다음과 같다.
1 2 3 | if (!m_currentFormElement) { m_currentFormElement = new HTMLFormElement(formTag, m_document); } |
태그 중첩이 너무 깊을 때
www.liceo.edu.mx 사이트는 약 1,500개 수준의 태그 중첩이 되어 있는 예제인데 모든 요소가 <b>로 되어 있다. 최대 20개의 중첩만 허용하고 나머지는 무시한다.
1 2 3 4 5 6 7 8 9 | bool HTMLParser::allowNestedRedundantTag( const AtomicString& tagName) { unsigned i = 0; for (HTMLStackElem* curr = m_blockStack; i < cMaxRedundantTagDepth && curr && curr->tagName == tagName; curr = curr->next, i++) { } return i != cMaxRedundantTagDepth; } |
잘못 닫힌 html 또는 body 태그
1 2 | if (t->tagName == htmlTag || t->tagName == bodyTag ) return ; |
웹킷 오류 처리 코드에 예제로 등장하는 것을 원치 않는다면 웹 제작자는 올바른 HTML 작성을 위해 신경 써야 할 것이다.
CSS 파싱
소개 글에서 설명했던 파싱의 개념을 기억하는가? HTML과는 다르게 CSS는 문맥 자유 문법이고 소개 글에서 설명했던 파서 유형을 이용하여 파싱이 가능하다. 실제로 CSS 명세는 CSS 어휘와 문법을 정의하고 있다.
몇 가지 예제를 보자. 어휘 문법은 각 토큰을 위한 정규 표현식으로 정의되어 있다.
1 2 3 4 5 6 7 | 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로 설명되어 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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)은 다음과 같은 구조를 나타낸다.
1 2 3 4 | div.error, a.error { color : red ; font-weight : bold ; } |
div.error와 a.error 는 선택자(selector)이다. 중괄호 안쪽에는 이 룰셋에 적용된 규칙이 포함되어 있다. 이 구조는 공식적으로 다음과 같이 정의되어 있다.
1 2 3 4 | Ruleset : selector [ ',' S* selector ]* '{' S* declaration [ ';' S* declaration ]* '}' S* ; |
룰셋은 쉼표와 공백(S가 공백을 의미함)으로 구분된 하나 또는 여러 개의 선택자라는 것을 의미한다. 룰셋은 중괄호 내부에 하나 또는 세미 콜론으로 구분된 여러 개의 선언을 포함한다. "선언"과 "선택자"는 이어지는 BNF에 정의되어 있다.
웹킷 CSS 파서
웹킷은 CSS 문법 파일로부터 자동으로 파서를 생성하기 위해 플렉스와 바이슨 파서 생성기를 사용한다. 파서 소개에서 언급했던 것처럼 바이슨은 상향식 이동 감소 파서를 생성한다. 파이어폭스는 직접 작성한 하향식 파서를 사용한다. 두 경우 모두 각 CSS 파일은 스타일 시트 객체로 파싱되고 각 객체는 CSS 규칙을 포함한다. CSS 규칙 객체는 선택자와 선언 객체 그리고 CSS 문법과 일치하는 다른 객체를 포함한다.
그림 12 CSS 파싱
스크립트와 스타일 시트의 진행 순서
스크립트
예측 파싱
스타일 시트
렌더 트리 구축
DOM 트리가 구축되는 동안 브라우저는 렌더 트리를 구축한다. 표시해야 할 순서와 문서의 시각적인 구성 요소로써 올바른 순서로 내용을 그려낼 수 있도록 하기 위한 목적이 있다.
파이어폭스는 이 구성 요소를 "형상(frames)" 이라고 부르고 웹킷은 "렌더러(renderer)" 또는 "렌더 객체(render object)"라는 용어를 사용한다.
렌더러는 자신과 자식 요소를 어떻게 배치하고 그려내야 하는지 알고 있다.
웹킷 렌더러의 기본 클래스인 RenderObject 클래스는 다음과 같이 정의되어 있다.
1 2 3 4 5 6 7 8 | 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 } |
각 렌더러는 CSS2 명세에 따라 노드의 CSS 박스에 부합하는 사각형을 표시한다. 렌더러는 너비, 높이 그리고 위치와 같은 기하학적 정보를 포함한다.
박스 유형은 노드와 관련된 "display" 스타일 속성의 영향을 받는다(스타일 계산 참고). 여기 보이는 웹킷 코드는 display 속성에 따라 DOM 노드에 어떤 유형의 렌더러를 만들어야 하는지 결정하는 코드이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | RenderObject* RenderObject::createObject(Node* node, RenderStyle* style) { Document* doc = node->document(); RenderArena* arena = doc->renderArena(); … RenderObject* o = 0; switch (style->display()) { case NONE: break ; case INLINE: o = new (arena) RenderInline(node); break ; case BLOCK: o = new (arena) RenderBlock(node); break ; case INLINE_BLOCK: o = new (arena) RenderBlock(node); break ; case LIST_ITEM: o = new (arena) RenderListItem(node); break ; ... } return o; } |
요소 유형 또한 고려해야 하는데 예를 들면 폼 콘트롤과 표는 특별한 구조이다. 요소가 특별한 렌더러를 만들어야 한다면 웹킷은 creatRenderer 메서드를 무시하고 비기하학 정보를 포함하는 스타일 객체를 표시한다.
DOM 트리와 렌더 트리의 관계
그림 13 렌더 트리와 DOM 트리 대응(3.1). "뷰포트"는 최초의 블록이다. 웹킷에서는 "RenderView" 객체가 이 역할을 한다.
트리를 구축하는 과정
파이어폭스에서 프레젠테이션은 DOM 업데이트를 위한 리스너로 등록된다. 프레젠테이션은 형상 만들기를 FrameConstructor에 위임하고 FrameConstructor는 스타일(스타일 계산 참고)을 결정하고 형상을 만든다.
웹킷에서는 스타일을 결정하고 렌더러를 만드는 과정을 "어태치먼트(attachment)" 라고 부른다. 모든 DOM 노드에는 "attach" 메서드가 있다. 어태치먼트는 동기적인데 DOM 트리에 노드를 추가하면 새 노드의 "attach" 메서드를 호출한다.
html 태그와 body 태그를 처리함으로써 렌더 트리 루트를 구성한다. 루트 렌더 객체는 CSS 명세에서 포함 블록(다른 모든 블록을 포함하는 최상위 블록)이라고 부르는 그것과 일치한다. 파이어폭스는 이것을 ViewPortFrame이라 부르고 웹킷은 RenderView라고 부른다. 이것이 문서가 가리키는 렌더 객체다. 트리의 나머지 부분은 DOM 노드를 추가함으로써 구축된다.
스타일 계산
렌더 트리를 구축하려면 각 렌더 객체의 시각적 속성에 대한 계산이 필요한데 이것은 각 요소의 스타일 속성을 계산함으로써 처리된다.
스타일을 계산하는 일에는 다음과 같은 몇 가지 어려움이 따른다.
스타일 정보 공유
파이어폭스 규칙 트리
스타일 문맥에는 최종 값이 저장되어 있다. 값은 올바른 순서 안에서 부합하는 규칙을 적용하고 논리로부터 구체적인 값으로 변환함으로써 계산된다. 예를 들어 논리적인 값이 화면의 백분율(%) 이라면 이 값은 계산에 의해 절대적인 단위(px)로 변환된다. 이런 규칙 트리 아이디어는 정말 현명하다. 노드 사이에서 이 값을 공유함으로써 그것들을 다시 계산하는 일을 방지하기 때문이다.
부합하는 모든 규칙은 트리에 저장하는데 경로의 하위 노드가 높은 우선순위를 갖는다. 규칙 저장은 느리게 처리된다. 트리는 처음부터 모든 노드를 계산하지 않지만 노드 스타일이 계산될 필요가 있을 때 계산된 경로를 트리에 추가한다.
트리 경로를 어휘 목록 속에 있는 단어라고 생각하고 이미 규칙 트리를 계산했다고 가정해 보자.
내용 트리에서 또 다른 요소에 부합하는 규칙이 필요하다고 가정하고 부합하는 규칙이 순서에 따라 B - E - I 라고 치자. 브라우저는 이미 A - B - E - I - L 경로를 계산했기 때문에 트리 안에 이 경로가 있고 할 일이 줄었다.
트리가 작업량을 줄이는 방법을 살펴보자.
구조체로 분리
규칙 트리를 사용하여 스타일 문맥을 계산
선언이 완전하지 않으면 구조체가 채워질 때까지 트리의 상단으로 거슬러 올라간다.
가장 구체적인 노드에 값을 추가하면 실제 값으로 변환하기 위해 약간의 추가적인 계산을 할 필요가 있는데 트리 노드에서 결과를 저장하기 때문에 자식에게도 사용할 수 있다.
같은 트리 노드를 가리키는 형제 요소가 있는 경우 전체 스타일 문맥이 이들 사이에서 공유된다.
1 2 3 4 5 6 7 | < div class = "err" id = "div1" > < p > this is a < span class = "big" > big error </ span > this is also a < span class = "big" > very big error</ span > error </ p > </ div > < div class = "err" id = "div2" >another error</ div > |
좀 단순하게 하기 위해 색상과 여백 이렇게 두 개의 구조체를 채울 필요가 있다고 치자. 색상 구조체는 오직 색상 값만을 포함하고 여백 구조체는 네 개의 면에 대한 값을 포함한다.
결과적으로 규칙 트리는 아래처럼 보일 것이다. 노드는 노드 이름과 노드가 가리키는 규칙의 번호로 표시되어 있다.
문맥 트리는 아래처럼 보일 것이다. 노드는 노드 이름과 노드가 가리키는 규칙 노드로 표시되어 있다.
부모로부터 상속된 규칙을 포함하고 있는 구조체의 저장은 문맥 트리에서 처리된다. 색상 속성은 실제로 상속된다. 그러나 파이어폭스는 재설정으로 처리해서 규칙 트리에 저장한다.
예를 들어 문단 요소에 글꼴을 위한 규칙을 추가한다면.
1 | p { font-family : Verdana ; font size : 10px ; font-weight : bold } |
문맥 트리에서 div의 자식인 p 요소는 그 부모의 동일 글꼴 구조체를 공유할 수 있다. p 요소에 지정된 규칙이 없는 경우라도 마찬가지다.
요약하면 스타일 객체는 전체 또는 일부를 공유함으로써 1번과 3번 문제를 해결한다. 파이어폭스 규칙 트리는 올바른 순서에 따라 속성을 적용하는 것을 돕는다.
쉬운 선택을 위한 규칙 다루기
CSS 규칙을 외부 스타일 시트에서 선언하거나 style 요소에서 선언
1 | p { color : blue } |
인라인 스타일 속성
1 | < p style = "color:blue" ></ p > |
HTML의 시각적 속성(이것들은 CSS 규칙으로 변환됨)
1 | < p bgcolor = "blue" ></ p > |
마지막 두 가지 스타일은 자신이 스타일 속성을 가지고 있거나 HTML 속성을 이용하여 연결할 수 있기 때문에 요소에 쉽게 연결된다.
위에서 언급한 문제 2번에 따라 CSS 규칙을 연결하는 것은 까다로울 수 있는데 이 문제를 해결하려면 쉽게 접근할 수 있도록 규칙을 교묘하게 처리해야 한다.
스타일 시트를 파싱한 후 규칙은 선택자에 따라 여러 해시맵 중 하나에 추가된다. 아이디, 클래스 이름, 태그 이름을 사용한 맵이 있고 이런 분류에 맞지 않는 것을 위한 일반적인 맵이 있다. 선택자가 아이디인 경우 규칙은 아이디 맵에 추가되고 선택자가 클래스인 경우 규칙은 클래스 맵에 추가된다.
이런 처리 작업을 통해 규칙을 찾는 일은 훨씬 쉬워진다. 맵에서 특정 요소와 관련 있는 규칙을 추출할 수 있기 때문에 모든 선언을 찾아 볼 필요가 없다. 이러한 최적화는 찾아야 할 규칙의 95% 이상을 제거하기 때문에 규칙을 찾는 동안 모든 선언을 고려할 필요가 없다.
다음 스타일 규칙 예제를 살펴보자.
1 2 3 | p.error { color : red } #messageDiv { height : 50px } div { margin : 5px } |
첫 번째 규칙은 클래스 맵에 추가된다. 두 번째는 아이디 맵에 추가되고 세 번째는 태그 맵에 추가된다.
위 스타일과 관련된 HTML 코드는 다음과 같다.
1 2 | < p class = "error" >an error occurred </ p > < div id = " messageDiv" >this is a message</ div > |
우선 p 요소의 규칙을 찾아보자. 클래스 맵은 발견된 "p.error"를 위한 규칙 하부의 "error" 키를 찾았다. div 요소는 아이디 맵(키는 아이디)과 태그 맵에 관련 규칙이 있다. 그러므로 이제 남은 작업은 키를 사용하여 추출한 규칙 중에 실제로 일치하는 규칙을 찾는 것이다.
예를 들어 div에 해당하는 다음과 같은 또 다른 규칙이 있다고 가정하자.
1 | table div { margin : 5px } |
이 예제는 여전히 태그 맵에서 규칙을 추출할 것이다. 가장 우측에 있는 선택자가 키이기 때문이다. 그러나 앞서 작성한 div 요소와는 일치하지 않는다. 상위에 table이 없기 때문이다.
웹킷과 파이어폭스 모두 이런 방식으로 처리하고 있다.
다단계 순서에 따라 규칙 적용하기
스타일 객체는 모든 CSS 속성을 포함하고 있는데 어떤 규칙과도 일치하지 않는 일부 속성은 부모 요소의 스타일 객체로부터 상속 받는다. 그 외 다른 속성들은 기본 값으로 설정된다.
문제는 하나 이상의 속성이 정의될 때 시작되고 다단계 순서가 이 문제를 해결하게 된다.
스타일 시트 다단계 순서
브라우저 선언의 중요도가 가장 낮으며 사용자가 저작자의 선언을 덮어 쓸 수 있는 것은 선언이 중요하다고 표시한 경우뿐이다. 같은 순서 안에서 동일한 속성 선언은 특정성(specificity)에 의해 정렬이 되고 이 순서는 곧 특정성이 된다. HTML 시각 속성은 CSS 속성 선언으로 변환되고 변환된 속성들은 저작자 일반 선언 규칙으로 간주된다.
특정성
선택자 특정성은 CSS2 명세에 다음과 같이 정의되어 있다.
- 선택자 없이 'style' 속성이 선언된 것이면 1을 센다. 그렇지 않으면 0을 센다. (=a)
- 선택자에 포함된 아이디 선택자 개수를 센다. (=b)
- 선택자에 포함된 속성 선택자(클래스 선택자와 속성 선택자)와 가상 클래스 선택자의 숫자를 센다. (=c)
- 선택자에 포함된 요소 선택자와 가상 요소 선택자의 숫자를 센다. (=d)
네 개의 연결된 숫자 a-b-c-d (큰 진법의 숫자)를 연결하면 특정성의 값이 된다.
사용할 진법은 분류 중에 가장 높은 숫자에 의해서 정의된다. 예를 들어 a=14이면 16진수를 사용할 수 있다. 흔치는 않겠지만 a=17과 같은 경우라면 17진법이 필요할 것이다. 17진법을 사용해야 하는 경우는 html body div div p … (선택자에 17개의 태그를 사용하는 경우로 흔치 않음)와 같이 선택자를 사용하는 경우에 발생할 수 있다.
다음과 같은 몇 가지 예제를 참고하기 바란다.
1 2 3 4 5 6 7 8 9 10 | *{} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */ li{} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */ li:first-line{} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */ ul li{} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */ ul ol+li{} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */ h 1 +*[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */ ul ol li. red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */ li. red . level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */ #x 34 y{} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */ style= "" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */ |
규칙 정렬
1 2 3 4 5 6 | static bool operator >(CSSRuleData& r1, CSSRuleData& r2) { int spec1 = r1.selector()->specificity(); int spec2 = r2.selector()->specificity(); return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2; } |
점진적 처리
배치
렌더러가 생성되어 트리에 추가될 때 크기와 위치 정보는 없는데 이런 값을 계산하는 것을 배치 또는 리플로라고 부른다.
HTML은 흐름 기반의 배치 모델을 사용하는데 이것은 보통 단일 경로를 통해 크기와 위치 정보를 계산할 수 있다는 것을 의미한다. 일반적으로 "흐름 속"에서 나중에 등장하는 요소는 앞서 등장한 요소의 위치와 크기에 영향을 미치지 않기 때문에 배치는 왼쪽에서 오른쪽으로 또는 위에서 아래로 흐른다. 단, 표는 크기와 위치를 계산하기 위해 하나 이상의 경로를 필요로 하기 때문에 예외가 된다 (3.5).
좌표계는 기준점으로부터 상대적으로 위치를 결정하는데 좌단(X축)과 상단(Y축) 좌표를 사용한다.
배치는 반복되며 HTML 문서의 <html> 요소에 해당하는 최상위 렌더러에서 시작한다. 배치는 프레임 계층의 일부 또는 전부를 통해 반복되고 각 렌더러에 필요한 크기와 위치 정보를 계산한다.
최상위 렌더러의 위치는 0,0 이고 브라우저 창의 보이는 영역에 해당하는 뷰포트 만큼의 면적을 갖는다.
모든 렌더러는 "배치" 또는 "리플로" 메서드를 갖는데 각 렌더러는 배치해야 할 자식의 배치 메소드를 불러온다.
더티 비트 체제
"더티"와 "자식이 더티" 이렇게 두 가지 플래그가 있다. 자식이 더터하다는 것은 본인은 괜찮지만 자식 가운데 적어도 하나를 다시 배치할 필요가 있다는 의미다.
전역 배치와 점증 배치
배치는 렌더러 트리 전체에서 일어날 수 있는데 이것을 "전역" 배치라 하고 다음과 같은 경우에 발생한다.
배치는 더티 렌더러가 배치되는 경우에만 점증되는데 추가적인 배치가 필요하기 때문에 약간의 손실이 발생할 수 있다.
점증 배치는 렌더러가 더티일 때 비동기적으로 일어난다. 예를 들면 네트워크로부터 추가 내용을 받아서 DOM 트리에 더해진 다음 새로운 렌더러가 렌더 트리에 붙을 때이다.
그림 17 점증 배치 - 오직 더티 렌더러와 그 자식만 배치된다(3.6).
비동기 배치와 동기 배치
"offsetHeight" 같은 스타일 정보를 요청하는 스크립트는 동기적으로 점증 배치를 실행한다.
때때로 배치는 스크롤 위치 변화와 같은 일부 속성들 때문에 초기 배치 이후 콜백으로 실행된다.
최적화
배치가 "크기 변경" 또는 렌더러 위치 변화 때문에 실행되는 경우 렌더러의 크기는 다시 계산하지 않고 캐시로부터 가져온다.
배치 과정
파이어폭스는 "상태" 객체(nsHTMLReflowState)를 배치("리플로"를 의미)를 위한 매개 변수로 사용하는데 상태는 부모의 너비를 포함한다.
파이어폭스 배치의 결과는 "매트릭스" 객체(nsHTMLReflowMatrics)인데 높이가 계산된 렌더러를 포함한다.
너비 계산
렌더러의 너비는 포함하는 블록의 너비, 그리고 렌더러의 너비와 여백, 테두리를 이용하여 계산된다.
1 | < div style = "width:30%" ></ div > |
웹킷은 다음(RenderBox 클래스의 calcWidth 메서드)과 같이 계산할 것이다.
여기까지 "미리 획득한 너비"의 계산이었다. 이제는 최소 너비와 최대 너비를 계산해야 한다.
미리 획득한 너비가 최대 너비보다 크면 최대 너비가 사용된다. 미리 획득한 너비가 최소 너비(깨지지 않는 가장 작은 단위)보다 작으면 최소 너비가 사용된다.
배치할 필요가 있지만 너비가 고정된 경우 값은 캐시에 저장된다.
줄 바꿈
렌더러가 배치되는 동안 줄을 바꿀 필요가 있을 때 배치는 중단되고 줄 바꿀 필요가 있음을 부모에게 전달한다. 부모는 추가 렌더러를 생성하고 배치를 호출한다.
그리기
그리기 단계에서는 화면에 내용을 표시하기 위한 렌더 트리가 탐색되고 렌더러의 "paint" 메서드가 호출된다. 그리기는 UI 기반의 구성 요소를 사용한다.
전역과 점증
그리기 순서
CSS 2는 그리기 과정의 순서를정의했다. 이것은 실제로 요소가 stacking contexts에 쌓이는 순서다. 스택은 뒤에서 앞으로 그려지기 때문에 이 순서는 그리기에 영향을 미친다. 블록 렌더러가 쌓이는 순서는 다음과 같다.
- 배경 색
- 배경 이미지
- 테두리
- 자식
- 아웃라인
파이어폭스 표시 목록
파이어폭스는 다른 불투명 요소 뒤에 완전히 가려진 요소는 추가하지 않는 방법으로 최적화를 진행한다.
웹킷 사각형 저장소
리페인팅 전에 웹킷은 기존의 사각형을 비트맵으로 저장하여 새로운 사각형과 비교하고 차이가 있는 부분만 다시 그린다.
동적 변경
렌더링 엔진의 스레드
통신은 몇 개의 병렬 스레드에 의해 진행될 수 있는데 병렬 연결의 수는 보통 2개에서 6개로 제한된다(예를 들면 파이어폭스 3은 6개를 사용).
이벤트 순환
1 2 | while (!mExiting) NS_ProcessNextEvent( thread ); |
CSS2 시각 모델
캔버스
CSS2 명세는 캔버스를 "서식 구조가 표현되는 공간" 이라고 설명한다. 브라우저가 내용을 그리는 공간인 것이다. 캔버스 공간 각각의 면적은 무한하지만 브라우저는 뷰포트의 크기를 기초로 초기 너비를 결정한다.
CSS2 명세에 따르면 캔버스는 기본적으로 투명하기 때문에 다른 캔버스와 겹치는 경우 비쳐 보이고, 투명하지 않을 경우에는 브라우저에서 정의한 색이 지정된다.
CSS 박스 모델
CSS 박스 모델은 문서 트리에 있는 요소를 위해 생성되고 시각적 서식 모델에 따라 배치된 사각형 박스를 설명한다.
각 박스는 콘텐츠 영역(문자, 이미지 등)과 선택적인 패딩과 테두리, 여백이 있다.
그림 18 CSS2 박스 모델
각 노드는 이런 상자를 0에서 n개 생성한다.
모든 요소는 만들어질 박스의 유형을 결정하는 "display" 속성을 갖는데 이 속성의 유형은 다음과 같다.
- block - 블록 상자를 만든다.
- inline - 하나 또는 그 이상의 인라인 상자를 만든다.
- none - 박스를 만들지 않는다.
기본 값은 인라인이지만 브라우저의 스타일 시트는 다른 기본 값을 설정한다. 예를 들면 "div" 요소의 display 속성에 대한 기본 값은 block 이다.
브라우저의 기본 스타일 시트 예제는 www.w3.org/TR/CSS2/sample.html에서 찾을 수 있다.
위치 결정 방법
위치는 "position" 속성과 "float" 속성에 의해 결정된다.
박스가 배치되는 방법은 다음과 같은 방법으로 결정된다.
박스 유형
인라인 박스: 블록이 되지 않고 블록 내부에 포함된다.
블록은 다른 블록 아래 수직으로 배치되고 인라인은 수평으로 배치된다.
위치 잡기
상대적인 위치
상대적인 위치 잡기는 일반적인 흐름에 따라 위치를 결정한 다음 필요한 만큼 이동한다.
플로트
플로트 박스는 라인의 왼쪽 또는 오른쪽으로 이동한다. 흥미로운 점은 다른 박스가 이 주변을 흐른다는 것이다.
1 2 3 4 | < p > < img src = "http://helloworld.naver.com/image.gif" alt = "image.gif" style = "float:right" height = "100" width = "100" > Lorem ipsum dolor sit amet, consectetuer... </ p > |
절대적인(absolute) 위치와 고정된(fixed) 위치
절대와 고정 배치는 일반적인 흐름과 무관하게 결정되고, 일반적인 흐름에 관여하지 않으며, 면적은 부모에 따라 상대적이다. 고정인 경우 뷰포트로부터 위치를 결정한다.
참고 - 고정된 박스는 문서가 스크롤되어도 따라 움직이지 않는다.
층 표현
이것은 CSS의 z-index 속성에 의해 명시된다. 층은 박스의 3차원 표현이고 "z 축"을 따라 위치를 정한다.
붉은색 박스가 초록색 박스보다 마크업에서 먼저 나오기 때문에 일반적인 흐름이라면 먼저 그려져야 하지만 z-index 속성이 높기 때문에 더 앞쪽에 표시된다.
참고 자료
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers.
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers (Google tech talk video)
- L. David Baron, Mozilla's Layout Engine
- L. David Baron, Mozilla Style System Documentation
- Chris Waterson, Notes on HTML Reflow
- Chris Waterson, Gecko Overview
- Alexander Larsson, The life of an HTML HTTP request

- 탈리 가르시엘(Tali Garsiel)
- 이 글의 저자 탈리 가르시엘(Tali Garsiel)은 이스라엘의 개발자이다. 그녀는 2000년에 웹 개발을 시작해서 넷스케이프의 "evil" 레이어 모델의 괴짜가 되었다. 리차드 패인먼(Richard Feynmann)처럼 그녀는 브라우저 동작에 매료되서 내부를 파헤치고 그녀가 발견한 것들을 문서화 했다. 탈리는 클라이언트측 성능 가이드에 관한 짧은 글을 쓰기도 했다.
번역
Ajax UI랩 장정환, 웹표준개발2팀 남덕현, 오픈퍼블리싱팀 정찬명
테크니컬 에디팅
웹플랫폼개발랩 송기선, 기술문서팀 박춘권
라이선스
특별한 고지가 없는 한 이 페이지의 내용은 크리에이티브
커먼즈
속성 3.0 라이선스 아래 있습니다. 코드 샘플은 아파치 2.0 라이선스 아래 있습니다.
이미지 출처
이 글에 첨부된 이미지의 출처는 특별한 언급이 없는 한 "How
Browsers
Work: Behind
the
scenes
of
modern
web
browsers"입니다.
'HTML.CSS' 카테고리의 다른 글
IE8 CSS expression 지원 중단 (0) | 2008.11.21 |
---|---|
DOM 의 이해 (0) | 2008.05.07 |
「HTML 5」를 주목해야 하는 이유 (0) | 2008.04.25 |
HTML 특수문자 표현하기 (0) | 2008.04.25 |
댓글