본문 바로가기

0/web

브라우저의 작동 원리 (2) (How browsers work)

반응형

브라우저의 오류 처리

HTML 페이지에서 "유효하지 않은 구문"이라는 오류를 본 적이 없을 것이다. 이는 브라우저가 모든 오류 구문을 교정하기 때문이다. 참 대단하지 않을가? 아래 오류가 포함된 HTML 예제를 보자.

<html>  
   <mytag></mytag>
   <div>
     <p>
   </div>
   Really lousy HTML
   </p>
</html>

일부러 여러가지 규칙을 위반한 코드이다. 

  • "mytag"는 표준 태그가 아니다.
  • "p"태그와 "div"태그는 중첩 오류가 있다.

그러나 브라우저는 투덜거리지 않고 올바르게 표시하는데 이는 파서가 HTML 작성자의 실수를 수정했기 때문이다.

이런 오류 처리 행태는 브라우저에서 꽤나 일반적임에도 불구하고 HTML의 현재 명세가 아니라는 점이 놀랍다. 많은 사이트에서 HTML 오류를 쉽게 발견할 수 있지만 브라우저는 관습적으로 오류를 고치고 있다.

HTML5 명세는 이런 요구 사항 일부를 정의했다. 웹킷은 이것을 HTML 파서 클래스의 시작 부분에 주석으로 잘 요약해 두었다.

파서는 토큰화된 입력 값을 파싱하여 문서를 만들고 문서 트리를 생성한다. 규칙에 맞게 잘 작성된 문서라면 파싱이 수월하겠지만 그렇지 않은 경우의 문서또한 많이 다뤄야 하기 때문에 파서는 오류에 대한 아량이 있어야 한다.

파서는 적어도 다음과 같은 오류를 처리해야 한다.

  • 어떤 태그의 안쪽에 추가하려는 태그가 금지된 것일 때 일단 허용된 태그를 먼저 닫고 금지된 태그는 외부에 추가한다.
  • 파서가 직접 요소를 추가해서는 안된다. 문서 작성자에 의해 뒤늦게 요소가 추가될 수도 있고 생략이 가능한 경우도 있다. HTML, HEAD, BODY, TBODY, TR, TD, LI 태그가 이런 경우에 해당한다.
  • 인라인 요소 안쪽에 블록 요소가 있는 경우 부모 블록 요소를 만날 때까지 모든 인라인 태그를 닫는다.
  • 이런 방법이 도움이 되지 않으면 태그를 추가하거나 무시할 수 있는 상태가 될 때까지 요소를 닫는다.

웹킷이 오류를 처리하는 예는 다음과 같다.

<br> 대신 </br>

어떤 사이트는 <br> 대신 </br>을 사용한다. IE, Firefox와 호환성을 갖기 위해 웹킷은 이것을 <br>으로 간주한다. 코드는 다음과 같다.

if(t->isCloseTag(brTag) && m_document->inCompatMode()) {  
    reportError(MalformedBRError);
    t->beginTag = true;
}

오류는 내부적으로 처리하고 사용자에게는 표시하지 않는다.

어긋난 표

어긋난 표는 표 안에 또 다른 표가 th 또는 td 셀 내부에 있지 않은 것을 의미한다. 아래 예제와 같은 경우를 말한다.

<table>
    <table>
    <tr><td>inner table</td></tr>
    </table>
    <tr><td>outer table</td></tr>
</table>

이런 경우 웹킷은 표의 중첩을 분해하여 형제 요소가 되도록 처리한다.

<table>
    <tr><td>outer table</td></tr>
</table>

<table>
    <tr><td>inner table</td></tr>
</table>  

코드는 다음과 같다.

if(m_inStrayTableContent && localName == tableTag)
	popBlock(tableTag);

웹킷은 이런 오류를 처리하는데 스택을 사용한다. 안쪽의 표는 바깥쪽 표의 외부로 옮겨져서 형제 요소가 된다.

 

중첩된 폼 요소

