38장 브라우저의 렌더링 과정
Node.js의 등장
Node.js의 등장으로 JS는 웹 브라우저를 벗어나, SSR 개발에서도 사용할 수 있는 범용 개발 언어로 발전
하지만, 여전히 JS가 가장 많이 사용되는 분야는 웹 브라우저 환경에서 동작하는 CSR 웹 페이지/애플리케이션
Node.js란? 구글의 V8 자바스크립트 엔진으로 빌드된 자바스크립트 런타임 환경
구글이 개발 중인 JIT(Just-in-Time compile, JIT compile) 타입 JS 엔진 (현재는 Chromium 산하 프로젝트)
대부분의 프로그래밍 언어는 OS, VM 위에서 실행
But, 웹 애플리케이션의 클라이언트 사이드인 JS는 브라우저에서 HTML, CSS와 함께 실행됨 → 즉, 더 효율적인 브라우저 환경의 프로그래밍을 제공하는 언어!
그럼 브라우저가 HTML, CSS, JS로 작성된 txt 문서를 어떻게 파싱하여 브라우저에 렌더링하는가?
파싱(syntax analysis)
프로그래밍 언어의 문법에 맞게 작성된 txt 문서를 읽어 들여 실행하기 위해 txt 문서의 문자열을 token으로 분해하고, token에 문법적 의미와 구조를 반영하여 트리 구조의 자료구조인 parse tree(syntax tree)를 생성하는 일련의 과정을 의미
렌더링(rendering)
HTML, CSS, JS로 작성된 문서를 파싱하여 브라우저에 시각적으로 출력하는 것
즉, 렌더링을 위해 파싱이 필요하다~
브라우저의 렌더링 과정

- 브라우저 → 서버 : 렌더링에 필요한 리소스 요청 및 응답 (HTML, CSS, JS, image, fonts …)
- 브라우저의 렌더링 엔진
- 응답 받은 HTML, CSS 파싱하여 각각 DOM, CSSOM 생성
- 결합하여 렌더 트리 생성
- 브라우저의 JS 엔진
- 응답 받은 JS를 파싱하여 AST(Abstract Syntax Tree)를 생성하고 바이트코드로 변환하여 실행
- 이때, JS는 DOM API를 통해 DOM이나 CSSDOM을 변경할 수 있음. 변경된 것은 렌더 트리로 결합됨
- 브라우저 : 렌더 트리를 기반으로 HTML 요소의 레이아웃 계산하고 화면에 HTML 요소를 페인팅
이 렌더링 과정을 하나 하나 자세히 살펴보자!
38.1. 요청과 응답
브라우저의 핵심 기능 : 필요한 리소스를 서버에 요청(request)하고 응답(response)받아 브라우저에 시각적으로 렌더링하는 것
즉, 렌더링에 필요한 리소스는 모두 서버에 존재
서버에 요청을 전송하는 방법
주소창을 활용

URL의 호스트 이름이 DNS를 통해 IP 주소로 변환 → 해당 IP 주소를 갖는 서버에게 요청을 전송
https://zziglet.com : 해당 URL의 서버가 index.html을 클라이언트(브라우저)로 응답 (루트 요청은 암묵적으로 index.html을 응답)
https://zziglet.com/data.json : 해당 URL의 서버가 data.json을 클라이언트(브라우저)로 응답
서버에게 정적 데이터만 요청할 수 있는 것은 아니다.
JS를 활용해 동적으로 서버에 정적/동적 데이터 요청이 가능하다 → ajax, REST API 내용에서 자세히 살펴보자
요청과 응답을 확인하는 법
개발자 도구의 Network 패널에서 확인 가능

그런데, 위 그림을 살펴보면 index.html(zziglet.com) 뿐만 아니라 다른 파일들도 응답됨. why?
→ 브라우저의 렌더링 엔진이 index.html을 파싱하는 도중에 외부 리소스 로드하는 태그 (css 파일을 로드하는 link 태그, 이미지 파일을 로드하는 img 태그, js를 로드하는 script 태그) 등을 만나면 HTML의 파싱을 일시 중단하고 해당 리소스 파일을 서버로 요청하기 때문이다.
38.2. HTTP/1.1과 HTTP/2.0
웹에서 브라우저와 서버가 통신하기 위한 프로토콜이다
1991년 : 최초로 문서화
1996년 : HTTP/1.0
1999년 : HTTP/1.1
2015년 : HTTP/2
HTTP/1.1과 HTTP/2의 차이에 대해 알아보자
HTTP/1.1
커넥션 당 하나의 요청과 응답만 처리

