HTML 파싱은 원시 HTML 텍스트를 구조화된 DOM(Document Object Model) 트리로 변환하는 과정이다.
이 과정은 주로 두 가지 주요 단계로 구성된다: 토큰화(Tokenization)와 트리 구축(Tree Construction)
토큰화는 HTML 소스 코드를 의미 있는 작은 단위(토큰)로 분할하는 과정이다.
<div></div>class="container"<!-- 주석 --><!DOCTYPE html>HTML:
<p class="container">Hello, world!</p>
토큰화 결과:
1. 시작 태그 토큰: <p>
2. 속성 토큰: class="container"
3. 텍스트 토큰: Hello, world!
4. 종료 태그 토큰: </p>
// 토큰 타입 정의
type TokenType = 'START_TAG' | 'END_TAG' | 'TEXT' | 'ATTRIBUTE';
interface Token {
type: TokenType;
value: string;
}
// DOM 노드 클래스
class Node {
tagName: string;
children: (Node | string)[];
attributes: Map<string, string>;
constructor(tagName: string) {
this.tagName = tagName;
this.children = [];
this.attributes = new Map();
}
}
// 토큰화 함수
function tokenize(htmlString: string): Token[] {
const tokens: Token[] = [];
let currentToken = '';
let state: 'DATA' | 'TAG_OPEN' | 'TAG_NAME' | 'ATTR_NAME' | 'ATTR_VALUE' = 'DATA';
for (const char of htmlString) {
switch (state) {
case 'DATA':
if (char === '<') {
if (currentToken) {
tokens.push({ type: 'TEXT', value: currentToken });
currentToken = '';
}
state = 'TAG_OPEN';
} else {
currentToken += char;
}
break;
case 'TAG_OPEN':
if (char === '/') {
tokens.push({ type: 'END_TAG', value: '' });
state = 'TAG_NAME';
} else if (char.match(/[a-z]/i)) {
currentToken = char;
state = 'TAG_NAME';
}
break;
case 'TAG_NAME':
if (char === '>') {
tokens.push({ type: 'START_TAG', value: currentToken });
currentToken = '';
state = 'DATA';
} else if (char === ' ') {
tokens.push({ type: 'START_TAG', value: currentToken });
currentToken = '';
state = 'ATTR_NAME';
} else {
currentToken += char;
}
break;
case 'ATTR_NAME':
if (char === '=') {
currentToken += char;
state = 'ATTR_VALUE';
} else if (char === '>') {
if (currentToken) {
tokens.push({ type: 'ATTRIBUTE', value: currentToken });
}
currentToken = '';
state = 'DATA';
} else {
currentToken += char;
}
break;
case 'ATTR_VALUE':
if (char === '"') {
if (currentToken.endsWith('=')) {
currentToken += char;
} else {
tokens.push({ type: 'ATTRIBUTE', value: currentToken + char });
currentToken = '';
state = 'ATTR_NAME';
}
} else {
currentToken += char;
}
break;
}
}
if (currentToken) {
tokens.push({ type: 'TEXT', value: currentToken });
}
return tokens;
}
트리 구축은 토큰화 단계에서 생성된 토큰들을 사용하여 DOM 트리를 생성하는 과정이다.
HTML:
<div>
<p>Hello</p>
<p>World</p>
</div>
트리 구축 결과:
div
├── p
│ └── "Hello"
└── p
└── "World"
// 트리 구축 함수
function buildTree(tokens: Token[]): Node {
const root = new Node('root');
let currentNode = root;
const stack: Node[] = [root];
for (const token of tokens) {
switch (token.type) {
case 'START_TAG':
const newNode = new Node(token.value);
currentNode.children.push(newNode);
stack.push(newNode);
currentNode = newNode;
break;
case 'END_TAG':
stack.pop();
currentNode = stack[stack.length - 1];
break;
case 'TEXT':
currentNode.children.push(token.value);
break;
case 'ATTRIBUTE':
const [name, value] = token.value.split('=');
currentNode.attributes.set(name, value.replace(/"/g, ''));
break;
}
}
return root;
}
// HTML 파싱 함수
function parseHTML(html: string): Node {
const tokens = tokenize(html);
return buildTree(tokens);
}
// 사용 예시
const html = '<div class="container"><p>Hello, <span>world</span>!</p></div>';
const parsedTree = parseHTML(html);
console.log(JSON.stringify(parsedTree, null, 2));
스트리밍 처리: 토큰화와 트리 구축은 동시에 진행된다. 전체 HTML을 메모리에 로드할 필요 없이 스트리밍 방식으로 처리할 수 있다.
상태 기반: 파서는 여러 상태(예: 태그 내부, 속성 값 내부 등)를 가지며, 현재 상태에 따라 입력을 다르게 해석한다.
특수 규칙: <script>, <style> 등 특정 태그는 특별한 파싱 규칙을 가진다.
비동기 로딩: 외부 리소스(CSS, JavaScript 등)를 만나면 비동기적으로 로드하면서 파싱을 계속한다.