폼 안에 또 다른 폼을 넣은 경우 안쪽의 폼은 무시된다.

if(!m_currentFormElement){
	m_currentFormElement = new HTMLFormElement(formTag, m_document);
}

 

태그 중첩이 너무 깊을 때

주석에는 이렇게 적혀있다.

www.liceo.edu.mx 사이트는 약 1,500개 수준의 태그 중첩이 되어있는 예제인데 모든 요소가 <b>로 되어있다. 최대 20개의 중첩만 허용하고 나머지는 무시한다.

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 태그

주석에는 이렇게 적혀 있다.

깨진 html을 지원한다. 일부 바보같은 페이지는 문서가 끝나기 전에 body를 닫아버리기 때문에 브라우저는 body 태그를 닫지 않는다. 대신 종료를 위해 end()를 호출한다. 

if(t->tagName == htmlTag || t->tagName == bodyTag)
	return;

 


CSS 파싱

이전에 설명했던 파싱 개념을 기억하는가? HTML과 다르게 CSS는 문맥 자유 문법이고 앞서 설명했던 파서 유형을 이용하여 파싱이 가능하다.

* 문맥 자유 문법 (Context Free Grammar) : 쉽게 설명하면 하나의 비말단 노드만을 고려하여 문자열을 생성하기 때문에 문맥에서 자유롭다는 의미에서 문맥 자유 문법이라고 불린다. 

몇가지 예제를 살펴보자.

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} // 식별자(identifier)를 줄인 것. ex)클래스 이름 등.

구문 문법은 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는 선택자이다. 중괄호 안쪽에는 이 ruleset에 적용된 규칙이 포함되어 있다. ruleset은 쉼표와 공백 (S가 공백을 의미함)으로 구분된 하나 또는 여러 개의 선택자라는 것을 의미한다. ruleset은 중괄호 내부에 하나 또는 세미 콜론으로 구분된 여러 개의 선언을 포함한다. "선언"과 "선택자"는 이어지는 BNF에 정의되어있다.

웹킷 CSS 파서

웹킷은 CSS 문법 파일로부터 자동으로 파서를 생성하기 위해 플렉스와 바이슨 파서 생성기를 사용한다. 파서 소개에서 언급했던 것처럼 바이슨은 상향식 이동 감소 파서를 생성한다. 파이어폭스는 직접 작성한 하향식 파서를 사용한다. 두 경우 모두 각 CSS 파일은 스타일시트 객체로 파싱되고 각 객체는 CSS 규칙을 포함한다. CSS 규칙 객체는 선택자와 선언 객체 그리고 CSS 문법과 일치하는 다른 객체를 포함한다.

CSS 파싱

스크립트와 스타일 시트의 진행 순서

스크립트

웹은 파싱과 실행이 동시에 수행되는 동기화(synchronous) 모델이다. 제작자는 파서가 <script> 태그를 만나면 즉시 파싱하고 실행하기를 기대한다. 스크립트가 실행되는 동안 문서의 파싱은 중단된다. 스크립트가 외부에 있는 경우 우선 네트워크로부터 자원을 가져와야 하는데 이 또한 실시간으로 처리되고 자원을 받을 때까지 파싱은 중단된다. 이 모델은 수 년간 지속됐다. 작성자는 스크립트를 "defer(지연)"으로 표시할 수 있는데 defer로 표시하게 되면 문서 파싱은 중단되지 않고 문서 파싱이 완료된 이후에 스크립트가 실행된다. HTML5는 스크립트를 비동기(asynchronous)로 처리하는 속성을 추가했기 때문에 별도의 맥락에 의해 파싱되고 실행된다.

예측 파싱

웹킷과 파이어폭스는 예측 파싱과 같은 최적화를 지원한다. 스크립트를 실행하는 동안 다른 스레드는 네트워크로부터 다른 자원을 찾아 내려받고 문서의 나머지 부분을 파싱한다. 이런 방법은 자원을 병렬로 연결하여 받을 수 있고 전체적인 속도를 개선한다. 참고로 예측파서는 DOM 트리를 수정하지 않고 메인 파서의 일로 넘긴다. 예측 파서는 외부 스크립트, 외부 스타일 시트와 외부 이미지와 같이 참조된 외부 자원을 파싱할 뿐이다.

