Naver D2에 올라온 번역글 ‘브라우저는 어떻게 동작하는가?’를 읽으면서 정리한 내용입니다.
출처: https://d2.naver.com/helloworld/59361

크롬, 파이어폭스, 사파리와 같은 오픈소스 브라우저의 예

브라우저의 주요 기능

  • 사용자가 선택한 자원을 서버에 요청하고 브라우저에 표시하는 것
  • 자원은 보통 html 문서이지만 pdf나 이미지일 수도 있다
  • 자원의 주소는 URI(Uniform Resource Identifier)에 의해 정해진다
  • 브라우저는 html과 css 명세에 따라 html을 해석해 표현. 예전에는 브라우저들이 독자적인 방법으로 해석해 호환성 문제가 있었지만, 최근에는 대부분 브라우저가 표준 명세를 따름.
  • 브라우저의 사용자 인터페이스는 서로 비슷

브라우저의 기본 구조

  • 사용자 인터페이스: 주소 표시줄, 이전/다음 버튼 등. 요청한 페이지를 보여주는 부분 외 전부
  • 브라우저 엔진: 사용자 인터페이스와 렌더링 엔진 사이 동작 제어
  • 렌더링 엔진: 요청한 컨텐츠를 표시
  • 통신: HTTP 요청과 같은 네트워크 호출에 사용
  • UI 백엔드: 콤보 박스와 창 같은 기본적인 장치를 그림
  • 자바스크립트 해석기: 자바스크립트 코드를 해석하고 실행
  • 자료 저장소: 자료를 저장하는 계층. HTML5 명세에는 브라우저가 지원하는 ‘웹 데이터 베이스’가 정의됨

렌더링 엔진

렌더링 엔진들

파이어폭스와 크롬, 사파리는 두 종류의 렌더링 엔진으로 제작됨. 파이어폭스는 모질라에서 만든 Gecko 엔진을 사용하고 크롬은 Webkit 엔진을 사용함.

동작 과정

렌더링 엔진은 통신으로부터 요청한 문서의 내용을 얻는 것으로 시작. 렌더링 엔진의 기본 동작 과정

  • DOM 트리 구축 위한 HTML 파싱
  • 렌더 트리 구축
  • 렌더 트리 배치
  • 렌더 트리 그리기

렌더링 엔진은 html 문서를 파싱하고 “콘텐츠 트리” 내부에서 태그를 dom 노드로 변환. 그 다음 외부 css 파일과 함께 포함된 스타일 요소도 파싱. 스타일 정보와 html 표시 규칙은 “렌더 트리”라 불리는 또 다른 트리 생성

렌더 트리는 색상 또는 면적과 같은 시각적 속성이 있는 사각형을 포함하고 있는데 정해진 순서대로 화면에 표시된다.

렌더 트리 생성이 끝나면 배치 시작. 노드가 화면의 정확한 위치에 표시됨.

파싱과 DOM 트리 구축

파싱 일반

문서 파싱은 브라우저가 코드를 이해하고 사용할 수 있는 구조로 변환하는 것. 파싱 결과는 보통 문서 구조를 나타내는 노드 트리. parsing tree, syntax tree라고도 함.

문법

파싱할 수 있는 모든 형식은 정해진 용어와 구문 규칙에 따라야 한다. 이것을 문맥 자유 문법이라고 함.

파서-어휘 분석기 조합

파싱은 어휘 분석과 구문 분석 두 가지로 구분할 수 있다.
어휘 분석은 자료를 토큰으로 분해하는 과정. 토큰은 유효하게 구성된 단위의 집합체.
구문 분석은 언어의 구문 규칙을 적용하는 과정.
파서는 보통 두 가지 일을 함. 자료를 유효한 토큰으로 분해하는 어휘 분석기와 언어 규칙에 따라 문서 구조를 분석함으로써 파싱 트리를 생성하는 파서. 어휘 분석기는 공백과 줄 바꿈 같은 의미 없는 문자를 제거.
파싱 과정은 반복됨.
규칙에 맞지 않으면 파서는 토큰을 내부적으로 저장하고 토큰과 일치하는 규칙이 발견될 때까지 요청. 맞는 규칙이 없는 경우 예외로 처리하는데 이는 문서가 구문 오류를 포함하고 있다는 것.

변환

파싱은 보통 문서를 다른 양식으로 변환하는데 컴파일이 하나의 예. 컴파일러는 파싱 트리 생성 후 이를 기계 코드 문서로 변환한다.

파서의 종류

하향식 파서와 상향식 파서. 하향식은 구문의 상위 구조로부터 일치하는 부분을 찾기 시작하고 상향식 파서는 낮은 수준에서 점차 높은 수준으로 찾는다.

파서 자동 생성

