앞 포스팅에서 웹브라우저의 구조와 웹브라우저의 렌더링 엔진에 대해 알아봤다.
렌더링 엔진은 웹페이지를 화면에 표시하는 프로세스를 담당하며 이 과정에서 파서(Parser)가 중요한 역할을 하게 된다. 이번 포스팅에서는 렌더링 엔진의 파서에 대해 설명한다.
파싱은 렌더링 엔진에서 매우 중요한 과정이기 때문에 더 자세히 다룰 필요가 있다. 먼저 파싱에 대해 간단히 알아보자.
문서 파싱은 브라우저가 코드를 이해하고 사용할 수 있는 구조로 변환하는 것을 의미한다. 파싱 결과는 보통 문서구조를 나타내는 노드 트리인데 파싱 트리(parse tree) 또는 문법 트리(syntax tree)라고 부른다.
예를 들면 2+3-1과 같은 표현식은 다음과 같은 트리가 된다.
(수학 표현식을 파싱한 트리 노드)
파싱은 문서에 작성된 언어 또는 형식의 규칙에 따르는데 파싱할 수 있는 모든 형식은 정해진 용어와 구문 규칙에 따라야 한다. 이것을 문맥 자유 문법이라고 한다.
파싱 과정은 크게 두 단계로 이루어진다.
이렇게 파싱을 통해 언어의 구조와 의미를 분석할 수 있다.
(문서 소스로부터 파싱 트리를 만드는 과정)
파싱 과정은 반복된다. 파서는 보통 어휘 분석기로부터 새 토큰을 받아서 구문 규칙과 일치하는지 확인한다. 규칙에 맞으면 토큰에 해당하는 노드가 파싱 트리에 추가되고 파서는 또 다른 토큰을 요청한다.
규칙에 맞지 않으면 파서는 토큰을 내부적으로 저장하고 토큰과 일치하는 규칙이 발견될 때까지 요청한다. 맞는 규칙이 없는 경우 예외로 처리하는데 이것은 문서가 유효하지 않고 구문 오류를 포함하고 있다는 의미다.
파서 트리는 최종 결과물이 아니다. 파싱은 보통 문서를 다른 양식으로 변환하는데 컴파일이 하나의 예가 된다. 소스 코드를 기계 코드로 만드는 컴파일러는 파싱 트리 생성 후 이를 기계 코드 문서로 변환한다.
HTML 파서(HTML Parser)는 웹 브라우저에서 사용되는 특수한 종류의 파서로, HTML 문서를 읽고 이를 DOM(Document Object Model) 트리로 변환하는 역할을 수행한다다.
안타깝게도 모든 전통적인 파서는 HTML에 적용할 수 없다. 그럼에도 불구하여 지금까지 파싱을 설명한 것이 무의미한 것은 아니다. 파싱은 CSS와 자바스크립트를 파싱하는 데 사용된다. HTML은 파서가 요구하는 문맥 자유 문법에 의해 쉽게 정의할 수 없다.
언뜻 이상하게 보일 수도 있는데 HTML이 XML과 유사하기 때문이다. 사용할 수 있는 XML 파서는 많다. HTML을 XML 형태로 재구성한 XHTML도 있는데 무엇이 큰 차이점일까?
차이점은 HTML이 더 "너그럽다"는 점이다. HTML은 암묵적으로 태그에 대한 생략이 가능하다. 가끔 시작 또는 종료 태그 등을 생략한다. 전반적으로 뻣뻣하고 부담스러운 XML에 반하여 HTML은 "유연한" 문법이다. (어떻게 유연한지는 아래에 브라우저의 오류 처리를 보면 알 수 있다)
결론적으로 HTML은 파싱하기 어렵고 전통적인 구문 분석이 불가능하기 때문에 문맥 자유 문법이 아니라는 것이다. XML 파서로도 파싱하기 쉽지 않다.
HTML 파서의 작동 과정은 크게 토큰화(Tokenization)와 트리 구축(Tree construction) 두 단계로 나뉩니다. 각 단계에서 사용되는 알고리즘은 다음과 같다.
- 토큰화 알고리즘(Tokenization algorithm)
- HTML 문서를 읽고, 여는 태그, 닫는 태그, 속성, 주석, 텍스트 콘텐츠 등과 같은 의미 있는 원자 단위인 토큰으로 분해하는 역할
- HTML 파서는 문자열을 순차적으로 읽으면서 특정 문자 또는 문자열 패턴에 따라 토큰을 생성
- 예를 들어, '<' 문자를 만나면 여는 태그가 시작되었다고 판단하고, 태그의 이름과 속성을 추출
- 이 과정은 상태 기반 유한 오토마타(Finite state automata)를 사용하여 구현
- 여기서 각 상태는 다음 토큰의 유형을 결정하는 데 도움이 됨
- 트리 구축 알고리즘(Tree construction algorithm)
- 토큰화 단계에서 생성된 토큰들을 사용하여 DOM 트리를 생성하는 역할
- HTML 파서는 토큰들을 순차적으로 처리하면서, 현재 토큰에 따라 트리를 조작
- 이 과정에서 HTML 파서는 "Insertion mode"라는 다양한 상태를 사용하여 트리 구축을 수행
- 각 삽입 모드는 다음 노드를 삽입하는 방법을 정의하며, 트리의 현재 위치와 토큰의 유형에 따라 변화
HTML 파서는 트리 구축 알고리즘 동안 오류 복구 및 수정을 수행한다. 이는 HTML 문서가 일부 오류나 누락된 태그를 포함하더라도 웹 페이지를 올바르게 렌더링할 수 있도록 도와주게 된다. 이를 위해 파서는 복잡한 규칙 및 알고리즘을 사용하여 가능한 한 최선의 결과를 도출한다.
HTML 파서는 HTML 문서가 일부 오류나 누락된 태그를 포함하더라도, 웹 페이지를 올바르게 렌더링하기 위해 오류를 자동으로 교정하는 기능을 가지고 있다. 이를 "오류 복구(Error Recovery)" 또는 "오류 수정(Error Correction)"이라고 한다. 이러한 기능 덕분에 웹 브라우저는 구문 오류가 있는 HTML 문서를 사용자에게 제공할 수 있다.
HTML 파서는 복잡한 규칙 및 알고리즘을 사용하여 오류를 수정하거나 무시하려고 시도합니다.
1. 누락된 태그 예시
HTML 파서는 종종 여는 태그 또는 닫는 태그가 누락된 경우를 처리합니다. 예를 들어, 다음과 같은 코드가 있을 때
<ul>
<li>Item 1
<li>Item 2
</ul>
여기서 각 <li>
태그에 대한 닫는 태그가 누락되었습니다. HTML 파서는 자동으로 필요한 닫는 태그를 삽입하여 문서를 올바르게 해석
2. 중첩 오류 예시
HTML에서 일부 태그는 다른 태그 내부에 중첩될 수 없습니다. 예를 들어, 다음과 같은 코드가 있을 때
<b>bold text<i>bold and italic text</b>italic text</i>
여기서 <i>
태그는 <b>
태그 내부에 중첩되었습니다. 이러한 중첩은 올바르지 않으므로, HTML 파서는 중첩 오류를 수정하여 문서를 올바르게 해석
3. 속성 오류 예시
속성 값에 따옴표가 누락되거나 유효하지 않은 속성 이름이 사용된 경우, HTML 파서는 오류를 수정하려고 시도합니다. 예를 들어, 다음과 같은 코드가 있을 때
<input type=text size=10 disabled>
여기서 속성 값에 따옴표가 누락됨. HTML 파서는 이를 처리하여 문서를 올바르게 해석하려고 시도
HTML과는 다르게 CSS는 문맥 자유 문법이고 소개 글에서 설명했던 파서 유형을 이용하여 파싱이 가능하다. 실제로 CSS 명세는 CSS 어휘와 문법을 정의하고 있다.
두 경우 모두 각 CSS 파일은 스타일 시트 객체로 파싱되고 각 객체는 CSS 규칙을 포함한다. CSS 규칙 객체는 선택자와 선언 객체 그리고 CSS 문법과 일치하는 다른 객체를 포함한다.
(CSS 파싱)
웹은 파싱과 실행이 동시에 수행되는 동기화(synchronous) 모델이다. 제작자는 파서가 <script>
태그를 만나면 즉시 파싱하고 실행하기를 기대한다. 스크립트가 실행되는 동안 문서의 파싱은 중단된다.
스크립트가 외부에 있는 경우 우선 네트워크로부터 자원을 가져와야 하는데 이 또한 실시간으로 처리되고 자원을 받을 때까지 파싱은 중단된다. 이 모델은 수 년간 지속됐고 HTML4와 HTML5의 명세에도 정의되어 있다. 제작자는 스크립트를 "지연(defer)"으로 표시할 수 있는데 지연으로 표시하게 되면 문서 파싱은 중단되지 않고 문서 파싱이 완료된 이후에 스크립트가 실행된다.
HTML5는 스크립트를 비동기(asynchronous)로 처리하는 속성을 추가했기 때문에 별도의 맥락에 의해 파싱되고 실행된다.
웹킷과 파이어폭스는 예측 파싱과 같은 최적화를 지원한다. 스크립트를 실행하는 동안 다른 스레드는 네트워크로부터 다른 자원을 찾아 내려받고 문서의 나머지 부분을 파싱한다.
이런 방법은 자원을 병렬로 연결하여 받을 수 있고 전체적인 속도를 개선한다.
참고로 예측 파서는 DOM 트리를 수정하지 않고 메인 파서의 일로 넘긴다. 예측 파서는 외부 스크립트, 외부 스타일 시트와 외부 이미지와 같이 참조된 외부 자원을 파싱할 뿐이다.
한편 스타일 시트는 다른 모델을 사용한다. 이론적으로 스타일 시트는 DOM 트리를 변경하지 않기 때문에 문서 파싱을 기다리거나 중단할 이유가 없다.
그러나 스크립트가 문서를 파싱하는 동안 스타일 정보를 요청하는 경우라면 문제가 된다. 스타일이 파싱되지 않은 상태라면 스크립트는 잘못된 결과를 내놓기 때문에 많은 문제를 야기한다. 이런 문제는 흔치 않은 것처럼 보이지만 매우 빈번하게 발생한다.
브라우저가 HTML, CSS, JavaScript 등의 웹 페이지를 불러올 때, 먼저 브라우저 파싱 엔진이 해당 파일을 읽고 해석한다. 이때, HTML 파일은 HTML 파서, CSS 파일은 CSS 파서, JavaScript 파일은 JavaScript 파서가 각각 해석하게 된다.
HTML 파서는 HTML 파일의 문법을 검사하고, 트리 구조로 변환한 후 DOM 트리를 생성.
이때, HTML 파일 내에 있는 각 요소(element)들은 노드(node)로 변환된다. 노드는 다시 엘리먼트 노드, 텍스트 노드, 주석 노드 등으로 나눌 수 있다.
CSS 파서는 CSS 파일의 문법을 검사하고, 스타일 규칙을 파싱.
이때, CSS 규칙은 선택자(selector), 속성(property), 값(value)으로 구성된다. CSS 파서는 이 규칙들을 해석하여 스타일 규칙을 생성하고, 스타일 규칙을 적용할 요소를 결정한다다.
JavaScript 파서는 JavaScript 파일의 문법을 검사하고, AST(Abstract Syntax Tree) 구조로 변환한 후 JavaScript 엔진에 의해 실행.
이때, 변수 선언, 함수 선언 등을 처리하고, 실행 가능한 코드로 변환한다.
이렇게 브라우저 파서는 웹 페이지를 불러오고 해석하여 DOM 트리와 스타일 규칙, 실행 가능한 코드를 생성한다. 이후, 이를 바탕으로 렌더링 엔진이 웹 페이지를 화면에 그리게 된다.