스타일 시트

스타일 시트는 다른 모델을 사용한다. 이론적으로 스타일 시트는 DOM 트리를 변경하지 않기 때문에 문서 파싱을 기다리거나 중단할 이유가 없다. 그러나 스크립트가 문서를 파싱하는 동안 스타일 정보를 요청하는 경우라면 문제가 된다. 스타일이 파싱되지 않은 상태라면 스크립트는 잘못된 결과를 내놓기 때문에 많은 문제를 야기한다. 이런 문제는 흔치 않은 것처럼 보이지만 매우 빈번하게 발생한다. 파이어폭스는 아직 로드 중이거나 파싱 중인 스타일 시트가 있는 경우 모든 스크립트의 실행을 중단한다. 한편 웹킷은 로드되지 않은 스타일 시트 가운데 문제가 될만한 속성이 있을 때만 스크립트를 중단한다.

 


렌더 트리 구축

DOM 트리가 구축되는 동안 브라우저는 렌더 트리를 구축한다. 표시해야 할 순서와 문서의 시각적인 구성 요소로서 올바른 순서로 내용을 그려낼 수 있도록 하기 위함이다.

파이어폭스는 이 구성 요소를 "frames"라고 부르고 웹킷은 "renderer" 또는 "render object"라는 용어를 사용한다.

렌더러는 자신과 자식 요소를 어떻게 배치하고 그려내야 하는지 알고 있다.

웹킷 렌더러의 기본 클래스인 RenderObject 클래스는 다음과 같이 정의되어 있다.

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 노드에 어떤 유형의 렌더러를 만들어야 하는지 결정하는 코드이다.

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;
}

요소의 유형 또한 고려해야 하는데 폼 컨트롤과 표는 특별한 구조이다. 요소가 특별한 렌더러를 만들어야 한다면 웹킷은 createRenderer 메서드를 무시하고 비기하학 정보를 포함하는 스타일 객체를 표시한다.

 

DOM 트리와 렌더 트리의 관계

렌더러는 DOM 요소에 부합하지만 1:1로 대응하는 관계는 아니다. 예를 들어 "head" 요소와 같은 비시각적 DOM 요소는 렌더 트리에 추가되지 않는다. 또한 display 속성에 "none"값이 할당된 요소는 트리에 나타나지 않는다. (visibility 속성에 "hidden" 값이 할당된 요소는 트리에 나타난다)

여러 개의 시각 객체와 대응하는 DOM 요소도 있는데 이것들은 보통 하나의 사각형으로 묘사할 수 없는 복잡한 구조이다. 예를 들면 "select" 요소는 '표시 영역, 드롭다운 목록, 버튼' 표시를 위한 3개의 렌더러가 있다. 또한 한 줄에 충분히 표시할 수 없는 문자가 여러 줄로 바뀔 때 새로운 줄은 별도의 렌더러로 추가된다. 여러 렌더러와 대응하는 또 다른 예는 깨진 HTML이다. CSS 명세에 의하면 인라인 박스는 블록 박스만 포함하거나 인라인 박스만을 포함해야 하는데 인라인 블록 박스가 섞인 경우 인라인 박스를 감싸기 위한 익명의 블록 렌더러가 생성된다.

어떤 렌더 객체는 DOM 노드에 대응하지만 트리의 동일한 위치에 있지 않다. float 처리 요소 또는 position 속성 값이 absolute로 처리된 요소는 흐름에서 벗어나 트리의 다른 곳에 배치된 상태로 형상이 그려진다. 대신 자리 표시자가 원래 있어야 할 곳에 배치된다.

렌더 트리와 DOM 트리 대응. "뷰포트"는 최초의 블록. (웹킷에서는 "RenderView")

트리를 구축하는 과정

