1. 브라우저 렌더링 과정 개요
[요청] → [HTML 파싱 → DOM] → [CSS 파싱 → CSSOM]
→ [렌더 트리 생성] → [레이아웃] → [페인트]
단계설명
| 1. 요청/응답 | DNS를 통해 IP 주소를 확인하고 서버에 리소스 요청 |
| 2. DOM 생성 | 바이트 → 문자 → 토큰 → 노드 → DOM 트리 |
| 3. CSSOM 생성 | CSS 파일을 동일한 파싱 과정으로 CSSOM 트리 생성 |
| 4. JS 실행 | 자바스크립트 엔진이 AST 생성 → 바이트코드 실행 |
| 5. 렌더 트리 | DOM + CSSOM 결합 (화면에 표시되는 노드만 포함) |
| 6. 레이아웃 | 각 요소의 위치와 크기 계산 |
| 7. 페인트 | 픽셀로 화면에 그리기 |
2. 요청과 응답
브라우저 주소창에 URL을 입력하면:
- URL의 호스트 이름이 DNS를 통해 IP 주소로 변환된다.
- 해당 IP 주소의 서버에 GET 요청을 전송한다.
- 루트 요청(/)의 경우 서버는 기본적으로 index.html을 응답한다.
https://www.example.com:80/docs/search?category=js#intro
│ │ │ │ │ │
scheme host port path query fragment
핵심: index.html을 파싱하는 중 <link>, <img>, <script> 태그를 만나면 해당 리소스를 서버에 추가로 요청한다.
3. HTTP 1.1 vs HTTP 2.0
구분HTTP/1.1HTTP/2.0
| 요청/응답 | 커넥션당 하나씩 처리 | 커넥션당 다중 처리 가능 |
| 전송 방식 | 순차적 (개별 전송) | 동시 전송 |
| 속도 | 느림 | 약 50% 빠름 |
4. HTML 파싱과 DOM 생성
서버에서 HTML을 바이트로 응답받아 다음 과정을 거쳐 DOM 트리를 생성한다.
바이트(2진수)
↓ (meta charset 기준 디코딩)
문자열
↓ (어휘 분석 · Lexical Analysis)
토큰 (문법적 의미를 가지는 코드의 최소 단위)
↓
노드 (문서 노드 / 요소 노드 / 어트리뷰트 노드 / 텍스트 노드)
↓
DOM 트리
DOM 트리 예시
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul>
<li id="apple">Apple</li>
<li id="banana">Banana</li>
</ul>
<script src="app.js"></script>
</body>
</html>
document
└── html
├── head
│ ├── meta [charset="UTF-8"]
│ └── link [rel="stylesheet" href="style.css"]
└── body
├── ul
│ ├── li [id="apple"] → "Apple"
│ └── li [id="banana"] → "Banana"
└── script [src="app.js"]
5. CSS 파싱과 CSSOM 생성
렌더링 엔진이 <link> 또는 <style> 태그를 만나면:
- DOM 생성을 일시 중단한다.
- CSS 파일을 서버에 요청한다.
- HTML과 동일한 파싱 과정(바이트 → 문자 → 토큰 → 노드)을 거쳐 CSSOM 트리를 생성한다.
- CSS 파싱 완료 후 HTML 파싱을 재개한다.
6. 렌더 트리 생성
DOM + CSSOM을 결합하여 렌더 트리를 생성한다.
렌더 트리에서 제외되는 노드:
- <meta>, <script>, <head> 같은 화면에 표시되지 않는 노드
- CSS display: none이 적용된 노드
visibility: hidden은 공간은 차지하지만 보이지 않으므로 렌더 트리에 포함된다.
렌더 트리 완성 후:
- 레이아웃(Layout): 각 요소의 위치(x, y)와 크기(width, height) 계산
- 페인트(Paint): 계산된 정보로 픽셀을 화면에 그리기
7. 자바스크립트 파싱과 실행
HTML 파싱 중 <script> 태그를 만나면:
- DOM 생성을 일시 중단한다.
- 자바스크립트 파일을 서버에 요청한다.
- 자바스크립트 엔진에 제어권을 넘긴다.
- JS 엔진이 코드를 파싱하여 **AST(Abstract Syntax Tree)**를 생성한다.
- AST를 기반으로 바이트코드를 생성하고 실행한다.
- 실행 완료 후 렌더링 엔진으로 제어권 반환 → HTML 파싱 재개
8. 리플로우와 리페인트
자바스크립트에서 DOM API로 DOM이나 CSSOM을 변경하면 렌더 트리가 재생성되며 다음이 발생한다.
구분설명발생 조건
| 리플로우(Reflow) | 레이아웃 계산을 다시 수행 | 요소 추가/삭제, 크기/위치 변경, 창 리사이징 |
| 리페인트(Repaint) | 재결합된 렌더 트리로 다시 페인트 | 리플로우 이후, 또는 색상 등 레이아웃 영향 없는 변경 |
중요: 레이아웃에 영향을 주지 않는 변경(색상, 투명도 등)은 리페인트만 발생한다. 리플로우 + 리페인트는 비용이 크므로 가급적 최소화해야 한다.
9. script 태그 위치와 async/defer
문제: <head> 안에 <script> 배치 시
<head>
<script src="app.js"></script> <!-- DOM이 완성되기 전에 실행! -->
</head>
<body>
<div id="app">...</div>
</body>
// app.js
const $app = document.getElementById('app'); // null → 에러 발생!
$app.style.color = 'red'; // TypeError
해결책 1: </body> 직전에 배치
<body>
<div id="app">...</div>
<script src="app.js"></script> <!-- DOM 완성 후 실행 -->
</body>
해결책 2: async / defer 어트리뷰트
<script async src="app.js"></script>
<script defer src="app.js"></script>
구분HTML 파싱JS 로드JS 실행 시점
| 기본 | 중단 | 순차 | 로드 즉시 |
| async | 병렬 | 병렬 | 로드 완료 즉시 (순서 보장 X) |
| defer | 병렬 | 병렬 | HTML 파싱 완료 후 (순서 보장 O) |
권장: DOM 조작이 필요한 스크립트는 defer 사용
10. DOM이란?
**DOM(Document Object Model)**은 HTML 문서를 파싱한 결과물로, 브라우저가 이해할 수 있는 트리 자료구조다.
노드의 4가지 핵심 타입
노드 타입설명
| 문서 노드 | DOM 트리 최상위 루트. document 객체. 다른 노드에 접근하는 진입점 |
| 요소 노드 | HTML 요소를 나타냄. 문서의 구조를 표현 |
| 어트리뷰트 노드 | HTML 요소의 어트리뷰트. 부모 노드 없이 요소 노드에만 연결됨 |
| 텍스트 노드 | HTML 요소의 텍스트. 리프 노드(자식 노드 없음). 문서의 정보를 표현 |
노드 상속
Object
└── EventTarget
└── Node
├── Document → HTMLDocument
├── Attr
├── CharacterData → Text / Comment
└── Element → HTMLElement
├── HTMLInputElement
├── HTMLDivElement
└── ...
11. 노드 취득 메서드 비교
id로 취득
// 단 하나의 요소 반환. 없으면 null
document.getElementById('apple');
태그 이름으로 취득
// HTMLCollection 반환 (live)
document.getElementsByTagName('li');
// 특정 요소의 자손 중에서만 탐색
$ul.getElementsByTagName('li');
class로 취득
// HTMLCollection 반환 (live)
document.getElementsByClassName('fruit');
CSS 선택자로 취득 ✅ 권장
// 첫 번째 요소 하나 반환
document.querySelector('.banana');
// 조건을 만족하는 모든 요소 반환 → NodeList (non-live)
document.querySelectorAll('ul > li');
HTMLCollection vs NodeList 비교
구분HTMLCollectionNodeList
| 반환 메서드 | getElementsByTagName, getElementsByClassName | querySelectorAll, childNodes |
| live 여부 | 항상 live (실시간 반영) | 대부분 non-live (childNodes는 live) |
| 순회 시 주의 | for 문 순회 중 변경되면 오작동 위험 | 비교적 안전 |
| 권장 사용법 | 배열로 변환 후 사용 [...$elems] | 배열로 변환 후 사용 권장 |
// HTMLCollection 순회 시 주의사항
const $elems = document.getElementsByClassName('red'); // live!
// ❌ 위험: 순회 중 DOM이 변경되어 일부 요소가 건너뛰어짐
for (let i = 0; i < $elems.length; i++) {
$elems[i].className = 'blue';
}
// ✅ 안전: 배열로 변환 후 순회
[...$elems].forEach(elem => elem.className = 'blue');
12. DOM 조작
텍스트 조작
// nodeValue: 텍스트 노드의 값을 직접 변경
const $text = document.getElementById('foo').firstChild;
$text.nodeValue = 'changed!';
// textContent: 요소의 모든 텍스트 취득/변경 (HTML 마크업 무시)
document.getElementById('foo').textContent = 'Hello World';
innerHTML vs insertAdjacentHTML vs createElement
방법장점단점
| innerHTML | 간단, 직관적 | XSS 취약, 기존 자식 노드 전부 제거 후 재생성 |
| insertAdjacentHTML | 기존 노드 유지, 빠름 | XSS 취약 |
| createElement + appendChild | 안전, XSS 위험 없음 | 코드 복잡 |
// insertAdjacentHTML 위치 옵션
element.insertAdjacentHTML('beforebegin', '<p>앞</p>'); // 요소 앞
element.insertAdjacentHTML('afterbegin', '<p>첫째</p>'); // 첫 자식 앞
element.insertAdjacentHTML('beforeend', '<p>마지막</p>');// 마지막 자식 뒤
element.insertAdjacentHTML('afterend', '<p>뒤</p>'); // 요소 뒤
노드 생성 및 추가
// 여러 노드 추가 시 DocumentFragment 사용 → DOM 변경 1회로 최소화
const $fragment = document.createDocumentFragment();
['Apple', 'Banana', 'Orange'].forEach(text => {
const $li = document.createElement('li');
$li.textContent = text;
$fragment.appendChild($li);
});
document.getElementById('fruits').appendChild($fragment);
// DOM 변경은 단 1회!
주요 노드 탐색 프로퍼티
// 자식 탐색
node.childNodes // NodeList (텍스트 노드 포함)
element.children // HTMLCollection (요소 노드만)
element.firstElementChild
element.lastElementChild
// 부모 탐색
node.parentNode
// 형제 탐색
element.previousElementSibling
element.nextElementSibling