파서를 생성해주는 도구가 파서 생성기.
웹킷은 두 개의 파서 생성기 - 어휘 생성을 위한 Flex와 파서 생성을 위한 Bison 사용. 플렉스는 토큰의 정규 표현식 정의를 포함하는 파일을 입력받고 바이슨은 BNF 형식의 언어 구문 규칙을 입력 받음.

HTML 파서

HTML 마크업을 파싱 트리로 변환
모든 전통적인 파서는 HTML에 적용할 수 없음. 다만 파싱은 css와 자바스크립트를 파싱하는데 사용됨.
HTML 정의를 위한 공식적인 형식으로 DTD(문서 형식 정의)가 있지만 이것은 문맥 자유 문법이 아님.

HTML DTD

HTML의 정의는 SGML 계열 언어의 정의를 이용한 것.
DTD는 여러 변종이 있다. 엄격한 형식은 명세만을 따르지만 다른 형식은 낡은 브라우저에서 사용된 마크업을 지원.

DOM

“파싱 트리”는 DOM 요소와 속성 노드의 트리로서 출력 트리가 된다. DOM은 문서 객체 모델(Document Object Model)의 준말.

DOM은 마크업과 1:1 관계를 맺는다.

파싱 알고리즘

html은 일반적인 상향식 또는 하향식 파서로 파싱이 안된다. 그 이유는

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

때문에 브라우저는 html 파싱을 위한 별도의 파서를 생성한다.
파싱 알고리즘은 토큰화와 트리 구축 두 단계로 되어있다.

토큰화 알고리즘

알고리즘의 결과물은 html 토큰이다. 알고리즘은 상태 기계이다. 각 상태는 하나 이상의 연속된 문자를 입력받아 이 문자에 다음 상태를 갱신한다.

<html>
   <body>
      Hello world
   </body>
</html>  

초기 상태는 “자료 상태”. < 문자를 만나면 “태그 열림 상태”로 변함. a-z 문자를 만나면 “시작 태그 토큰”을 생성하고 상태는 “태그 이름 상태”로 변하는데 > 문자를 만날 때까지 유지. 각 문자에는 새로운 토큰 이름이 붙는데 이 경우 생성된 토큰은 html 토큰.
> 문자에 도달하면 현재 토큰이 발행되고 상태는 “자료 상태”로 바뀜. 이렇게 html 태그와 body 태그를 발행, Hello world의 H 문자를 만나면 문자 토큰이 발행될 것. 종료 태그의 < 문자를 만날 때까지 진행됨.
다시 태그 열림 상태가 됨. /문자는 종료 태그 토큰을 생성하고 “태그 이름 상태”로 변경됨. 이 상태는 > 문자를 만날 때까지 유지됨.

트리 구축 알고리즘.

파서가 생성되면 문서 객체가 생성됨. 트리 구축이 진행되는 동안 문서 최상단에는 DOM 트리가 수정되고 요소가 추가됨. 토큰화에 의해 발행된 각 노드는 트리 생성자에 의해 처리됨. 각 토큰을 위한 DOM 요소의 명세는 정의됨. DOM 트리에 요소를 추가하는 것이 아니라면 열린 요소는 스택에 추가된다.

<html>
   <body>
      Hello world
   </body>
</html>

트리 구축 단계의 입력 값은 토큰화 단계에서 만들어지는 일련의 토큰. 받은 html 토큰은 html 이전 모드가 되고 토큰은 이 모드에서 처리됨. 이것은 HTMLHtmlElement 요소를 생성하고 문서 객체의 최상단에 추가됨.
상태는 “head 이전”모드로 바뀌고 “body” 토큰을 받았다. “head” 토큰이 없더라도 HTMLHtmlElement는 묵시적으로 생성되어 트리에 추가될 것이다.
다음은 “head 다음” 모드. body 토큰이 처리되고 HTMLBodyElement가 생성되어 추가되었으며 “body 안쪽” 모드가 되었다.
“Hello world” 문자열의 문자 토큰을 받음. 첫 번째 토큰이 생성되고 “본문” 노드가 추가되면서 다른 문자들이 그 노드에 추가될 것.
body 종료 토큰을 받으면 “body 다음” 모드가 된다. html 종료 태그를 만나면 “body 다음 다음” 모드로 바뀐다. 마지막 파일 토큰을 받으면 파싱을 종료한다.

파싱이 끝난 이후의 동작

문서 상태는 “완료”가 되고 “로드” 이벤트가 발생.

브라우저의 오류 처리

“유효하지 않은 구문” 이라는 오류. 이는 브라우저가 모든 오류 구문을 교정하기 때문.

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