파이어폭스에서 프레젠테이션은 DOM 업데이트를 위한 리스너로 등록된다. 프레젠테이션은 형상 만들기를 FrameConstructor 에 위임하고 FrameConstructor 는 스타일(스타일 계산 참고)을 결정하고 형상을 만든다.

웹킷에서는 스타일을 결정하고 렌더러를 만드는 과정을 "attachment"라고 부른다. 모든 DOM 노드에는 "attach" 메서드가 있다. attachment는 동기적인데 DOM 트리에 노드를 추가하면 새 노드의 "attach" 메서드를 호출한다.

html 태그와 body 태그를 처리함으로써 렌더 트리 루트를 구성한다. 루트 렌더 객체는 CSS 명세에서 포함 블록 (다른 모든 블록을 포함하는 최상위 블록)이라고 부르는 것과 일치한다. 파이어폭스는 이것을 ViewPortFrame 이라 부르고 웹킷은 RenderView 라고 부른다. 이것이 문서가 가리키는 렌더 객체이다. 트리의 나머지 부분은 DOM 노드를 추가함으로써 구축된다.

 

스타일 계산

렌더 트리를 구축하려면 각 렌더 객체의 시각적 속성에 대한 계산이 필요한데 이것은 각 요소의 스타일 속성을 계산함으로써 처리된다.

스타일은 인라인 스타일 요소와 HTML의 시각적 속성(예를 들면 bgcolor 같은 HTML 속성)과 같은 다양한 형태의 스타일 시트를 포함하는데 HTML의 시각적 속성들은 대응하는 CSS 스타일 속성으로 변환된다.

최초의 스타일 시트는 브라우저가 제공하는 기본 스타일 시트인데 페이지 제작자 또는 사용자도 이를 제공할 수 있다. 브라우저는 사용자가 선호하는 스타일을 정의할 수 있도록 지원하는데 파이어폭스의 경우 "파이어폭스 프로필" 폴더에 있는 스타일 시트를 변경함으로써 사용자 선호 스타일을 정의할 수 있다.

스타일을 계산하는 일에는 다음과 같은 몇 가지 어려움이 따른다.

  1. 스타일 데이터는 구성이 매우 광범위하다. 수 많은 스타일 속성들을 수용하면서 메모리 문제를 야기할 수 있다.
  2. 최적화되어 있지 않다면 각 요소에 할당된 규칙을 찾는 것은 성능 문제를 야기할 수 있다. ex) div div div div { ... }
  3. 규칙을 적용하는 것은 계층 구조를 파악해야 하는 꽤나 복잡한 다단계 규칙을 수반한다.

브라우저가 이 문제를 어떻게 처리하는지 살펴보자.

스타일 정보 공유

웹킷 노드는 스타일 객체(RenderStyle)을 참조하는데 이 객체는 일정 조건 아래 공유할 수 있다. 노드가 형제이거나 또는 사촌일 때 공유하며 다음과 같은 조건일 때 공유할 수 있다.

 

  1. 동일한 마우스 반응 상태를 가진 요소여야 한다. ex) :hover, :focus 등.
  2. 아이디가 없는 요소여야 한다.
  3. 태그 이름이 일치해야 한다.
  4. 클래스 속성이 일치해야 한다.
  5. 지정된 속성이 일치해야 한다.
  6. 링크 상태가 일치해야 한다.
  7. focus 상태가 일치해야 한다.
  8. 문서 전체에서 속성 선택자의 영향을 받는 요소가 없어야 한다. 여기서 영향이라 함은 속성 선택자를 사용한 경우를 말한다. ex) input[type=text]{...}
  9. 요소에 인라인 스타일 속성이 없어야 한다. ex) <p style="..."></p>
  10. 문서 전체에서 형제 선택자를 사용하지 않아야 한다. 웹 코어는 형제 선택자를 만나면 전역 스위치를 열고 전체 문서의 스타일 공유를 중단한다. 형제 선택자는 + 선택자와 :first-child, :last-child를 포함한다.

