브라우저의 렌더링 과정
HTML 파일은 어떻게 브라우저에 그려지는지에 대하여 명확하게 답을 하지 못하기에 오늘은 브라우저의 렌더링 과정에 대해서 공부하고 정리해보려고 한다.
모자딥다에 나와있는 브라우저의 렌더링 과정
1. 브라우저는 HTML, CSS, 자바스크립트, 이미지, 폰트 파일 등 렌더링에 필요한 리소스를 요청하고 서버로부터 응답을 받는다.
2. 브라우저의 렌더링 엔진은 서버로부터 응답된 HTML과 CSS를 파싱 하여 DOM과 CSSOM을 생성하고 이들을 결합하여 렌더 트리를 생성한다.
3. 브라우저의 자바스크립트 엔진은 서버로부터 응답된 자바스크립트를 파싱하여 AST(Abstract Syntax Tree)를 생성하고 바이트코드로 변환하여 실행한다. 이때 자바스크립트는 DOM API를 통해 DOM이나 CSSOM을 변경할 수 있다. 변경된 DOM과 CSSOM은 다시 렌더 트리로 결합된다.
4. 렌더 트리를 기반으로 HTML 요소의 레이아웃을 계산하고 화면에 HTML 요소를 페인팅 한다.
요청과 응답
브라우저의 핵심 기능은 HTML, CSS, 자바스크립트, 이미지, 폰트 등 리소스를 서버에 요청하고 서버로부터 응답받아 리소스를 파싱 하여 브라우저에 시각적으로 렌더링 한다.
서버에 요청을 전송하기 위해 브라우저의 주소창에 URL을 입력하여 엔터키를 누르면, URL의 호스트 이름이 DNS를 통해 IP 주소로 변환되고 이 IP 주소를 갖는 서버에게 요청을 전송한다.
예를 들어, 주소창에 https://www.google.com 을 검색하면 루트 요청(/, 스킴과 호스트 만으로 구성된 URI에 의한 요청)이 www.google.com 서버로 전송된다. 서버는 루트 요청에 암묵적으로 index.html을 응답하도록 기본 설정돼있기 때문에 https://www.google.com 을 검색하면 https://www.google.com/index.html 을 요청하는 것과 같다. 만약 다른 파일을 요청하고 싶다면 뒤에 다른 파일 경로를 적거나 자바스크립트를 통해서 정적/동적으로 요청할 수도 있다.
개발자 도구의 네트워크창을 열어서 보면 서버에 요청을 보내고 응답을 받는 과정을 확인할 수 있다.
자세히 보면 index.html뿐만 아니라 요청한 적 없는 리소스들이 응답된 것을 확인할 수 있다. 이는 브라우저의 렌더링 엔진이 HTML을 파싱 하는 도중에 외부 리소스를 로드하는 태그(CSS 파일을 로드하는 <link> 태그, 이미지 파일을 로드하는 <img> 태그, 자바스크립트를 로드하는 <script> 태그 등)를 만나면 HTML의 파싱을 일시 중단하고 해당 리소스 파일을 서버로 요청하기 때문이다.
HTML 파싱과 DOM 생성
HTML 문서는 텍스트로만 이루어져 있다. 브라우저 렌더링 엔진은 이 문서를 브라우저가 이해할 수 있는 형태로 파싱 하여 DOM을 생성한다
1. 바이트(Bytes) : 서버에 존재하던 HTML 파일이 브라우저의 요청에 의해 응답. 이때 서버는 브라우저가 요청한 HTML 파일을 읽어 메모리에 저장한 다음 메모리에 저장된 바이트(2진수)를 인터넷을 경유하여 응답.
2. 문자열(Characters) : 브라우저는 서버가 응답한 HTML 문서를 바이트 형태로 응답받음. 응답된 문서는 meta 태그의 charset 어트리뷰트에 의해 지정된 인코딩 방식을 기준으로 문자열로 변환됨. 브라우저는 이를 확인하고 문자열로 변환함.
3. 토큰(Tokens) : 문자열로 변환된 문서를 읽어 들여 문법적 의미를 갖는 최소 단위인 토큰들로 분해.
4. 노드(Nodes) : 각 토큰들을 객체로 변환하여 노드를 생성함. 토큰의 내용에 따라 문서 노드, 요소 노드, 어트리뷰트 노드, 텍스트 노드가 생성됨. 노드는 이후 DOM을 구성하는 기본 요소가 됨.
5. DOM(Document Object Model) : HTML 문서는 HTML 요소들의 집합으로 이루어지며 HTML 요소는 중첩 관계를 가짐. 즉, HTML 요소의 콘텐츠 영역(시작 태그와 종료 태그 사이)에는 텍스트뿐만 아닌 다른 HTML 요소도 포함될 수 있음. 이때 HTML 요소 간에는 중첩 관계에 의해 부자 관계가 형성되고 이러한 관계를 반영하여 모든 노드들을 트리 자료구조로 구성함. 이 노드들로 구성된 트리 자료구조를 DOM이라 부름.
DOM === HTML 문서를 파싱 한 결과물
왜 굳이 HTML을 이렇게까지 복잡하게 바꿔야 할까? 브라우저는 자바스크립트 언어만 알아듣는데, 자바스크립트는 HTML의 태그나 속성들을 다룰 수 없기 때문에, 다룰 수 있는 형태인 '객체'로 바꿔주어야 하기 때문이다.
CSS 파싱과 CSSOM 생성
앞서 얘기했듯이 렌더링 엔진이 HTML을 한 줄씩 순차적으로 파싱 하여 DOM을 생성해 나가다가 CSS를 로드하는 <link> 태그나 <style> 태그를 만나면 DOM 생성을 일시 중단한다. 그리고 <link> 태그의 href 어트리뷰트에 지정된 CSS 파일을 서버에 요청하여 로드한 CSS 파일이나 <style> 태그 내의 CSS를 HTML과 동일한 파싱 과정을 거치며 해석하여 CSSOM을 생성한다. 이후 CSS 파싱을 완료하면 HTML 파싱이 중단된 지점부터 다시 HTML을 파싱 하기 시작하여 DOM 생성을 재개한다.
바이트 -> 문자 -> 토큰 -> 노드 -> CSSOM(CSS Object Model)
CSSOM은 CSS의 상속을 반영하여 생성된다.
body {
font-size: 18px;
}
ul {
list-style-type: none;
}
위 코드에서 body 요소에 적용된 font-size, ul 요소에 적용된 list-style-type은 모든 li 요소에 상속된다.
렌더 트리 생성
렌더링 엔진은 DOM과 CSSOM을 생성하고 DOM과 CSSOM은 렌더링을 위해 렌더 트리로 결합된다.
렌더 트리는 렌더링을 위한 트리 구조의 자료구조다. 따라서 브라우저 화면에 렌더링 되지 않는 노드(meta, script 태그 등)와 CSS에 의해 비표시(display: none)되는 노드들은 포함하지 않는다. 말 그대로 화면에 렌더링 되는 노드만으로 구성된다. (단 visibility: hidden은 레이아웃 트리에 포함된다고 한다)
이후 완성된 렌더 트리는 각 HTML 요소의 레이아웃을 계산하는 데 사용되며 브라우저 화면에 픽셀을 렌더링 하는 페인팅 처리에 입력된다.
자바스크립트 파싱과 실행
HTML 문서를 파싱 한 결과로 생성된 DOM은 HTML 문서의 구조와 정보뿐만 아니라 HTML 요소와 스타일 등을 변경할 수 있는 프로그래밍 인터페이스로서 DOM API를 제공한다. 즉, 자바스크립트 코드에서 DOM API를 사용하면 생성된 DOM을 동적으로 조작할 수 있다.
CSS 파싱과 마찬가지로 렌더링 엔진은 자바스크립트 파일을 로드하는 <script> 태그나 자바스크립트 코드를 콘텐츠로 담은 <script> 태그를 만나면 DOM 생성을 일시 중단한다. 그러고 나서 src 어트리뷰트에 정의된 자바스크립트 파일을 서버에 요청하여 로드한 자바스크립트 파일이나 <script> 태그 내의 자바스크립트 코드를 파싱 하기 위해 자바스크립트 엔진에 제어권을 넘긴다. 이후 자바스크립트 파싱과 실행이 종료되면 렌더링 엔진으로 다시 제어권을 넘겨 HTML 파싱이 중단된 지점부터 다시 HTML 파싱을 시작하여 DOM 생성을 재개한다.
자바스크립트 파싱과 실행은 렌더링 엔진이 아닌 자바스크립트 엔진이 담당한다. 자바스크립트 엔진은 코드를 파싱 하여 CPU가 이해할 수 있는 저수준 언어로 변환하고 실행하는 역할을 한다. 자바스크립트 엔진은 구글 크롬, Node.js의 V8, 파이어폭스의 SpiderMonkey, 사파리의 JavaScriptCore 등 다양한 종류가 있으며, 모든 자바스크립트 엔진은 ECMAScript 사양을 준수한다.
렌더링 엔진이 HTML, CSS를 파싱하여 DOM, CSSOM을 생성하듯, 자바스크립트 엔진은 자바스크립트를 해석하여 AST(Abostract Syntax Tree : 추상적 구문 트리)를 생성한다. 그리고 AST 기반으로 인터프리터가 실행할 수 있는 중간 코드인 바이트코드를 생성하여 실행한다.
토크나이징 : 단순한 문자열인 자바스크립트 코드를 어휘 분석하여 문법적 의미를 갖는 코드의 최소단위인 토큰들로 분해한다. 이 과정을 렉싱이라고 부르기도 하지만 토크나이징과 미묘한 차이가 있다.
파싱 : 토큰들의 집합을 구문 분석하여 AST를 생성한다. AST는 토큰에 문법적 의미와 구조를 반영한 트리 구조의 자료구조이다. AST는 인터프리터나 컴파일러만이 사용하는 것은 아니다. AST를 사용하면 TypeScript, Babel, Prettier 같은 트랜스파일러를 구현할 수도 있다.
바이트코드 생성과 실행 : 파싱의 결과물로 생성된 AST는 인터프리터가 실행할 수 있는 중간 코드인 바이트고 드로 변환되고 인터프리터에 의해 실행된다. 참고로 V8 엔진의 경우 자주 사용되는 코드는 터보팬이라 불리는 컴파일러에 의해 최적화된 머신 코드로 컴파일되어 성능을 최적화한다. 만약 코드의 사용 빈도가 적어지면 다시 디옵티마이징하기도 한다.
마무리
1. 브라우저는 HTML, CSS, JS, 이미지, 폰트 등 리소스를 서버에 요청하고 응답으로 받아온다.
2. 브라우저 렌더링 엔진은 HTML, CSS를 파싱해 DOM, CSSOM을 생성하고 결합해 렌더 트리를 생성한다.
3. 자바스크립트 엔진은 자바스크립트를 파싱해 AST를 생성하고 바이트코드로 변환하여 실행한다.
4. 렌더 트리를 기반으로 HTML 요소의 레이아웃을 계산한다.
5. 화면에 HTML 요소를 페인팅 한다.