여러 규칙을 위반했지만 브라우저는 올바르게 표시. 파서가 실수를 수정했기 때문.
파서는 토큰화된 입력 값을 파싱하여 문서를 만들고 문서 트리를 생성. 규칙에 맞게 작성된 문서라면 파싱이 수월하겠지만 형식에 맞지 않은 많은 html 문서를 다뤄야 하기에 파서는 오류에 대한 아량이 있어야 한다.
파서는 다음과 같은 오류를 처리해야됨

  1. 어떤 태그의 안쪽에 추가하려는 태그가 금지된 것일 때 일단 허용된 태그를 먼저 닫고 금지된 태그는 외부에 추가한다
  2. 파서가 직접 요소를 추가해서는 안된다. html, head, body, tbody, tr, td, li 태그가 이런 경우에 해당.
  3. 인라인 요소 안쪽에 블록 요소가 있는 경우 부모 블록 요소를 만날 때까지 모든 인라인 태그를 닫는다
  4. 이런 방법이 도움이 되지 않으면 태그를 추가하거나 무시할 수 있는 상태가 될 때까지 요소를 닫는다.

웹킷이 오류를 처리하는 예

<br> 대신 </br>

어떤 사이트는 <br> 대신 </br> 사용. 익스플로러, 파이어폭스와 호환성을 갖기 위해 웹킷은 이를 <br>로 간주

어긋난 표

표 안에 또다른 표가 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>  

웹킷은 이런 처리를 할 때 스택 사용. 안쪽 표는 바깥쪽 표의 외부로 옮겨져 형제 요소가 됨.

중첩된 폼 요소

폼 안에 또다른 폼을 넣는 경우 안쪽 폼은 무시됨

태그 중첩이 너무 깊을 때

최대 20개의 중첩만 허용하고 나머지는 무시한다.

잘못 닫힌 html 또는 body 태그

문서가 끝나기 전 body 태그를 닫은 경우 브라우저는 body 태그를 닫지 않는다.
대신 종료를 위해 end()를 호출한다.

css 파싱

css는 문맥 자유 문법이고 소개 글에서 설명했던 파서 유형을 이용하여 파싱이 가능하다.

웹킷 CSS 파서

css 문법 파일로부터 자동으로 파서를 생성하기 위해 플렉스와 바이슨 파서 생성기를 사용. 바이슨은 상향식 이동 감소 파서를 생성. 파이어폭소는 하향식 파서 사용. 각 css 파일은 스타일 시트 객체로 파싱, 각 객체는 css 규칙을 포함. css 규칙 객체는 선택자와 선언 객체, 그리고 css 문법과 일치하는 다른 객체를 포함.

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

스크립트

웹은 파싱과 실행이 동시에 수행되는 동기화(synchronous) 모델. 제작자는 파서가

예측 파싱

웹킷과 파이어폭스는 예측 파싱과 같은 최적화를 지원. 스크립트를 실행하는 동안 다른 스레드는 네트워크로부터 다른 자원을 찾아 내려받고 문서의 나머지 부분을 파싱. 이렇게 하면 자원을 병렬로 연결해 받을 수 있고 전체적인 속도 개선. 예측 파서는 외부 스크립트, 외부 스타일 시트 등 참조된 외부 자원을 파싱.

스타일 시트

스타일 시트는 DOM 트리를 변경하지 않기 때문에 문서 파싱을 기다리거나 중단하지 않음. 그러나 스크립트가 문서를 파싱하는 동안 스타일 시트를 요청하는 경우라면 문제가 됨. 스타일이 파싱되지 않은 상태라면 스크립트는 잘못된 결과를 내놓기 때문에 많은 문제를 야기함.

렌더 트리 구축

DOM 트리가 구축되는 동안 브라우저는 렌더 트리 구축. 표시해야 할 순서와 문서의 시각적 구성 요소로써 올바른 순서로 내용을 그려내기 위함.
파이어폭스는 이를 형상(frames)이라 부르고 웹킷은 렌더러(renderer) 또는 render object라는 용어 사용.
렌더러는 자신과 자식 요소를 어떻게 배치해야 하는지 알고 있음.
RenderObject class

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 박스에 부합하는 사각형을 표시. 렌더러는 너비, 높이, 위치 등의 기하학적인 정보 포함.

DOM 트리와 렌더 트리의 관계

둘은 1:1로 대응하는 관계는 아님. head 요소 같은 비시각적 DOM 요소는 렌더 트리에 추가되지 않음. display: none 은 트리에 나타나지 않음.
여러 개의 시각 객체와 대응하는 DOM 요소도 있음. “select” 요소는 ‘표시 영역, 드롭다운 목록, 버튼’ 표시를 위한 3개의 렌더러가 있음. 한 줄에 충분히 표시할 수 없는 문자가 여러 줄로 바뀔 때 별도의 렌더러로 추가됨. 또한 깨진 html. 인라인과 블록 박스가 섞인 경우 인라인 박스를 감싸기 위한 익명의 블록 렌더러가 생성됨.

트리를 구축하는 과정

