본문 바로가기
D.evelop/Web

[2] HTML의 렌더링에 대해 궁금했던 것들 - HTML Parsing

by Danne 2025. 4. 27.

파싱 Parsing 과정

  • HTML 파싱 : 받은 텍스트(string)를 해석해서 브라우저가 이해할 수 있는 노드(node) 단위로 변환하는 것

 

 

HTML 파서(Parser) 작동

  • HTML스트림을 받으면 브라우저는 HTML parser를 실행한다.
    • 스트림으로 받은 텍스트를 토큰(token) 으로 쪼갠 뒤
    • 토큰을 노드(node)로 바꿔서 트리(Tree) 구조를 만듦

 

1. Tokenizing (토크나이징)

  • 스트링을 읽으면서 문법 단위(Token)로 나누는 것
  • HTML파서에는 State Machine(상태 기계)가 있다.
    상태(state)를 바꿔가면서 1글자씩 문자(character)를 읽고, 토큰(token)을 만든다
<div class="container">Hello</div>
<div class="container"> ⭢ 시작 태그 토큰 (start tag token)
Hello ⭢ 텍스트 토큰 (text token)
</div> ⭢ 종료 태그 토큰 (end tag token)



1-1) HTML Tokenizing의 규칙

< 를 만나면 태그 시작
> 를 만나면 태그 종료
<script>, <style> 등은 별도의 특별한 규칙 적용
상태 설명 전환 조건
Data State 기본 상태. 그냥 텍스트 읽음 < 만나면 Tag Open으로 이동
Tag Open State < 를 만난 후.
어떤 태그인지 알아내야 함
/면 End Tag
알파벳이면 Start Tag
Tag Name State 태그 이름 읽는 중 (div, p, 등) 스페이스나 > 만나면 끝
Attribute State 속성 읽는 중 (class="x") > 만나면 끝
End Tag State </ 이후. 닫는 태그 이름 읽음 (</div>) > 만나면 종료

 

// <div class="greeting"> 를 읽는다면
1. Data State → '<' → Tag Open State
2. Tag Open State → 알파벳 d → Tag Name State
3. Tag Name State → 읽기: d → i → v
4. 스페이스 → Attribute State
5. 읽기: class → '=' → "greeting"
6. '>' → Data State 복귀

 

이때, img, video 같은 태그는 병렬처리되는 리소스로 분리 된다.

<img> 자체는 DOM에 들어가지만, 이미지 파일(.png, .jpg)은 백그라운드 다운로드가 시작됨.

 

 

2. Tree Construction (트리 구성)

생성된 토큰을 사용해서 노드(Node)를 만든 후, 이를 DOM 트리(DOM Tree)로 구성함

시작 태그 ⭢ 새 노드를 만들고 현재 부모 노드에 추가 ⭢ 새 노드를 현재 노드로 설정
텍스트 ⭢ 현재 노드에 텍스트 노드로 추가.
종료 태그 ⭢ 현재 노드를 닫고 상위 노드로 이동.

 

 

2-1) DOM 트리 (Document Object Model Tree) 구성

DOM : HTML 문서를 트리 형태(Tree Structure)로 모델링한 것

DOM 트리는 각 요소가 부모(parent)  - 자식(children)관계로 연결

[DOM의 기본 구성요소]
• Document 노드 (전체 문서)
• Element 노드 (<div>, <span>, <a> 같은 태그들)
• Text 노드 (태그 안에 있는 글자)
• Attribute 노드 (class="...", id="...")
• Comment 노드 (<!-- 주석 -->)

 

<div class="box">
  Hello
  <span>World</span>
</div>


위 같은 HTML 문서가 DOM으로 만들어지면 다음과 같은 구조가 된다.

Document
 └── html
      └── body
           └── div (class="box")
                ├── Text("Hello")
                └── span
                    └── Text("World")

 

 

중첩(Nesting) 구조의 HTML 문서의 구조는 어떨까?

<div> 
  오늘도 
  <p>안녕</p>
  <div>
    <p>막내야</p>
  </div>
  <img/>
</div>