→ 여러 개의 요청을 한 번에 전송/응답 불가
⇒ 요청할 리소스의 개수에 비례하여 응답 시간이 증가한다는 단점
HTTP/2.0
커넥션 당 여러 개의 요청과 응답 처리 가능
⇒ HTTP/1.1에 비해 페이지 로드 속도가 약 50% 빠르다고 알려져 있다.
38.3. HTML 파싱과 DOM 생성
순수한 텍스트인 HTML 문서를 브라우저에 시각적인 픽셀로 렌더링하려면?
→ HTML 문서를 브라우저가 이해할 수 있는 자료구조(객체)로 변환하여 메모리에 저장해야 함
다음과 같은 예시를 통해 이해해보자.
<!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>
<li id="orange">Orange</li>
</ul>
<script src="app.js"></script>
</body>
</html>브라우저의 렌더링 엔진이 해당 HTML 문서를 응답 받았다면,
HTML을 파싱하여 브라우저가 이해할 수 있는 자료구조인 DOM(Document Object Model)을 생성
DOM 생성 과정
브라우저의 렌더링 엔진이 이 과정을 수행한다

- 서버
- 브라우저가 요청한 HTML 파일을 읽어 메모리에 저장한
- 메모리에 저장된 바이트를 인터넷 경유하여 브라우저에 응답
- 브라우저 렌더링 엔진
- 바이트 형태로 응답 받아 meta 태그의 charset 어트리뷰트에 의해 지정된 인코딩 방식(ex. UTF-8)을 기준으로 문자열로 변환
- meta 태그의 인코딩 방식은 응답 헤더에 담겨 응답 됨
content-type: text/html; charset=utf-8
- 브라우저 렌더링 엔진
- 문자열로 변환된 HTML 문서를 토큰(문법적 의미를 갖는 코드의 최소 단위)들로 분해
- 브라우저 렌더링 엔진
- 토큰들을 객체로 변환하여 내용에 따라 각 노드들을 생성
- 노드는 DOM을 구성하는 기본요소가 됨
- DOM 생성
- DOM은 HTML 요소 간의 중첩 관계(부자 관계)를 기반으로 트리 자료구조로 구성됨
- 이 트리 자료구조 결과물을 DOM이라고 부름
더 자세한 내용은 39장 ‘DOM’에서 살펴보자..
38.4. CSS 파싱과 CSSOM 생성
DOM이랑 CSSOM 생성 시점이 어떻게 다른가?
브라우저의 렌더링 엔진은 HTML을 한 줄 씩 순차적으로 파싱하여 DOM을 생성해 나간다
- DOM 생성하다가 CSS를 로드하는
<link>,<style>을 만나면 DOM 생성을 일시 중단한다 <link>의 href 어트리뷰트에 지정된 CSS 파일을 서버에 요청하여 로드- 로드한 CSS 파일이나
<style>내의 CSS를 HTML과 동일한 파싱과정을 거치며 CSSOM을 생성 - CSS 파싱이 완료되면 다시 HTML을 파싱하며 DOM 생성을 재개함
이전 예시를 다시 확인해보자.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="style.css" />
...
</head>
</html>-
브라우저의 렌더링 엔진 : meta 태그까지 HTML을 순차적으로 해석하고,
<link>를 만나면 DOM 생성을 일시 중단 -
브라우저의 렌더링 엔진 :
<link>태그의 href에 지정된style.css파일을 서버에 요청 및 응답 받음body { font-size: 18px; } ul { list-style-type: none; } -
브라우저의 렌더링 엔진 : HTML과 동일한 해석 과정 (바이트 → 문자 → 토큰 → 노드 → CSSOM)을 거쳐 CSS를 파싱하여 CSSOM을 생성
-
CSSOM 생성 : DOM과 마찬가지로 상속 관계가 반영되어 생성됨

38.5. 렌더 트리 생성
DOM, CSSOM이 모두 생성되었다면 렌더링을 위해 렌더 트리로 결합되어야 한다.