웹킷에서는 스타일을 결정하고 렌더러를 만드는 과정을 attachment라고 부른다. 모든 DOM 노드에는 attach 메서드가 있다. 어태치먼트는 동기적인데 DOM 트리에 노드를 추가하면 새 노드의 attach 메서드를 호출한다.
html과 body 태그를 처리함으로써 렌더 트리 루트를 구성. 루트 렌더 객체는 css 명세에서 포함 블록과 일치. 파이어폭스는 이것을 ViewPortFrame 이라 부르고, 웹킷은 RenderView라 부름. 이것이 문서가 가리키는 렌더 객체이고 트리의 나머지 부분은 DOM 노드를 추가함으로써 구축됨.

스타일 계산

렌더 트리를 구축하려면 각 렌더 객체의 시각적 속성에 대한 계산이 필요한데, 이것은 각 요소의 스타일 속성을 계산함으로써 처리됨.
스타일은 인라인 스타일 요소와 html의 시각적 속성과 같은 다양한 스타일 시트를 포함. html의 시각적 속성은 대응하는 css 스타일 속성으로 변환됨.

스타일을 계산할 때 어려움

  1. 스타일 데이터는 구성이 매우 광범위. 많은 스타일 속성을 수용하면서 메모리 문제를 야기할 수 있음
  2. 최적화되어 있지 않다면 각 요소에 할당된 규칙을 찾는 것이 성능 문제 야기할 수 있음. 맞는 규칙을 찾는 과정이 실상 쓸모가 없거나 다른 길을 찾아야 하는 복잡한 구조가 될 수 있음.
    예를 들어 ` div div div div {…}` 이 선택자는 3번째 자손<div>에 규칙을 적용한다는 뜻. 규칙을 적용할 div 요소를 확인하려면 트리로부터 임의의 줄기를 선택하고 탐색하는 과정에서 규칙에 맞지 않는 줄기를 선택했다면 또 다른 줄기를 선택해야 함.
  3. 규칙을 적용하는 것은 계층 구조를 파악해야 하는 꽤나 복잡한 다단계 규칙을 수반한다.

스타일 정보 공유

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

  1. 동일한 마우스 반응 상태를 가진 요소여야 함. 예를 들어 한 요소가 :hover 상태가 될 수 없는데 다른 요소는 :hover가 될 수 있다면 동일한 마우스 상태가 아님.
  2. 아이디가 없는 요소
  3. 태그 이름이 일치해야 함
  4. 클래스 속성이 일치해야 함
  5. 지정된 속성이 일치해야 함
  6. link 상태가 일치해야 함
  7. focus 상태가 일치해야 함
  8. 문서 전체에서 속성 선택자의 영향을 받는 요소가 없어야 함.
  9. 요소에 인라인 스타일 속성이 없어야 함
  10. 문서 전체에서 형제 선택자를 사용하지 않아야 함. 웹 코어는 형제 선택자를 만나면 전역 스위치를 열고 전체 문서의 스타일 공유를 중단함. 형제 선택자는 + 선택자와 :first-child 그리고 :last-child를 포함.

파이어 폭스 규칙 트리

파이어 폭스는 스타일 계산을 쉽게 처리하기 위해 규칙 트리와 스타일 문맥 트리라고 하는 두 개의 트리를 더 가지고 있다. 웹킷도 스타일 객체를 가지고 있지만 스타일 문맥 트리처럼 저장되지 않고 오직 DOM 노드로 관련 스타일을 처리.
스타일 문맥에는 최종 값이 저장되어 있음. 값은 올바른 순서 안에서 규칙을 적용하고 구체적인 값으로 변환하면서 계산됨. 논리적인 값이 화면의 백문율이라면 이 값은 절대적인 단위(px)로 변환됨. 부합하는 모든 규칙은 트리에 저장하는데 경로의 하위 노드가 높은 우선순위를 갖는다. 규칙 저장은 느리게 처리됨. 트리는 처음부터 모든 노드를 계산하지 않지만 노드 스타일이 계산될 필요가 있을 때 계산된 경로를 트리에 추가한다.
트리가 작업량을 줄이는 방법을 살펴보자.

구조체로 분리