파이어폭스 규칙 트리

파이어폭스는 스타일 계산을 쉽게 처리하기 위해 규칙트리와 스타일 문맥 트리라고 하는 두 개의 트리를 더 가지고 있다. 웹킷도 스타일 객체를 가지고 있지만 스타일 문맥 트리처럼 저장되지 않고 오직 DOM 노드로 관련 스타일을 처리한다.

파이어폭스 스타일 문맥 트리

스타일 문맥에는 최종 값이 저장되어 있다. 값은 올바른 순서 안에서 부합하는 규칙을 적용하고 논리로부터 구체적인 값으로 변환함으로써 계산된다. 예를 들어 논리적인 값이 화면의 백분율(%)이라면 이 값은 계산에 의해 절대적인 단위(px)로 변환된다. 이런 규칙 트리 아이디어는 정말 현명하다. 노드 사이에서 이 값을 공유함으로써 다시 계산하는 일을 방지하기 때문이다.

부합하는 모든 규칙은 트리에 저장하는데 경로의 하위 노드가 높은 우선순위를 갖는다. 규칙 저장은 느리게 처리된다. 트리는 처음부터 모든 노드를 계산하지 않지만 노드 스타일이 계산될 필요가 있을 때 계산된 경로를 트리에 추가한다.

트리 경로를 어휘 목록 속에 있는 단어라고 생각하고 이미 규칙 트리를 계산했다고 가정해 보자.

내용 트리에서 또 다른 요소에 부합하는 규칙이 필요하다고 가정하고 부합하는 규칙이 순서에 따라 B-E-I라고 치자. 브라우저는 이미 A-B-E-I-L 경로를 계산했기 때문에 트리 안에 이 경로가 있고 할 일이 줄었다.

트리가 작업량을 줄이는 방법을 살펴보자.

구조체로 분리

스타일 문맥은 구조체로 나뉘는데 선 또는 색상과 같은 종류의 스타일 정보를 포함한다. 구조체의 속성들은 상속되거나 또는 상속되지 않는다. 속성들은 요소에 따라 정해져 있지 않은 한 부모로부터 상속된다. 상속되지 않는 속성들은 "reset" 속성이라 부르는데 상속을 받지 않는 것으로 정해져 있다면 기본 값을 사용한다.

트리는 최종으로 계산된 값을 포함하여 전체 구조체를 저장하는 방법으로 도움을 준다. 하위 노드에 구조체를 위한 속성 선언이 없다면 저장된 상위 노드의 구조체 속성을 그대로 받아서 사용하는 것이다.

규칙 트리를 사용하여 스타일 문맥을 계산

어떤 요소의 스타일 문맥을 계산할 때 가장 먼저 규칙 트리의 경로를 계산하거나 또는 이미 존재하는 경로를 사용한다. 그 다음 새로운 스타일 문맥으로 채우기 위해 경로 안에서 규칙을 적용한다. 가장 높은 우선순위(보통 가장 구체적인 선택자)를 가진 경로의 하위 노드에서 시작하여 구조체가 가득 찰 때까지 트리의 상단으로 거슬러 올라간다. 규칙 노드 안에서 구조체를 위한 특별한 선언이 없다면 상당한 최적화를 할 수 있다. 선언이 가득 채워질 때까지 노드 트리의 상위로 찾아 올라가서 간단하게 적용하면 최상의 최적화가 되고 모든 구조체는 공유된다. 이것은 최종 값과 메모리 계산을 절약한다.

선언이 완전하지 않으면 구조체가 채워질 때까지 트리의 상단으로 거슬러 올라간다.

구조체에서 어떤 선언도 발견할 수 없는 경우 구조체는 "상속(inherit)" 타입인데 문맥 트리에서 부모 구조체를 향하면서 구조체를 공유한다. 재설정 구조체라면 기본 값들이 사용될 것이다.

가장 구체적인 노드에 값을 추가하면 실제 값으로 변환하기 위해 약간의 추가적인 계산을 할 필요가 있는데 트리 노드에서 결과를 저장하기 때문에 자식에게도 사용할 수 있다.