1 - 여는 태그(<div>, <p>)를 만나면 새 노드 생성
2 - 닫는 태그(</p>, </div>)를 만나면 현재 노드 스택에서 pop(빠져나오기) 
즉, 구조적으로 스택(Stack)처럼 작동함

 

 

div
 ├── Text("오늘도")
 ├── p
 │    └── Text("안녕")
 ├── div
 │    └── p
 │         └── Text("막내야")
 └── img

 

2-1) a. 트리구조라면서 스택으로 처리한다는 게 무슨 말일까?

  • 트리(DOM Tree)는 최종 구조
  • 파싱 중에는 스택(stack)을

트리 만들 때, "현재 어디에 추가해야 할지"를 기억하려고 스택을 쓴다.

// 파싱 중 처리 흐름

(초기 상태)
스택: [document]

1. <div> 만남
→ div 노드 생성
→ document의 자식으로 div 추가
→ 스택에 div push
스택: [document, div]

2. "오늘도" 텍스트
→ 현재 스택 최상단(div)에 텍스트 노드 추가

3. <p> 만남
→ p 노드 생성
→ div 안에 p 추가
→ 스택에 p push
스택: [document, div, p]

4. "안녕" 텍스트
→ 현재 스택 최상단(p)에 텍스트 노드 추가

5. </p> 닫힘
→ 스택 pop (p 빠짐)
스택: [document, div]

6. <div> 만남
→ div 생성
→ div(첫번째 div)의 자식으로 추가
→ 스택에 새 div push
스택: [document, div, div]

7. <p> 만남
→ p 생성
→ 현재 div에 추가
→ 스택에 p push
스택: [document, div, div, p]

8. "막내야" 텍스트
→ p에 추가

9. </p> 닫힘
→ 스택 pop
스택: [document, div, div]

10. </div> 닫힘
→ 스택 pop
스택: [document, div]

11. <img/> 만남 (빈 요소)
→ img 노드 div에 추가
(스택 변경 없음)

12. </div> 닫힘
→ 스택 pop
스택: [document]

 

위 흐름을 거쳐 아래와 같은 최종 트리 구조가 만들어지는 것

div
 ├── Text("오늘도")
 ├── p
 │    └── Text("안녕")
 ├── div
 │    └── p
 │         └── Text("막내야")
 └── img

 

 

2-1) b. HTML 파서의 Insertion Mode (Before Head, In Body 등)

Insertion Mode = 현재 어디에 어떤 노드를 추가할지 결정하는 규칙

Insertion Mode 의미
Initial 문서 시작. Doctype 읽는 중.
Before Html <html> 요소를 기다리는 중.
Before Head <head> 요소를 기다리는 중.
In Head <title>, <meta>, <style> 같은 걸 삽입.
After Head <body> 기다리는 중.
In Body 본문 내용 삽입 (대부분 여기서 작동)
After Body </body> 이후 처리
After After Body 정말 예외적 에러 복구 (ex: body 밖에 요소 있을 때)

 

파서는 위 모드를 실시간으로 바꿔가면서 "지금 들어온 토큰을 어디 추가할지", "지금 정상적인 위치에 있는지" 등을 체크한다.

 

 

2-1) c. DOM 완성 전, 병렬 처리되는 것

브라우저는 HTML 파싱 중에도 다른 작업을 병렬로 처리한다.

CSS 파싱 <link>, <style> 만나는 순간 별도로 CSSOM(CSS Object Model) 파싱 시작
JS 실행 <script> 만나는 순간 파싱 중단하고, 스크립트 다운로드 → 실행. (기본은 'blocking')
리소스 다운로드 <img>, <video>, <audio> 등은 비동기적으로 다운로드 시작

 

이때 CSSOM파싱은 HTML파싱과 다르다.

단계 설명
HTML 파싱 HTML 문자열 → 토큰 → DOM 트리 구성
CSS 파싱 CSS 파일/코드 → 토큰 → CSSOM 트리 구성
DOM + CSSOM 통합 두 트리를 합쳐서 Render Tree 생성
레이아웃(Layout) Render Tree를 기반으로 요소 크기/위치 계산
페인팅(Paint) 계산된 레이아웃을 기반으로 픽셀 단위로 그리기

 