스타일 문맥은 구조체로 나뉘는데 선 또는 색상과 같은 종류의 스타일 정보를 포함. 구조체의 속성들은 상속되거나 상속되지 않음. 속성들은 요소에 따라 한 부모로부터 상속됨. 상속되지 않은 속성들은 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이 된다. 이것은 요소가 사용할 수 있는 트리 경로가 이미 존재한다는 것을 의미하고 규칙6에 이르는 또 다른 노드를 문맥 트리에 추가하면 된다. 스타일 문맥을 생성하고 문맥 트리에 추가하면 새로운 스타일 문맥이 규칙 트리의 F:6 노드를 가리킨다.
스타일 구조체를 채우는데 여백 구조체를 채우는 것부터 시작. 마지막 규칙 노드 F:6이 여백 구조체를 포함하지 않기 때문에 거슬러 올라가 계산된 값을 사용. 여백 규칙이 선언된 최상위 노드의 구조체를 규칙 노드 B:1에서 찾았다.
색상 구조체 정의에는 저장된 구조체를 사용할 수 없음. 색상은 이미 하나의 속성 값을 가지고 있기 때문에 다른 값을 채우기 위해 규칙 트리 상단으로 거슬러 올라갈 필요가 없음. 최종 값을 계산하고 계산된 값을 이 노드에 저장할 것이다.
두 번째 요소는 보다 수월하게 진행된다. 맞는 규칙을 찾다 보면 이전 span과 같이 규칙 트리의 G:3를 가리킨다는 결론에 이르는데 동일한 노드를 가리키는 형제가 있기 때문에 전체 스타일 문맥을 공유하고 이전 span 문맥을 취한다.
부모로부터 상속된 규칙을 포함하고 있는 구조체의 저장은 문맥 트리에서 처리된다. 색상 속성은 실제로 상속된다. 그러나 파이어폭스는 재설정으로 처리해서 규칙 트리에 저장한다.
규칙 트리가 없는 웹킷은 선언이 일치하는 규칙이 4번 탐색된다. 우선 중요하지 않은 상위속성이 적용되고, 그 다음 중요한 상위 속성이 적용된다. 그리고 중요하지 않은 일반 속성, 마지막으로 중요한 일반 속성이 적용된다. 이것은 여러 번 나타나는 속성들이 정확한 다단계 순서에 따라 결정된다는 것을 의미하고 가장 마지막 값이 적용된다.

쉬운 선택을 위한 규칙 다루기

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

  • css 규칙을 외부 스타일 시트에서 선언하거나 style 요소에서 선언 p {color:blue}
  • 인라인 스타일 속성 ` <p style="color:blue"></p> `
  • html의 시각적 속성 <p bgcolor="blue"></p> 마지막 두가지는 자신이 스타일 속성을 가지고 있거나 html 속성을 이용하여 연결할 수 있기에 요소에 쉽게 연결됨.
    위에 언급한 문제 2번에 따라 css 규칙은 연결하는 것은 까다로울 수 있음. 그래서 이를 해결하려면 쉽게 접근할 수 있도록 규칙을 교묘하게 처리해야함.
    스타일시트 파싱 후 규칙은 선택자에 따라 여러 해시맵 중 하나에 추가됨. 아이디, 클래스 이름, 태그 이름을 사용한 맵이 있고 이런 분류에 맞지 않는 것을 위한 일반적인 맵이 있음. 선택자가 아이디인 경우 규칙은 아이디 맵에 추가되고 선택자가 클래스인 경우 규칙은 클래스 맵에 추가됨.
    이런 처리 작업으로 규칙을 찾는 일은 쉬워짐. 맵에서 특정 요소와 관련 있는 규칙을 추출할 수 있기 때문에 모든 선언을 찾아볼 필요가 없음. 이런 최적화는 찾아야 할 규칙의 95% 이상을 제거하기 때문에 규칙을 찾는 동안 모든 선언을 고려할 필요 없음.
    p.error {color:red}  
    #messageDiv {height:50px}
    div {margin:5px}  
    

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

an error occurred

this is a message
위와 관련된 html 코드.  
p 요소의 규칙. 클래스 맵은 "p.error"를 위한 규칙 하부의 "error" 키를 찾음. div 요소는 아이디 맵과 태그 맵에 관련 규칙이 있음. 그러므로 이제 남은 작업은 키를 사용하여 추출한 규칙 중 실제로 일치하는 규칙을 찾는 것.  
div에 해당하는 다음과 같은 규칙이 있다고 가정하자.  

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

다단계 순서에 따라 규칙 적용하기

스타일 객체는 모든 css 속성을 포함하고 있다. 어떤 규칙과도 일치하지 않는 일부 속성은 부모 요소의 스타일 객체로부터 상속받는다. 그 외 다른 속성들은 기본 값으로 설정된다.
문제는 하나 이상의 속성이 정의될 때 시작되고 다단계 순서가 이 문제를 해결하게 됨.

스타일 시트 다단계 순서

스타일 속성 선언은 여러 스타일 시트에서 나타날 수 있고 하나의 스타일 시트 안에서도 여러 번 나타날 수 있는데 이것은 규칙을 적용하는 순서가 매우 중요하다는 것을 의미. 이것을 다단계(cascade) 순서라고 한다. css2 명세에 따른 다단계 순서 (우선 순위가 낮은 것에서 높은 순서)

  1. 브라우저 선언
  2. 사용자 일반 선언
  3. 저작자 일반 선언
  4. 저작자 중요 선언
  5. 사용자 중요 선언

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

특정성

  • 선택자 없이 ‘style’ 속성이 선언된 것이면 1을 센다. 그렇지 않으면 0을 센다 (a)
  • 선택자에 포함된 아이디 선택자 개수를 센다 (b)
  • 선택자에 포함된 속성 선택자와 가상 클래스 선택자의 숫자를 센다. (c)
  • 선택자에 포함된 요소 선택자와 가상 요소 선택자의 숫자를 센다. (d)

