브라우저는 다음과 같은 과정을 거쳐 렌더링을 수행한다.
1. 브라우저는 HTML, CSS, JavaScript, 이미지, 폰트 등 렌더링에 필요한 리소스를 요청하고 서버로부터 응답을 받는다.
2. 브라우저의 렌더링 엔진은 서버로부터 응답된 HTML과 CSS를 파싱하여 DOM과 CSSOM을 생성하고 이들을 결합해 렌더 트리를 생성한다.
3. 브라우저의 자바스크립트 엔진은 서버로부터 응답된 자바스크립트를 파싱하여 Abstract Syntax Tree를 생성하고 바이트코드로 변환하여 실행한다. 이때 자바스크립트는 DOM API를 통해 DOM이나 CSSOM을 변경할 수 있다. 변경된 DOM과 CSSOM은 다시 렌더 트리로 결합된다.
4. 렌더 트리를 기반으로 HTML 요소의 레이아웃(위치, 크기)를 계산하고 브라우저 화면에 HTML요소를 페인팅한다.
렌더링에 필요한 리소스는 모두 서버에 존재하므로 필요한 리소스를 서버에 요청하고 서버가 응답한 리소스를 파싱하며 렌더링한다.
브라우저 주소창에 URL을 입력하고 엔터를 누르면 URL의 호스트 이름이 DNS를 통해 IP 주소로 변환되고 이 IP 주소를 갖는 서버에게 요청을 전송한다.
예를 들어, 브라우저의 주소창에 https://poiemaweb.com
을 입력하고 엔터 키를 누르면 루트 요청(/, 스킴과 호스트만으로 구성된 URI에 의한 요청)이 https://poiemaweb.com
서버로 전송된다. 일반적으로 서버는 루트 요청에 대해 암묵적으로 index.html을 응답하게 되어 있으므로 https://poiemaweb.com
은 https://poiemaweb.com.html
과 같은요청이다.
만약 index.html
이 아닌 다른 정적 파일을 서버에 요청하면 브라우저의 주소창에 https://poiemaweb.com/assets/data/data.json
과 같이 요청할 정적 파일의 경로와 파일 이름을 URI의 호스트 뒤의 패스에 기술하여 서버에 요청한다. 그러면 서버는 루트 폴더의 assets/data 폴더 내에 있는 data.json을 응답한다.
하지만, Ajax와 REST API를 사용해서 정적/동적 데이터를 요청할 수 있다.
다음과 같이 네트워크 탭을 이용해서 정적파일을 확인할 수 있다.
하지만, index.html 뿐 만 아니라 CSS, JavaScript, 이미지, 폰트 파일들도 응답된것을 확인할 수 있다. 그 이유는 브라우저 렌더링 엔진이 HTML을 파싱하는 도중에 CSS, JavaScript와 같은 외부 리소스가 필요한 경우 HTML 파싱을 일시 중단하고 해당 리소스 파일을 서버러 요청하기 때문이다.
HTTP는 웹에서 브라우저와 서버가 통신하기 위한 프로토콜이다.
HTTP/1.1
따라서, HTML 문서 내에 포함된 여러 개의 리소스 요청(CSS, JavaScript 등)이 개별적으로 전송되고 응답 또한 개별적으로 전송된다. 리소스 개수에 비례하여 응답 시간도 증가한다.
HTTP/2.0
브라우저의 요청에 의해 서버가 응답한 HTML문서는 문자열로 이루어진 순수한 텍스트다. 순수한 텍스트인 HTML문서를 시각적인 픽셀로 렌더링하려면 HTML 문서를 브라우저가 이해할 수 있는 자료구조로 변환하여 메모리에 저장해야 한다.
브라우저의 렌더링 엔진은 응답받은 HTML 문서를 파싱하여 브라우저가 이해할 수 있는 DOM을 생성한다.
렌더링 엔진은 HTML을 처음부터 한 줄씩 순차적으로 파싱하여 DOM을 생성한다. 생성 과정에서 CSS를 로드하는 link태그나 style 태그를 만나면 DOM 생성일 일시 중단한다. 그 후, CSS 파일을 서버에 요청하여 로드한 CSS 파일이나 style 태그 내의 CSS를 HTML과 동일하게 바이트 -> 문자 -> 토큰 -> 노드 -> CSSOM 을 거치며 해석하여 CSSOM을 생성한다. CSS 파싱을 완료하면 HTML 파싱이 중단되 지점부터 다시 HTML을 파싱하기 시작하여 DOM 생성을 재개한다.
DOM과 CSSOM 파싱을 완료하면 렌더링을 위해 렌더 트리로 결합한다.
렌더 트리에는 우저 화면에 렌더링되지 않는 meta태그나 script 태그, CSS의 display:none에 의해 비표시되는 노드들을 제외한 노드들이 포함된다.
이후 완성된 렌더 트리는 각 HTML 요소의 레이아웃(위치, 크기)을 계산하는데 사용되며 브라우저 화면에 픽셀을 렌더링하는 페인팅 처리에 입력된다.
이런 렌더링 과정은 반복해서 실행될 수 있다. 레이아웃과 페인팅을 다시 실행하는 리렌더링은 비용이 많이 든다. 따라서 지양해야 한다.
DOM은 프로그래밍 인터페이스로서 DOM API를 제공한다. 이를 사용해 이미 생성된 DOM을 동적으로 조작할 수 있다.
렌더링 엔진은 HTML을 한 줄씩 순차적으로 파싱하며 DOM을 생성해나가다가 자바스크립트 파일을 로드하거나 자바스크립트 코드를 콘텐츠로 담은 script태그를 만나면 DOM 생성을 일시적으로 중단한다.
자바스크립트 파싱과 실행은 브라우저의 렌더링 엔진이 아닌 자바스크립트 엔진이 처리한다. 자바스크립트 엔진은 자바스크립트 코드를 파싱하여 CPU가 이해할 수 있는 low level language로 변환한다. 렌더링으로부터 제어권을 넘겨받은 자바스크립트 엔진은 자바스크립트 코드를 파싱한다. 이를 해석하며 Abstract Syntax Tree를 생성한다. 그리고, AST를 기반으로 인터프리터가 실행할 수 있는 중간 코드인 바이트코드를 생성하여 실행한다. 자세한 절차는 다음과 같다.
토크나이징
문자열인 자바스크립트 코드를 어휘 분석하여 문법적 의미를 갖는 코드의 최소 단위인 토큰들로 분해한다. Lexing이라고도 부른다.
파싱
토큰들의 집합을 구문 분석하여 AST를 생성한다. AST는 토큰에 문법적 의미와 구조를 반영한 트리 구조의 자료구조다. AST를 활용하면 TypeScript, Babel, Prettierr 같은 트랜스파일러를 구현할 수도 있다.
바이트코드 생성과 실행
AST는 인터프리터가 실행할 수 있는 중간 코드인 바이트코드로 변환되고 인터프리터에 의해 실행된다.
자바스크립트 코드에 DOM이나 CSSOM을 변경하는 DOM API가 사용된 경우 이것들이 변경되며 다시 렌더 트리로 결합되고 변경된 렌더 트리를 기반으로 레이아웃과 페인트 과정을 거쳐 브라우저의 화면에 다시 렌더링된다. 이를 리플로우, 리페인트라 한다.
리플로우는 레이아웃 계산을 다시 하는 것을 말하며, 노드 추가/삭제, 요소의 크기/위치 변경, 윈도우 리사이증 등 레이아웃에 영향을 주는 변경이 발생한 경우에 실행된다.
리페인트는 재결합된 렌더 트리를 기반으로 다시 페인트를 하는것이다.
즉, 리플로우 없이 리페인트만 실행되는 경우도 있다.
렌더링 엔진과 자바스크립트 엔진은 직렬적으로 파싱을 수행한다.
즉, 브라우저는 동기적으로 HTML, CSS, JavaScript를 파싱하고 실행한다. 이러한 이유로 script 태그의 위치에 따라 HTML 파싱이 블로킹되어 DOM 생성이 지연될 수 있다.
script 태그를 body요소의 가장 아래에 위치시켜 이를 해결할 수 있다. 이렇게 위치를 바꾸면 자바스크립트가 실행될 시점에는 이미 렌더링 엔진이 HTML 요소를 모두 파싱하여 DOM 생성을 완료한 이후다. 따라서, DOM이 완성되지 않은 상태에서 자바스크립트가 DOM을 조작하는 에러가 발생할 우려가 없다. 또한, 자바스크립트 실행 전에 DOM 생성이 완료되어 렌더링되므로 페이지 로딩 시간이 단축된다.
async는 의존성이 없는 스크립트에, defer는 DOM제어와 관련이 있는 스크립트에 사용한다.