요청과 응답
브라우저 렌더링 과정을 큰 틀에서 보면 다음 사진과 같다.
왼쪽과 같이 사용자가 해당 URL을 입력하면 오른쪽 사진과 같은 과정으로 렌더링이 진행된다.
(자세히 보면 DNS를 통해 IP주소를 파악하는 등의 과정이 있지만, 큰 흐름만 보면 HTML 파일을 서버에 요청하고 해당하는 파일을 응답받고, CSS 파일을 요청하고 해당 파일을 응답받고, JS도 동일하게 진행된다.)
HTML을 브라우저가 아닌 메모장 같은 응용 프로그램으로 열어보면 왼쪽과 같이 일반 문자열로 나타난다.
이처럼 브라우저가 요청을 해서 서버가 응답으로 보내주는 HTML 문서는 문자열로 이루어진 순수한 텍스트이다.
따라서 브라우저가 이를 이해하고 사용자에게 최종적으로 보여주기 위해서는
브라우저가 이해하도록 번역하는 과정이 필요하다.
이렇게 브라우저가 이해할 수 있는, 노드들로 구성된 트리 자료구조를 DOM(Document Object Model) 이라고 한다.
즉, DOM은 HTML 문서를 파싱한 결과물이다.
렌더링 엔진은 처음부터 한 줄씩 순차적으로 파싱하며 DOM을 생성한다.
이때, 중간에 CSS를 로드하는 link 태그 또는 style 태그를 만나면 DOM 생성을 일시 중단한다.
그 후 DOM을 만들 듯 CSS 파일을 서버에 요청하고 응답받은 CSS 파일을 HTML과 동일한 파싱 과정을 거치며 해석해 CSSOM(CSS Object Model)을 생성한다.
이후 CSS 파싱이 완료되면, HTML 파싱이 중단된 시점으로 돌아가 다시 HTML을 파싱한다.
DOMrhk CSSOM이 생성되면 이 둘은 렌더링을 위해 렌더 트리(Render Tree)로 결합된다.
렌더 트리는 브라우저 화면에 보여지지 않는 것들은 포함하지 않는다.
(HTML meta 태그, CSS의 display: none 등)
즉, 렌더 트리는 브라우저 화면에 렌더링되는 노드만으로 구성된다.
이후 렌더 트리는 HTML 요소의 레이아웃(위치와 크기)을 계산하는 데 사용되며,
브라우저 화면에 픽셀을 렌더링하는 페인팅 처리에 입력된다.
DOM은 HTML의 구조 및 정보뿐만 아니라 HTML 요소를 제어할 수 있는 DOM API를 제공한다.
즉, DOM API는 DOM의 각 노드와 상호작용하기 위한 인터페이스, 또는 HTML을 JS에서 제어하기 위한 명령들의 집합이며, 대표적으로 자바스크립트에서 자주 사용하는 document.querySelector()
등을 예로 들 수 있다.
이러한 DOM API를 사용하면 이미 생성된 DOM을 동적으로 조작할 수 있다.
CSS를 파싱할 때 <link>
나 <script>
를 만나면 그 아래의 DOM 생성을 멈추고 CSSOM을 생성했던 것처럼,
<script>
태그도 동일하다.
다른 점은 CSSOM도 렌더링 엔진이 만들었다면, <script>
태그 내의 자바스크립트 코드를 파싱할 때는 렌더링 엔진이 자바스크립트 엔진에게 제어권을 넘긴다. (이를 blocking이 일어났다고도 함)
이후 자바스크립트 파싱과 실행이 종료되면, 렌더링 엔진으로 다시 제어권이 넘어가 HTML 파싱이 중단된 시점부터 다시 시작하여 DOM 생성을 재개한다.
자바스크립트 엔진이 처리하는 자바스크립트의 파싱과 실행은 큰 단락으로 보면 위 사진과 같이 구분할 수 있다.
소스코드를 토큰으로 분해하고, 파싱해 AST라는 추상적 구문 트리로 생성한 뒤 인터프리터가 읽을 수 있도록
바이트 코드를 생성하여 실행한다.
이 때, 자바스크립트 코드에 DOM이나 CSSOM을 변경하는 DOM API가 사용된 경우,
DOM이나 CSSOM이 변경된다.
변경된 DOM과 CSSOM은 다시 렌더 트리로 결합되며 리렌더링 되는데, 이를 리플로우(reflow, 레이아웃 계산을 다시 해주는 것)과 리페인트(repaint, 재결합된 렌더 트리를 기반으로 다시 페인트하는 것)라고 한다.
<script src=””></script>
코드를 만나면 위에서 아래로 동기적으로 이루어지던 파싱이 중단된다.
이는 <script>
태그 위치에 따라 HTML 파싱이 블로킹되어 DOM 생성이 지연될 수 있다는 의미이므로
<script>
태그의 위치는 중요한 의미를 갖는다.
[참고] 동기와 비동기의 의미
동기적(synchronous):
어떤 작업을 요청했을 때, 그 작업이 종료될 때까지 기다린 후 다음 작업을 수행하는 방식
비동기적(asynchronous):
어떤 작업을 요청했을 때, 그 작업이 종료될 때까지 기다리지 않고 다른 작업을 하고 있다가
요청했던 작업이 종료되면 그에 대한 추가 작업을 수행하는 방식
위와 같이 DOM API를 사용하는 코드가 <script>
안에 있는데, 이 <script>
태그가 <body>
태그보다 위에 위치한다면 getElementById(’apple’)
에서 HTML 엘리먼트가 만들어지지 않았으므로
ID가 apple인 요소를 찾지 못하고, 아래의 style도 실행할 수 없다.
따라서 위와 같은 오류가 발생하게 된다.
이전의 코드를 위와 같이 <script>
태그가 <body>
태그 아래에 위치하도록 수정한다면,
<script>
태그로 인한 HTML 블로킹이 발생하지 않아 HTML 요소가 다 만들어진 뒤 <script>
태그가 실행된다.
따라서 <body>
요소의 가장 아래에 <script>
태그를 위치하는 것이 상대적으로 더 권장된다.
DOM이 완성된 후 JS가 DOM을 조작하니 엘리먼트를 찾지 못하는 오류도 없고, HTML 블로킹이 없어 페이지 로딩 시간이 단축되기 때문이다.
사용자가 웹사이트에 접속하여 주소를 기반으로 IP를 파악하고 서버에 요청, 응답한 HTML을 파싱해
DOM 트리를 만들고, CSSOM까지 더해 Render Tree를 생성해 주는 1~5의 과정을 Construction이라고 한다.
렌더 트리가 만들어진 후, 그 안에 있는 node를 배치하고(Layout), UI를 그리고(Paint),
순서대로 구성(Composition)한 뒤, 결과물을 출력하는 6~9의 과정을 Operation이라고 한다.