네 개의 연결된 숫자 a-b-c-d를 연결하면 특정성의 값이 됨.
사용할 진법은 분류 중에 가장 높은 숫자에 의해 정의됨. a=14이면 16 진수를 사용할 것.

규칙 정렬

맞는 규칙을 찾으면 다단계 규칙에 따라 정렬된다. 웹킷은 목록이 적으면 버블 정렬을 사용하고 목록이 많을 때는 병합 정렬을 사용한다. 웹킷은 규칙에 “>” 연산자를 덮어쓰는 방식으로 정렬을 실행한다.

점진적 처리

웹킷은 @import를 포함한 최상위 수준의 스타일 시트가 로드되었는지 표시하기 위해 플래그를 사용. DOM 노드와 시각정보를 연결하는 과정에서 스타일이 완전히 로드되지 않았다면 문서에 자리 표시자를 사용하고 스타일 시트가 로드됐을 때 다시 계산.

배치

렌더러가 생성되어 트리에 추가될 때 크기와 위치 정보는 없는데 이런 값을 계산하는 것을 배치 또는 리플로라고 부름.
html은 흐름 기반의 배치 모델을 사용하는데 이것은 보통 단일 경로를 통해 크기와 위치 정보를 계산할 수 있다는 것을 의미. 배치는 왼쪽에서 오른쪽, 또는 위에서 아래로 흐른다. 단, 표는 크기와 위치를 계산하기 위해 하나 이상의 경로를 필요로 하기에 예외가 됨.
좌표계는 기준점으로부터 상대의 위치를 결정하는데 X축과 Y축 좌표를 사용한다.
배치는 반복되며 HTML 문서의 <html> 요소에 해당하는 최상위 렌더러에서 시작한다. 배치는 프레임 계층의 일부 또는 전부를 통해 반복되고 각 렌더러에 필요한 크기와 위치 정보를 계산한다.
최상위 렌더러의 위치는 0,0이고 브라우저 창의 보이는 영역에 해당하는 뷰포트 만큼의 면적을 갖는다.
모든 렌더러는 “배치” 또는 “리플로” 메서드를 갖는데 각 렌더러는 배치해야 할 자식의 배치 메소드를 불러온다.

더티 비트 체제

소소한 변경 때문에 전체를 다시 배치하지 않기 위해 브라우저는 “더티 비트” 체제를 사용한다. 렌더러는 다시 배치할 필요가 있는 변경 요소 또는 추가된 것과 그 자식을 “더티”라고 표시한다.
“더티”와 “자식이 더티” 두 가지 플래그가 있음. 자식이 더티하다는 것은 본인은 괜찮지만 자식 중 적어도 하나를 다시 배치할 필요가 있다는 의미.

전역 배치와 점증 배치

배치는 렌더러 트리 전체에서 일어날 수 있는데 이것을 “전역” 배치라 하고 다음과 같은 경우에 발생

  1. 글꼴 크기 변경과 같이 모든 렌더러에 영향을 주는 전역 스타일 변경
  2. 화면 크기 변경에 의한 결과

배치는 더티 렌더러가 배치되는 경우에만 점증되는데 추가적인 배치가 필요하기 때문에 약간의 손실이 발생할 수 있다.
점증 배치는 렌더러가 더티일 때 비동기적으로 일어남. 예를 들어 네트워크로부터 추가 내용을 받아서 DOM 트리에 더해진 다음 새로운 렌더러가 렌더 트리에 붙을 때.

비동기 배치와 동기 배치

점증 배치는 비동기로 실행됨. 파이어폭스는 점증 배치를 위해 “리플로 명령”을 쌓아 놓고 스케줄러는 이 명령을 한꺼번에 실행함. 웹킷도 점증 배치를 실행하는 타이머가 있는데 트리를 탐색하여 “더티” 렌더러를 배치함.
“offsetHeight” 같은 스타일 정보를 요청하는 스크립트는 동기적으로 점증 배치를 실행한다.
전역 배치는 보통 동기적으로 실행됨.

최적화

배치가 “크기 변경” 또는 렌더러 위치 변화 때문에 실행되는 경우 렌더러의 크기는 다시 계산하지 않고 캐시로부터 가져온다.
어떤 경우는 하위트리만 수정되고 최상위로부터 배치가 시작되지 않는 경우도 있음. 이런 경우는 입력 필드에 텍스트를 입력하는 경우와 같이 변화 범위가 한정적이어서 주변에 영향을 미치지 않을 때 발생. 만약 입력 필드 바깥쪽에 텍스트가 입력되는 경우라면 배치는 최상단으로부터 시작이 될 것.

배치 과정