같은 트리 노드를 가리키는 형제 요소가 있는 경우 전체 스타일 문맥이 이들 사이에서 공유된다.

이런 HTML이 있다고 가정해 보자.

<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 div { margin:5px; color:black }
2 .err { color:red }
3 .big { margin-top:3px }
4 div span { margin-bottom:4px }
5 #div1 { color:blue }
6 #div2 { color:green }

색상과 여백 이렇게 두개의 구조체를 채울 필요가 있다고 치자. 색상 구조체는 오직 색상 값만 포함하고 여백 구조체는 네 개의 면에 대한 값을 포함한다.

결과적으로 규칙트리는 아래처럼 보일 것이다. 노드는 노드 이름과 노드가 가리키는 규칙의 번호로 표시되어 있다.

규칙 트리

문맥 트리는 아래처럼 보일 것이다. 노드는 노드 이름과 노드가 가리키는 규칙 노드로 표시되어 있다.

문맥 트리

HTML을 파싱하여 두 번째 <div> 태그인 <div class="err" id="div2"> 에 이르렀다고 가정하자. 이 노드에 필요한 스타일 문맥을 생성하고 스타일 구조체를 채워야 한다.

두 번째 <div> 규칙에 맞는 것을 찾으면 1, 2, 6이 되는데 이것은 요소가 사용할 수 있는 트리 경로 (규칙 트리의 B:1, C:2)가 이미 존재한다는 것을 의미하고 규칙 6(규칙 트리에서 노드 F:6)에 이르는 또 다른 노드를 문맥 트리에 추가하면 된다. 스타일 문맥을 생성하고 문맥 트리에 추가하면 새로운 스타일 문맥이 규칙 트리의 F:6 노드를 가리킨다.

이제는 스타일 구조체를 채워야 하는데 margin 구조체를 채우는 것으로부터 시작한다. 마지막 규칙 노드(F:6)가 margin 구조체에 포함하지 않기 때문에 이전 노드에 저장된 구조체를 찾을 때까지 위로 거슬러 올라가서 계산된 값을 사용한다. margin 규칙이 선언된 최상위 노드의 구조체를 규칙 노드 B:1에서 찾았다.

color 구조체 정의에는 저장된 구조체를 사용할 수 없다. color는 이미 하나의 속성 값을 가지고 있기 때문에 다른 값을 채우기 위해 규칙 트리 상단으로 거슬러 올라갈 필요가 없다. 최종 값을 계산하고 계산된 값(문자열에서 RGB 등으로 변환된)을 이 노드에 저장할 것이다.

두 번째 <span> 요소에 대한 작업은 훨씬 쉽다. 맞는 규칙을 찾다보면 이전 span과 같이 규칙 트리의 G:3을 가리킨다는 결론에 이르는데 동일한 노드를 가리키는 형제가 있기 때문에 전체 스타일 문맥을 공유하고 이전 span의 문맥을 취하면 된다.

부모로부터 상속된 규칙을 포함하는 구조체의 경우 캐싱은 문맥 트리에서 수행된다. (color 속성은 실제로 상속된다. 그러나 파이어폭스는 재설정으로 처리해서 규칙 트리에 저장한다.)

예를 들어 문단 요소에 글꼴을 위한 규칙을 추가한다면,

p { font-family:Verdana; font-size:10px; font-weight:bold; }

문맥 트리에서 div의 자식인 p 요소는 부모와 동일한 글꼴 구조체를 공유할 수 있다. p 요소에 지정된 규칙이 없는 경우라도 마찬가지이다.

규칙 트리가 없는 웹킷에서 일치하는 선언은 네 번 탐색된다. 

  1. 중요하지 않은 상위 속성이 적용. (다른 요소들이 의존할 수 있기 때문. ex) display) 
  2. 그 다음 중요한 상위 속성이 적용.
  3. 중요하지 않은 일반 속성이 적용.
  4. 중요한 일반 속성이 적용.

 