렌더 트리 렌더링을 위한 트리 구조의 자료 구조 브라우저 화면에 렌더링되는 노드 만으로 구성됨 (ex. display: none 속성을 가진 노드들은 렌더 트리에 포함 x)

- 렌더 트리는 각 HTML 요소의 레이아웃을 계산하는데 사용
- 페인팅 : 브라우저 화면에 픽셀을 렌더링, 이때 렌더 트리가 입력되어 실행됨
이런 렌더링 과정은 언제 실행되는가?
- JS에 의한 노드 추가 or 삭제
- 브라우저 창의 리사이징에 의한 viewport 크기 변경
- HTML 요소의 레이아웃 변경을 일으키는 스타일 변경 (ex. width, height, margin, padding, border 등)
레이아웃 계산, 페인팅을 다시 실행해야 하기에 리렌더링은 비용이 많이 드는 작업이다.
⇒ 즉, 가급적 리렌더링이 빈번하게 발생되지 않도록 주의할 필요가 있다!
38.6. 자바스크립트 파싱과 실행
그렇다면, 이미 렌더링 된 요소들을 JS가 어떻게 동적으로 바꾸는가?
→ DOM API 활용
DOM API DOM이 제공하는 HTML 문서의 구조, 정보, 요소, 스타일을 변경할 수 있는 프로그래밍 인터페이스 ex.
document.getElementById()
해당 DOM API를 활용해 Javascript가 브라우저에서 어떻게 실행되는지 살펴보자

- 브라우저의 렌더링 엔진
- HTML을 순차적으로 해석하다가, JS 파일을 로드하거나 JS 코드를 담은
<script>태그를 만나면 DOM 생성을 일시 중단 <script>src 어트리뷰트의 JS 파일이 정의된 경우, 서버에게 JS 파일을 요청하여 로드- JS 코드를 파싱하기 위해 JS 엔진에 제어권을 넘김
- JS 파싱과 실행이 종료되면 다시 렌더링 엔진으로 제어권이 돌아오고 DOM 생성을 재개
- HTML을 순차적으로 해석하다가, JS 파일을 로드하거나 JS 코드를 담은
- 브라우저의 JS 엔진
- JS 코드를 파싱하여 CPU가 이해할 수 있는 저수준 언어로 변환하고 실행하는 역할을 수행
- 브라우저마다 다양한 종류가 있으며, 모든 JS 엔진은 ECMAScript 사양을 준수 더 자세히 알아보고 싶다면
- 제어권을 넘겨 받은 후 어떻게 실행하는가?
- JS를 해석하여 AST(Abstract Syntax Tree)를 생성
- 토크나이징 : 단순한 문자열인 JS 소스코드를 토큰으로 분해
- 파싱 : 토큰들의 집합을 syntax analysis하여 AST를 생성
+) AST를 활용하여 Transpiler를 구현할 수도 있다. (더 자세히 직접 사용해보자면… )
Transpiler (이해하기 쉬운 아티클 ) 코드를 변환하는 도구 다양한 문법, 프레임워크 등을 브라우저가 이해할 수 있는 Javascript로 바꿔주는 도구 ex) Babel, SWC, Typescript 등
- AST를 기반으로 인터프리터가 실행할 수 있는 중간 코드인 바이트코드를 생성하여
- 인터프리터가 바이트코드를 실행
- ex. v8 엔진 : 터보팬이라 불리는 컴파일러에 의해 최적화된 머신 코드로 컴파일되어 성능을 최적화, 해당 코드의 사용 빈도가 적어지면 다시 deoptimizing 하기도 함! 더 자세히 알고 싶다면..
- JS를 해석하여 AST(Abstract Syntax Tree)를 생성
38.7. 리플로우와 리페인트
위에서 살펴본 DOM API를 활용하여 DOM, CSSOM이 변경되면?

