탐색으로 돌아가기
Browser10 / 24 단계

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는 다운로드가 끝나는 즉시 순서 무관하게 실행되어 파싱을 중단시킬 수 있다.

내부 구성

바이트 스트림 디코더 (Byte→Character Decoding)
Content-Type charset·BOM·meta를 근거로 인코딩을 결정해 원시 바이트를 유니코드 문자 스트림으로 변환한다
입력 스트림 전처리 (Input Stream Preprocessing)
개행 정규화(CRLF→LF), 잘못된 문자 처리 등 토크나이저에 넘기기 전 문자 스트림을 정돈한다
토크나이저 (Tokenizer)
문자 스트림을 상태 기계로 훑어 시작태그·종료태그·텍스트·주석·DOCTYPE 등의 토큰을 방출한다
트리 구성기 (Tree Construction)
토큰을 받아 insertion mode 규칙에 따라 노드를 생성하고 open elements 스택을 관리하며 DOM 트리에 삽입한다
삽입 모드 & 스택 (Insertion Mode / Stack of Open Elements)
현재 파싱 문맥(예: in head, in body)을 나타내며 올바른 부모-자식 관계와 자동 태그 보정을 결정한다
프리로드 스캐너 (Preload / Speculative Scanner)
파서가 스크립트로 막힌 동안 뒤쪽 HTML을 미리 스캔해 img·link·script 등 하위 리소스를 병렬로 선다운로드한다
스크립트 블로킹 (Script Blocking: sync/async/defer)
동기 스크립트는 파싱을 멈추고 즉시 실행, defer는 파싱 후 순서대로, async는 다운로드 완료 시 즉시 실행되도록 제어한다
CSSOM 의존성 (Style/CSSOM Dependency)
스크립트가 스타일 정보를 조회할 수 있어, CSS가 파싱되어 CSSOM이 준비될 때까지 스크립트 실행이 지연될 수 있다
DOM 생성 (DOM Construction)
트리 구성의 최종 산출물로 Document 루트에서 시작하는 노드 트리를 만들어 렌더링·스크립트가 접근하게 한다
오류 복구 (Error Recovery)
닫히지 않은 태그·잘못된 중첩 등 비표준 마크업을 스펙에 정의된 규칙으로 보정해 항상 유효한 DOM을 만든다

핵심 포인트

  • 파싱은 바이트→문자 디코딩 → 토크나이저(토큰화) → 트리 구성 → 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가 안티패턴인 근본 이유다.

쉽게 말하면 긴 문장을 단어(토큰)로 끊어 읽으며 문장 구조(트리)를 그려나가는 것.

면접 예상 질문

#토크나이저#파서#DOM 구성#script 블로킹