이것은 여러 번 나타나는 속성들이 정확한 순서에 따라 결정된다는 것을 의미하고 가장 마지막 값이 적용된다.

요약하면 스타일 객체는 전체 또는 일부를 공유함으로써

  1. 스타일 데이터는 구성이 매우 광범위하다. 수 많은 스타일 속성들을 수용하면서 메모리 문제를 야기할 수 있다.
  2. 최적화되어 있지 않다면 각 요소에 할당된 규칙을 찾는 것은 성능 문제를 야기할 수 있다. ex) div div div div { ... }
  3. 규칙을 적용하는 것은 계층 구조를 파악해야 하는 꽤나 복잡한 다단계 규칙을 수반한다.

1번과 3번 문제를 해결한다. 파이어폭스 규칙 트리는 올바른 순서에 따라 속성을 적용하는 것을 돕는다.

 

쉬운 매치를 위한 규칙 다루기

스타일 규칙을 위한 몇 가지 소스가 있다.

<style>
// CSS 규칙을 외부 스타일 시트에서 선언하거나 style 요소에서 선언
p {color:blue}
</style>

<!-- 인라인 스타일 속성 -->
<p style="color:blue"></p>

<!-- HTML의 시각적 속성 (이것들은 CSS 규칙으로 변환됨.)-->
<p bgcolor="blue"></p>

마지막 두 가지 스타일은 자신이 스타일 속성을 가지고 있거나 HTML 속성을 이용하여 연결할 수 있기 때문에 요소에 쉽게 연결된다.

위에서 언급한 문제 2번에 따라 CSS 규칙을 연결하는 것은 까다로울 수 있는데 이 문제를 해결하려면 쉽게 접근할 수 있도록 규칙을 교묘하게 처리해야 한다.

스타일 시트를 파싱한 후 규칙은 선택자에 따라 여러 해시맵 중 하나에 추가된다. 

  • 아이디 맵
  • 클래스 이름 맵
  • 태그 이름 맵
  • 일반적인 맵

선택자가 아이디인 경우 규칙은 아이디 맵에 추가되고 선택자가 클래스인 경우 규칙은 클래스 맵에 추가된다.

이런 처리 작업을 통해 규칙을 찾는 일은 훨씬 쉬워진다. 맵에서 특정 요소와 관련 있는 규칙을 추출할 수 있기 때문에 모든 선언을 찾아볼 필요가 없다. 이러한 최적화는 찾아야 할 규칙의 95% 이상을 제거하기 때문에 규칙을 찾는 동안 모든 선언을 고려할 필요가 없다.

다음 스타일 규칙 예제를 살펴보자.

p.error {color:red}  
#messageDiv {height:50px}
div {margin:5px} 

첫 번째 규칙은 클래스 맵에, 두 번째는 아이디 맵, 세 번째는 태그 맵에 추가된다.

위 스타일과 관련된 HTML 코드는 다음과 같다.

<p class="error">an error occurred </p>  
<div id=" messageDiv">this is a message</div>  

우선 p 요소의 규칙을 찾아보자. 클래스 맵은 "p.error"에 대한 규칙인 "error" 키를 가지고 있다. div 요소는 아이디 맵과 태그 맵에 관련 규칙이 있다. 그러므로 이제 남은 작업은 키를 사용하여 추출한 규칙 중에 실제로 일치하는 규칙을 찾는 것이다.

예를 들어 div에 해당하는 또 다른 규칙이 있다고 가정하자.

table div {margin:5px}

이 예제는 태그 맵에서 규칙을 추출할 것이다. 가장 우측에 있는 선택자가 키이기 때문이다. 그러나 앞서 작성한 div 요소와는 일치하지 않는다. 상위에 table이 없기 때문이다.

웹킷과 파이어폭스 모두 이런 방식으로 처리하고 있다.

 

* 이 글은 브라우저는 어떻게 동작하는가? 와 How browsers work? 을 재구성한 글입니다.

반응형