HTML Parser
토크나이저와 파서
HTML 문자열을 토큰화하고, 토큰을 DOM 노드로 변환하며 트리를 조립한다.
HTML 파서는 서버가 보낸 바이트 스트림을 브라우저가 다룰 수 있는 DOM 트리로 변환하는 컴포넌트로, WHATWG 스펙상 크게 두 단계로 나뉜다. 먼저 바이트→문자 디코딩(byte-to-character decoding) 단계에서 Content-Type의 charset이나 BOM, meta 태그를 근거로 인코딩(주로 UTF-8)을 결정해 바이트를 유니코드 문자로 바꾼다. 그다음 토크나이저(tokenizer)가 문자 스트림을 상태 기계(state machine)로 훑어 시작 태그·종료 태그·텍스트·주석 같은 토큰(token)을 뽑아내고, 트리 구성(tree construction) 단계가 그 토큰들을 받아 삽입 모드(insertion mode) 규칙에 따라 노드를 만들어 DOM 트리에 붙인다. 이 과정에서 `<script>`를 만나면(async/defer가 아닌 한) 스크립트가 DOM을 바꿀 수 있으므로 트리 구성이 멈추고 스크립트를 즉시 실행한다. 이 블로킹으로 인한 낭비를 줄이기 위해 브라우저는 프리로드 스캐너(preload scanner, Firefox는 speculative parser)를 따로 돌려, 파서가 막혀 있는 동안에도 뒤쪽 HTML을 미리 훑어 img·link·script 등 리소스를 병렬로 선다운로드한다. defer는 파싱을 막지 않고 문서 파싱이 끝난 뒤 순서대로 실행하며, async는 다운로드가 끝나는 즉시 순서 무관하게 실행되어 파싱을 중단시킬 수 있다.
내부 구성
핵심 포인트
- 파싱은 바이트→문자 디코딩 → 토크나이저(토큰화) → 트리 구성 → DOM 생성의 파이프라인으로 진행된다
- 토크나이저는 상태 기계로 문자 스트림을 훑어 시작/종료 태그·텍스트·주석 토큰을 생성한다
- 트리 구성 단계는 insertion mode 규칙에 따라 토큰을 노드로 만들고 DOM 트리에 삽입하며 오류 복구(error recovery)도 수행한다
- 동기 `<script>`는 DOM을 변경할 수 있어 파서를 블로킹하고 즉시 실행되며, CSSOM이 필요하면 CSS 다운로드까지 기다린다
- 프리로드 스캐너(speculative parsing)는 파서가 막힌 동안 앞쪽 HTML을 미리 훑어 리소스를 병렬 프리페치한다
- defer는 파싱을 막지 않고 파싱 완료 후 순서대로, async는 다운로드 완료 즉시 순서 무관하게 실행된다
- document.write는 파서 스트림에 바이트를 주입해 프리로드 스캐너의 추측 작업을 무효화하므로 성능에 해롭다
심화
가장 자주 나오는 심화 질문은 'HTML 파서는 왜 XML 파서와 달리 절대 실패하지 않는가'이다. HTML 파싱 스펙은 잘못된 마크업(닫히지 않은 `<p>`, 잘못 중첩된 `<b><i></b></i>` 등)에 대해 예외를 던지는 대신, 모든 경우에 대한 결정적(deterministic) 오류 복구 규칙과 삽입 모드 전이를 명시한다. 그래서 어떤 브라우저든 같은 깨진 HTML로부터 동일한 DOM을 만들어야 하며, 이것이 웹 호환성의 근간이다. adoption agency algorithm(잘못 중첩된 포매팅 태그 처리)이 그 대표적 예로, 면접에서 '왜 이런 태그가 이렇게 파싱되냐'를 물으면 이 복구 규칙을 언급하면 좋다. 또 하나의 깊은 지점은 '토크나이저와 트리 구성기가 완전히 분리되어 있지 않다'는 사실이다. HTML은 문맥 의존적이라 `<script>`, `<style>`, `<textarea>` 안에서는 토크나이저가 raw text/RCDATA 모드로 전환되어 태그를 태그로 보지 않는다. 즉 트리 구성 단계가 토크나이저의 상태를 바꾼다(재진입적 결합). 실무 성능 관점에서 결정적인 것은 프리로드 스캐너의 존재다. `<head>`의 CSS를 늦게 두거나 렌더 블로킹 스크립트를 상단에 두면 파서는 막히지만 프리로드 스캐너가 뒤 리소스를 미리 긁어오므로 완전한 정지는 아니다. 그러나 document.write로 마크업을 주입하면 스캐너가 미리 읽은 스트림이 무효가 되어 추측 작업이 버려지므로, 이것이 document.write가 안티패턴인 근본 이유다.
쉽게 말하면 긴 문장을 단어(토큰)로 끊어 읽으며 문장 구조(트리)를 그려나가는 것.