배치는 보통 다음과 같은 형태로 진행됨

  1. 부모 렌더러가 자신의 너비를 결정
  2. 부모가 자식을 검토
    1. 자식 렌더러를 배치 (자식의 x와 y를 설정)
    2. (부모와 자식이 더티하거나 전역 배치 상태이거나 또는 다른 이유로) 필요하다면 자식 배치를 호출하여 자식의 높이를 계산함.
  3. 부모는 자신의 누적된 높이와 여백, 패딩을 사용하여 자신의 높이를 설정함. 이 값은 부모 렌더러의 부모가 사용하게 됨.
  4. 더티 비트 플래그를 제거한다.

파이어폭스는 “상태” 객체를 배치를 위한 매개변수로 사용하는데 상태는 부모의 너비를 포함한다.
파이어폭스 배치의 결과는 매트릭스 객체인데 높이가 계산된 렌더러를 포함한다.

너비 계산

렌더러의 너비는 포함하는 블록의 너비, 그리고 렌더러의 너비와 여백, 테두리를 이용하여 계산됨
예를 들어 ` <div style="width:30%"></div> `
웹킷은 다음과 같이 계산할 것.

  • 컨테이너의 너비는 컨테이너 availableWidth와 0 사이의 최대값. 이 경우 availableWidth는 다음과 같이 계산된 contentWidth이다.
    clientWidth() - paddingLeft() - paddingRight()
    clientWidth와 clientHeight는 객체의 테두리와 스크롤바를 제외한 내부 영역을 의미한다.
  • 요소의 너비는 “width” 스타일 속성의 값이다. 이 컨테이너 너비의 백분률 값은 절대 값으로 변환될 것이다
  • 좌우측 테두리와 패딩 값이 추가된다.

줄 바꿈

렌더러가 배치되는 동안 줄을 바꿀 필요가 있을 때 배치는 중단되고 줄 바꿀 필요가 있음을 부모에게 전달한다. 부모는 추가 렌더러를 생성하고 배치를 호출한다.

그리기

그리기 단계에서는 화면에 내용을 표시하기 위한 렌더 트리가 탐색되고 렌더러의 “paint” 메서드가 호출된다. 그리기는 UI 기반의 구성 요소를 사용한다.

전역과 점증

그리기는 배치와 마찬가지로 전역 또는 점증 방식으로 수행된다. 점증 그리기에서 일부 렌더라는 전체 트리에 영향을 주지 않는 방식으로 변경된다. 변경된 렌더러는 화면 위의 사각형을 무효화 하는데 OS는 이것을 “더티 영역”으로 보고 “paint” 이벤트를 발생시킨다. OS는 몇 개의 영역을 하나로 합치는 방법으로 효과적으로 처리한다. 크롬은 렌더러가 별도의 처리 과정이기 때문에 더 복잡. 크롬은 OS의 동작을 어느 정도 모방. 프레젠테이션은 이런 이벤트에 귀 기울이고 렌더 최상위로 메시지를 전달. 그러면 트리는 적절한 렌더러에 이를 때까지 탐색되고 스스로 다시 그려짐.

그리기 순서

실제로 요소가 stacking contents에 쌓이는 순서.

  1. 배경색
  2. 배경 이미지
  3. 테두리
  4. 자식
  5. 아웃라인

파이어폭스 표시 목록

파이어폭스는 렌더 트리를 검토하고 그려진 사각형을 위한 표시 목록을 구성. 목록은 올바른 그리기 순서에 따라 사각형을 위한 적절한 렌더러를 포함. 이런 방법으로 트리는 여러 번 리페인팅을 실행하는 대신 한 번만 탐색하면서 배경색, 배경 이미지, 테두리, 나머지 순으로 그려냄.
파이어폭스는 다른 불투명 요소 뒤에 완전히 가려진 요소는 추가하지 않는 방법으로 최적화를 진행한다.

웹킷 사각형 저장소

리페인팅 전에 웹킷은 기존의 사각형을 비트맵으로 저장, 새로운 사각형과 비교하고 차이가 있는 부분만 다시 그림.

동적 변경

브라우저는 변경에 가능한 한 최소한의 동작으로 반응하려고 노력. 그렇기에 요소의 색깔이 바뀌면 해당 요소의 리페인팅만 발생. 요소의 위치가 바뀌면 요소와 자식 그리고 형제의 리페인팅과 재배치가 발생. DOM 노드를 추가하면 노드의 리페인팅과 재배치가 발생. “html” 요소의 글꼴 크기를 변경하는 것과 같은 큰 변경은 캐시를 무효화하고 트리 전체의 배치와 리페인팅이 발생.

렌더링 엔진의 스레드

렌더링 엔진은 통신을 제외한 거의 모든 경우에 단일 스레드로 동작. 파이어폭스와 사파리의 경우 렌더링 엔진의 스레드는 브라우저의 주요한 스레드에 해당. 크롬에서는 이것이 탭 프로세서의 주요 스레드.
(스레드: 어떠한 프로그램 내에서 실행되는 흐름의 단위)

이벤트 순환