DOM, CSSOM은 둘 다 "트리"지만, 목적과 과정이 다르며 통합은 별도 단계이다.

 

❗️HTML 파싱 도중 <link rel="stylesheet">, <style> 같은 CSS을 만나면, 그때부터 브라우저가 CSS 파싱도 같이 시작한다
그래서 "HTML파싱 중"에 CSSOM 만드는 것처럼 보이기도 하지만 "병렬"로 처리되고 있는 별도의 단계.

[HTML 파싱] + [CSS 파싱]  
   ↓
[DOM 트리] + [CSSOM 트리]  
   ↓
[Render Tree 생성]  
   ↓
[Layout 계산 (Reflow)]  
   ↓
[Paint (그리기)]  
   ↓
[사용자가 보는 화면]

 

2-1) d. blocking : "파서를 멈춘다"는 의미

  • 기본 (nothing special) : <script> 태그를 만나면 무조건 파싱 멈춤. 다운로드 완료 즉시 실행.
  • 예외
    • async(파싱과 다운로드 병렬, 파싱은 안 멈추지만 렌더링 블로킹 위험)
    • defer(파싱 끝나고 실행, 렌더링에 최적화)
      defer 스크립트는 파싱 끝나고, 렌더링 직전이나 중간에 실행.
      DOMContentLoaded 이벤트 직전에 스크립트 실행
  • 실무 tip : 외부 스크립트는 defer를 최대한 쓰고
    페이지 구조를 진짜 동적으로 바꿔야 하는 경우만 blocking <script>를 사용



2-1) e. 그럼에도 HTML파싱 ➝ CSS파싱 같아 보이는 이유?

CSS 파싱자체는 HTML 파싱의 이벤트를 트리거로 해서 시작되기 때문.

 

HTML 파싱은 순차적(linear)이라 <html>, <head>, <meta>, <title> 순서대로 파싱해 나간다.
HTML이 <link>를 파싱 할 때까지는 CSS 파싱이 아예 존재하지 않는다.

<html>, <head> 등의 선행이 일어나야 <link rel="stylesheet">가 실행되는 것.

HTML 파서:
<html> → <head> → <meta> → <title> → <link> (여기서 CSS 요청 발생!)
    ↓
계속 <body> 파싱 진행 중

CSS 파서:
      <------ (link 태그를 만난 순간부터 별도 스레드로 시작)

→ 이후 둘 다 병렬로 진행

 

 

 

2-1) f. <img>를 빨리 로딩하려면 부모 가까이에 두는 게 유리한가?

맞기도, 아니기도 함.

 

  • 브라우저는 HTML 파싱 중 <img> 태그를 만나야 이미지를 다운로드 요청을 시작한다.
    <img>를 빨리 만나야 리퀘스트를 빨리 보낼 수 있다.
    즉, <img>를 문서 초반부 (ex: body 상단) 배치하면 다운로드 시작이 빨라져서 더 빨리 화면에 뜨는 건 맞다.
  • 하지만 가장 좋은 방법은
    <link rel="preload" as="image" href="..."> 같은 걸 <head>에 사용.
    body태그까지 가기도 전에 미리 다운로드 트리거 걸어버리는 것

 

이 문제는 게임원화와 같은 고화질, 고용량의 이미지 사용 시 마주했던 문제였다.
"웅장하게 보여야 할 배경이 늦게 렌더링 된다는 것"
당시 해결법은 이러했다.

1. css의 background로 호출했던 이미지를 html의 <img> 태그로 빼고 가장 최상단(body 시작 바로 밑)에 작성
2. 시각적 착시를 주기 위해 css로 opacity transition을 줌

그중 1의 방법을 수행하면서 그 원리가 궁금했는데, 파싱과정을 알게 되니 이해가 됐다.

+ 이미지 소스를 용량을 일정 이상 저하시키면 그라데이션 영역에서 미세한 노이즈가 발생해서 컴플레인이 들어왔와서 용량을 낮추는게 방법은 아니었음.

.

반응형

댓글