- DOM API에 의해 변경된 DOM, CSSOM들이 다시 렌더 트리로 결합
- 변경된 렌더 트리를 기반으로 레이아웃과 페인트 과정을 거쳐 브라우저의 화면에 리렌더링
리플로우 레이아웃 계산을 다시 하는 것 → 노드 추가/삭제, 요소의 크기/위치 변경, 윈도우 리사이징 등 레이아웃에 영향을 주는 변경이 발생할 때만 실행
리페인트 재결합된 렌더 트리를 기반으로 다시 페인트 과정을 거치는 것
무조건 순차적으로 동시에 실행되는 것은 아니고, 레이아웃에 영향이 없는 변경은 리페인트만 실행된다.
38.8. 자바스크립트 파싱에 의한 HTML 파싱 중단
지금까지 살펴본 바와 같이, 브라우저의 렌더링 엔진과 JS 엔진은 직렬적으로 파싱을 수행
즉, synchronous하게 위에서 아래 방향으로 HTML, CSS, JS를 파싱하고 실행
⇒ <script> 위치에 따라 HTML 파싱이 blocking되어 DOM 생성이 지연될 수 있다는 것을 의미

위의 예제에서는 app.js의 파싱과 실행 이전까지는 DOM의 생성이 일시 중단된다.
이때, app.js에서 DOM API를 사용할 경우 이미 DOM, CSSOM이 생성되어 있어야 한다.
해당 예제에서는 DOM의 생성이 완료되지 않은 상태일 수 있기에 문제가 발생할 수 있다!
다음 예제도 살펴보자
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="style.css" />
<script>
const $apple = document.getElementById('apple');
$apple.style.color = 'red'; // TypeError : Cannot read property 'style' of null
</script>
</head>
<body>
<ul>
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</body>
</html>DOM API인 document.getElementById('apple'); 은 DOM에서 id가 ‘apple’인 HTML 요소를 취득한다. 하지만, 아직 id가 ‘apple’인 HTML 요소를 파싱하지 않았기 때문에 정상적으로 동작할 수 없다.
body 요소의 가장 아래에 JS를 위치하자
위의 예제를 수정해보자
<!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>
<li id="orange">Orange</li>
</ul>
<script>
const $apple = document.getElementById('apple');
$apple.style.color = 'red';
</script>
</body>
</html>script 내부의 JS 코드가 실행될 시점에는, 이미 렌더링 엔진이 HTML 요소를 모두 파싱하여 DOM 생성을 완료한 이후이기에 정상적으로 동작한다.
또, DOM이 모두 생성이 완료된 후 JS를 실행하기에 페이지 로딩 시간이 단축될 수 있다.
38.9. script 태그의 async/defer 어트리뷰트
앞선 38.8장의 문제를 근본적으로 해결하기 위해
HTML5부터 <script>에 async, defer 어트리뷰트가 추가되었다.
다음과 같이 외부 JS 파일을 로드하는 경우에만 사용할 수 있다. 인라인 JS에서는 사용이 어렵다.
<script async src="extern.js"></script>
<script defer src="extern.js"></script>async 어트리뷰트
JS의 파싱과 실행이 JS 파일의 로드가 완료된 직후진행되며, HTML 파싱이 중단된다.

여러 개의 script 태그에 async 어트리뷰트를 지정하면 script 태그의 순서가 보장되지 X
defer 어트리뷰트
JS의 파싱과 실행이 DOM 생성이 완료된 직후 진행된다.

따라서 대부분의 서비스 코드에서는 defer 사용이 기본 선택지가 된다.
추천 아티클 연계
Naver D2 – 브라우저는 어떻게 동작하는가 시리즈 1,2,3,4 가볍게 읽어보고 와주세요! 스터디 당일에는 3,4를 집중적으로 딥다이브🤿 해보아요~
퀴즈
퀴즈 1 — HTML 파싱 중단 시점
<!DOCTYPE html>
<html>
<head>
<script src="a.js"></script>
</head>
<body>
<p>Hello</p>
</body>
</html>Q. DOM 생성은 언제 중단되는가?
퀴즈 2 — async vs defer 동작 판단
<script async src="a.js"></script>
<script async src="b.js"></script>Q. 실행 순서는 보장되는가? 왜?
퀴즈 3 — 리플로우 발생 여부
element.style.color = 'red';Q. 리플로우가 발생하는가?
퀴즈 4 — DOM 접근 오류 원인
<head>
<script>
document.getElementById('box').style.width = '100px';
</script>
</head>
<body>
<div id="box"></div>
</body>Q. 에러 발생 원인은? 해결 방법은?