브라우저의 주요 스레드는 이벤트 순환으로 처리 과정을 유지하기 위해 무한 순환. 배치와 그리기 같은 이벤트를 위해 대기하고 이벤트를 처리한다.

CSS2 시각 모델

캔버스

CSS2 명세는 캔버스를 “서식 구조가 표현되는 공간”이라고 설명. 브라우저가 내용을 그리는 곳. 캔버스 공간 각각의 면적은 무한하지만 브라우저는 뷰포트의 크기를 기초로 초기 너비를 결정.
캔버스는 기본적으로 투명하기에 다른 캔버스와 겹치는 경우 비쳐 보이고, 투명하지 않을 경우에는 브라우저에서 정의한 색이 지정됨.

CSS 박스 모델

CSS 박스 모델은 문서 트리에 있는 요소를 위해 생성되고 시각적 서식 모델에 따라 배치된 사각형 박스를 설명.
각 박스는 콘텐츠 영역과 선택적인 패딩, 테두리, 여백이 있다.
모든 요소는 만들어질 박스의 유형을 결정하는 “display” 속성을 갖는데 이 속성의 유형은 다음과 같다.

  • block - 블록 상자를 만든다
  • inline - 하나 또는 그 이상의 인라인 상자를 만든다
  • none - 박스를 만들지 않는다

기본 값은 인라인이지만 브라우저의 스타일 시트는 다른 기본 값을 설정. “div” 요소의 display 속성 기본 값은 block이다.

위치 결정 방법

  1. Normal - 객체는 문서 안의 자리에 따라 위치가 결정됨. 이것은 렌더 트리에서 객체의 자리가 DOM 트리의 자리와 같고 박스 유형과 면적에 따라 배치됨을 의미.
  2. Float - 객체는 일반적인 흐름에 따라 배치된 다음 왼쪽이나 오른쪽으로 흘러 이동한다.
  3. Absolute - 객체는 DOM 트리 자리와는 다른 렌더 트리에 놓인다.

위치는 “position” 속성과 “float” 속성에 의해 결정됨

  • static과 relative로 설정하면 일반적인 흐름에 따라 위치가 결정된다
  • absolute와 fixed로 설정하면 절대적인 위치가 된다

position 속성을 정의하지 않으면 static이 기본 값이 되며 일반적인 흐름에 따라 위치가 결정됨. static이 아닌 다른 속성 값을 사용하면 top, bottom, left, right 속성으로 위치를 결정할 수 있음.

박스가 배치되는 방법은 다음과 같은 방법으로 결정됨

  • 박스 유형 (display, inline…)
  • 박스 크기 (width, height…)
  • 위치 결정 방법 (position, float)
  • 추가적인 정보 - 이미지 크기와 화면 크기 등

박스 유형

  • 블록 박스: 브라우저 창에서 사각형 블록을 형성
  • 인라인 박스: 블록이 되지 않고 블록 내부에 포함된다 블록은 다른 블록 아래 수직으로 배치되고 인라인은 수평으로 배치된다.
    inlineBlock 인라인 박스는 라인 또는 라인 박스 안쪽에 놓임. 라인은 적어도 가장 큰 박스만큼 크지만 “baseline”정렬일 때 더 커질 수 있다. 이것은 요소의 하단이 다른 상자의 하단이 아닌 곳에 배치된 경우를 의미한다. 포함하는 너비가 충분하지 않으면 인라인은 몇 줄의 라인으로 배치되는데 이것은 보통 문단 안에서 발생한다.

위치 잡기

상대적인 위치

상대적인 위치 잡기는 일반적인 흐름에 따라 위치를 결정한 다음 필요한 만큼 이동한다.
image

플로트

플로트 박스는 라인의 왼쪽 또는 오른쪽으로 이동. 흥미로운 점은 다른 박스가 이 주변을 흐른다는 것.

absolute 위치와 fixed 위치

절대와 고정 배치는 일반적인 흐름과 무관하게 결정되고, 일반적인 흐름에 관여하지 않으며, 면적은 부모에 따라 상대적이다. 고정인 경우 뷰포트로부터 위치를 결정한다.

층 표현

css의 z-index 속성에 의해 명시됨. 층은 박스의 3차원 표현이고 “z축”을 따라 위치를 정함.
박스는 스택으로 구분됨. 각 스택에서 뒤쪽 요소가 먼저 그려지고 앞쪽 요소는 사용자에게 가까운 쪽으로 나중에 그려짐. 가장 앞쪽에 위치한 요소는 겹치는 이전 요소를 가린다.
스택은 z-index 속성에 따라 순서를 결정. z-index 속성이 있는 박스는 local stack을 형성. 뷰포트는 바깥쪽의 스택이다.


읽다가 중간에 이해 안되는 내용도 있었지만, 일단 훑어보자는 생각으로 다 정리했다.
몇 번 더 읽어봐서 다 이해할 수 있게 해야겠다.