다음 HTML 은 모두 정상 렌더링된다:
<p>Hello world
<img src="cat.jpg">
<br/>
<BR>
<Br />
<DIV><span>nested</p></div>닫는 태그 없음, attribute quote 없음, tag 대소문자 혼용, 중첩 잘못. XML 이라면 모두 fatal error. 그러나 브라우저는 모두 받아 화면 표시한다. 어떻게? 이 가이드는 HTML5 parser 의 state machine, error recovery, void element 같은 quirk 의 원리를 정리한다.
HTML5 parser 의 핵심 원칙 — "never throw"
HTML5 spec (2014) 은 parser 의 동작을 byte-by-byte 정밀하게 정의 — 어떤 입력이 들어와도 결정적으로 같은 DOM 트리 생성. 에러는 발생하지 않음. "malformed" 입력도 정확한 복구 규칙이 spec 에 박혀 있다.
설계 의도 — Web 의 30 년 누적 HTML 이 망가지면 안 됨. 한 줄 오타로 사이트가 안 떠선 안 된다는 사회적 계약. XHTML 1.0 의 strict 시도가 실패한 이유.
두 단계 — Tokenizer + Tree Construction
HTML bytes
↓
1. Tokenizer — character stream → token stream
↓
2. Tree builder — token stream → DOM tree
↓
DOM1. Tokenizer state machine
HTML5 tokenizer 는 80+ state 의 거대한 state machine. 각 state 에서 다음 글자에 따라 다른 state 로 전이:
Data state ← 보통의 텍스트
'<' 만남 → Tag open state
다른 글자 → 텍스트 token 누적
Tag open state
'/' 만남 → End tag open state
알파벳 → Tag name state
'!' 만남 → Markup declaration state (<!DOCTYPE / <!-- 등)
그 외 → 다시 Data state (< 를 텍스트로 처리)
Tag name state
알파벳 → 누적
공백 → Before attribute name state
'>' → token emit, 다시 Data state
'/' → Self-closing start tag state실제 spec 80 state. 각각 정밀히 정의. 결과 — 같은 HTML 을 어느 브라우저로 봐도 결정적으로 같은 token stream.
왜 <br/> 와 <br> 가 같나
HTML5 의 void element (자식 X 강제):
area, base, br, col, embed, hr, img, input, link, meta,
param, source, track, wbrparser 가 void element 의 시작 태그 만나면 즉시 element 노드 emit + close. / 는 무시:
<br> ← 정상 (HTML5)
<br/> ← / 무시, 정상
<br /> ← 공백 + / 무시, 정상
</br> ← end tag 는 무시 (void 는 close 불가)XHTML 1.0 호환 위해 / 허용. 그러나 효과는 0 — void 는 어차피 즉시 close.
void 가 아닌 element 의 self-close
<div/> ← HTML5: / 무시, <div> open 그대로
<span/> ← 같음HTML5 는 void 외 element 의 self-close 인식 X. React JSX 의 <div /> 는 React 가 알아서 <div></div> 로 emit. raw HTML 의 self-close 는 함정.
Error recovery — 잘못된 입력의 정확한 복구
spec 이 모든 malformed case 의 복구 동작을 정의.
1. 누락된 닫는 태그
입력: <p>First<p>Second
DOM: <p>First</p><p>Second</p>새 <p> 만나면 이전 <p> 자동 close. <p> 가 다른 <p> 의 자식이 되지 않는다는 spec 룰.
2. 잘못된 nesting
입력: <b>Hello <i>world</b> friend</i>
DOM: <b>Hello <i>world</i></b><i> friend</i>Adoption agency algorithm — formatting element (b/i/em/strong 등) 가 잘못 nested 되면 자동 재구성. Netscape 시대부터의 호환성 위해.
3. body 안 head 의 콘텐츠
입력:
<html>
<body>
<p>text
<title>Late title</title>
</body>
</html>
DOM:
<html>
<head>
<title>Late title</title> ← body 에서 head 로 자동 이동
</head>
<body>
<p>text</p>
</body>
</html>4. table 의 자동 복구
입력: <table><div>weird</div></table>
DOM:
<div>weird</div> ← table 밖으로 자동 이동 (foster parenting)
<table></table>spec 의 "foster parenting" — table 안 비 table-content 는 table 앞으로 자동 이동.
대소문자 무시
<DIV> → <div>
<BR> → <br>
<Img> → <img>Tokenizer 가 tag name·attribute name 을 lowercase 로 정규화. attribute value 는 case 보존 (<a href="HTTP://EXAMPLE.COM"> 의 URL 은 그대로).
XHTML / SVG / MathML 안에서는 case-sensitive. <svg> 안 <foreignObject> 대소문자 정확해야.
HTML entity decoding
Tokenizer 가 entity reference 를 실제 글자로 변환:
& → &
< → <
> → >
" → "
A → A (decimal codepoint)
A → A (hex codepoint)
Å → Å (named entity, 2200+ 종)실험 — HTML 엔티티 인코딩 / 디코딩 양방향 변환.
왜 attribute 안 등 escape 가 위험한가
<a href="javascript:alert(1)">click</a> ← raw, XSS
<a href="javascript:alert(1)">click</a> ← entity-encoded, 여전히 XSS
이유 — href value 의 entity decode 가 일찍 일어남.
URL scheme 검사가 그 후 → "javascript:" 통과.Defense — server-side 에서 attribute value 의 scheme whitelist 검증.
Script 와 Style 의 특수 처리
Tokenizer 가 <script> 안에서는 일반 HTML 파싱 중단. JavaScript 안의 </ 까지 raw text:
<script>
const x = "<div>"; // 정상 — < 가 HTML 시작으로 해석 X
</script>그러나 </script> string 이 안에 있으면 종료:
<script>
const x = "</script>"; // ← 여기서 script 종료!
// 나머지가 HTML 로 해석됨
</script>해결 — escape:
const x = "<\/script>"; // 또는 "<\u002fscript>"DOCTYPE 의 영향 — Quirks Mode
<!DOCTYPE html> 가 처음에 박히면 standards mode. 없으면 quirks mode — 90 년대 호환 동작 (margin/padding 계산 다름, table layout 다름 등).
Quirks mode 의 흔한 증상:
box-sizing의 default 가 content-box 가 아닌 border-box (IE 5 호환)- table cell 안 image 의 bottom margin 처리 다름
- font-size
1em의 base 가 다름
새 사이트는 무조건 <!DOCTYPE html> 첫 줄.
XML 과의 차이 — XHTML 의 실패
2000 년대 초 XHTML 1.0/1.1 가 strict XML 기반 HTML 표준. malformed 입력에 fatal error.
<p>open paragraph
<!-- XHTML: parse error, 빈 화면
<br> ← XHTML 에러 (반드시 <br/>)
<DIV> ← XHTML 에러 (반드시 lowercase)결과 — 한 글자 오타로 사이트가 안 뜸. 개발자·CMS 사용자가 반발. WHATWG (Mozilla/Apple/Opera) 가 2004 년 HTML5 작업 시작 — "기존 web 호환 + tolerant parsing". 2014 년 HTML5 표준 채택. XHTML 은 history.
HTML formatter 의 동작
Formatter 도 HTML parser 위에 박힘:
- HTML 을 parse → DOM
- DOM 을 prettifier (들여쓰기·줄바꿈 룰) 로 재출력
결과 — input 의 fluent malformed HTML 도 formatter 가 자동 정정 + 들여쓰기 (parser 가 이미 복구한 DOM 사용).
HTML 포매터 가 같은 패턴.HTML → Markdown 도 parser → DOM 후 markdown 으로 변환.
흔한 함정
1. <script> 안 </script> 문자열
위에서 본 — JS code 의 closing tag 가 parser 를 일찍 종료시킴.
2. attribute quote 누락
<a href=https://example.com>click</a>
<!-- 통하긴 함. 그러나 -->
<a href=https://example.com/path?q=v>click</a>
<!-- ? 부터 attribute name 으로 해석. 깨짐. -->3. inner text 의 & 잊음
<p>Tom & Jerry</p>
<!-- 일부 entity 패턴 (&...; 까지) 으로 해석되어 깨질 수 있음 -->
<p>Tom & Jerry</p> ← 안전4. URL 안 &
<a href="page.php?a=1&b=2"> ← &b 가 entity 로 해석되려 함
<a href="page.php?a=1&b=2"> ← 표준5. innerHTML 의 XSS
element.innerHTML = userInput;
<!-- userInput 이 "<img src=x onerror=alert(1)>" 면 XSS -->
element.textContent = userInput; ← 안전 (HTML parser 안 거침)참고 자료
- HTML Living Standard — Parsing — WHATWG
- HTML5 tokenizer states (시각화) — WHATWG
- html5lib (Python HTML5 parser) — GitHub
- The vendor that gave up on XHTML — W3C
요약
- HTML5 parser = "never throw". 모든 malformed 입력에 spec 정의된 복구 동작.
- 2 단계 — tokenizer (80+ state machine) → tree builder (DOM).
- Void element (br/img/hr/input 등) 는 자식 X 강제.
<br/>의 / 는 무시. - Error recovery — 빠진 닫는 태그·잘못된 nesting·잘못된 위치 ( body 안 title) 모두 자동 복구.
- Tag/attribute name 은 case-insensitive. SVG/MathML 안은 예외.
- DOCTYPE 으로 standards vs quirks mode. 새 사이트는 무조건
<!DOCTYPE html>. - XHTML 의 strict 시도는 사회적 계약 위반으로 실패. HTML5 가 tolerant parsing 으로 표준화.
- 실험 — HTML 포매터 / HTML 엔티티 인코딩 / 디코딩 / HTML → Markdown.