[JS] DeepDive(38) 브라우저의 렌더링 과정
FrontEnd/Deep Dive

[JS] DeepDive(38) 브라우저의 렌더링 과정

728x90

구글의 V8 자바스크립트 엔진으로 빌드된 JS 러낱임 환경인 node.js 가 등장하면서 자바스크립트는 웹 브라우저를 벗어나 서버 사이드 애플리케이션으로도 사용할 수 있는 언어가 되었다.

 

하지만 여전히 자바스크립트가 사용되는 분야는 웹 브라우저에서 동자갛는 클라이언트 사이드다.

 

대부분 프로그래밍 언어는 운영체제나 가상 머신 위에서 실행되지만 웹 애플리케이션의 클라이언트 사이드 자바스크립트는 브라우저에서 HTML,CSS와 함께 실행된다.

 

이번에는 브라우저가 HTML,CSS JS로 작성된 텍스트 문서를 어떻게 해석하여 렌더링하는지 서술해보겠다.

 

 

 

 

 

 

브라우저는 아래와 같은 과정을 거쳐 렌더링을 수행한다.

 

1. 브라우저는 HTML,CSS,JS,image,폰트 파일 등 렌더링에 필요한 리소스를 요청하고 서버로부터 응답을 받는다.

2. 브라우저의 렌더링 엔진은 응답한 HTML,CSS를 파싱하여 DOM과 CSSOM을 새엇ㅇ한다.

3. 브라우저의 JS엔진은 JS를 파싱하여 AST를 생성하고 바이트코드로 변환하여 실행한다. 이때 JS는 DOM API를 통해서 DOM이나 CSSOM을 변경할 수 있다.

4. 렌더 트리를 기반으로 HTML의 레이아웃을 계산하고 브라우저 화면에 HTML요소를 페인팅한다.

 

 

 

 

요청과 응답

 

 

브라우저의 핵심 기능은 필요한 리소스를 서버에 요청하고 서버로부터 받아서 브라우저에 렌더링 하는 것이다. 이를 위해서 브라우저는 주소창을 제공한다.

 

 

 

 

반드시 브라우저의 주소창을 통해서 정적 파일을 요청할 수 있는것은 아니며 정적/동적 데이터를 동적으로 요청하는 것도 가능하다.

 

개발자 도구의 Network패널을 활성화하기 전에 브라우저가 이미 응답을 받았다면 리소스가 표시되지 않으며 이 경우 페이지를 새로고침해야 한다.

 

 

 

HTTP 1.1 & HTTP 2.0

 

HTTP는 웹에서 브라우저와 서버가 통신하기 위한 프로토콜(규약)이다. 

 

HTTP/1.1은 기본적으로 커넥션 당 하나의 요청과 응답만 처리한다. 따라서 요청할 리소스의 개수에 비례해 응답 시간이 증가하는 단점이 있다.

 

 

 

하지만 HTTP/2.0은 위 예제에서 볼 수 있다시피 다중 요청/응답이 가능하다.

 

 

 

HTML파싱과 DOM 생성

 

브라우저 요청에 의해 서버가 응답한 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을 생성한다.

 

 

 

CSS 파싱과 CSSOM 생성

 

렌더링 엔진은 HTML을 처음부터 한줄씩 순차적으로 파싱하여 DOM을 생성해 나간다. 렌더링 엔진은 DOM을 생성하다가 link태그나 style태그를 만나면 DOM 생성을 잠깐 멈춘다.

 

이어서 로드한 css파일이나 style태그 내의  CSS를 HTML과 동일한 파싱과정(바이트 => 문자 => 토큰 => 노드 => CSSOM)을 거치며 해석하며 CSSOM을 생성한다.

 

방금 본 파일에 link태그가 존재한다.

 

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css">
...

 

 

 

만약 아래와 같은 css파일이 응답되었다고 생각해보자.

 

body {
  font-size: 18px;
}

ul {
  list-style-type: none;
}

 

 

CSSOM은 CSS의 상속을 반영하여 생성된다. 따라서 아래와 같은 CSSOM이 생성된다.

 

 

 

 

 

 

렌더 트리 생성

 

 

렌더링 엔진은 서버로부터 응답된 HTML과 CSS를 파싱하여 각각 DOM 과 CSSOM을 생성한다. 이들은 렌더링을 위한 렌더 트리로 결합된다.

 

렌더 트리는 렌더링을 위한 트리구조의 자료구조이다. 따라서 브라우저에 렌더링되지 않는 노드와 CSS에 의해 비표시 되는 노드들은 포함되지 않게 된다.

 

 

 

 

이후 완성된 렌더 트리는 각 HTML요소의 레이아웃을 계산하는데 사용되며 브라우저 화면에 픽셀을 렌더링하는 페인팅 처리에 입력되게 된다.

 

 

 

 

 

JS 파싱과 실행

 

 

HTML 문서를 파싱한 결과물로써 생성된 DOM은 구조,정보 뿐아니라 HTML요소를 직접적으로 변경할 수 있는 DOM API 를 제공한다.

 

script태그의 src어트리뷰트에 정의된 JS 파일을 서버에 요청하여 로드한 JS 파일이나 script태그 내의 JS 코드를 파싱하기 위해 자바스크립트 엔진에 제어권을 넘긴다.

 

렌더링 엔진으로부터 제어권을 넘겨받은 JS엔진은 JS코드를 파싱하며 JS를 해석하여 AST(추상적 구문 트리)를 생성한다.

 

 

 

토크나이징

단순한 문자열인 JS소스코드를 어휘분서갛여 문법적 의미를 가진 토큰들로 분해한다.

 

 

 

 

파싱


토큰들의 집합을 구문 분석하여 AST(Abstract Syntax Tree)를 생성한다. AST는 토큰에 문법적 의미와 구조를 반영한 트리 구조의 자료구조다. AST는 인터프리터나 컴파일러만이 사용하는 것은 아니다. AST를 사용하면 TypeScript, Babel, Prettier 같은 트랜스파일러를 구현할 수도 있다.


바이트코드 생성과 실행


파싱의 결과물로서 생성된 AST는 인터프리터가 실행할 수 있는 중간 코드인 바이트코드로 변환되고 인터프리터에 의해 실행된다.

 


리플로우와 리페인트

 


만약 자바스크립트 코드에 DOM이나 CSSOM을 변경하는 DOM API가 사용된 경우 DOM이나 CSSOM이 변경된다. 이때 변경된 DOM이나 CSSOM은 다시 렌더 트리로 결합되고 변경된 렌더 트리를 기반으로 레이아웃과 페인트 과정을 거쳐 브라우저의 화면에 다시 렌더링된다. 이를 리플로우, 리페인트라 한다.


리플로우는 레이아웃 계산을 다시 하는 것을 말하며, 노드 추가/삭제, 요소의 크기/위치 변경, 윈도우 리사이징 등 레이아웃에 영향을 주는 변경이 발생한 경우에 한하여 실행된다.

리페인트는 재결합된 렌더 트리를 기반으로 다시 페인트를 하는 것을 말한다.


script 태그의 async/defer 어트리뷰트

 

<!DOCTYPE html>
<html> 
  <head> 
    <meta charset="UTF-8"> 
    <link rel="stylesheet" href="style.css"> 
    <script> 
      /* 
      DOM API인 document.getElementById는 DOM에서 id가 'apple'인 HTML 요소를 취득한다. 
      아래 DOM API가 실행되는 시점에는 아직 id가 'apple'인 HTML 요소를 파싱하지 않았기 때문에 
      DOM에는 id가 'apple'인 HTML 요소가 포함되어 있지 않다. 
      따라서 아래 코드는 정상적으로 id가 'apple'인 HTML 요소를 취득하지 못한다. 
      */ 
      const $apple = document.getElementById('apple'); 
      
      // id가 'apple'인 HTML 요소의 css color 프로퍼티 값을 변경한다. 
      // 이때 DOM에는 id가 'apple'인 HTML 요소가 포함되어 있지 않기 때문에 에러가 발생한다. 
      $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 요소를 취득한다. 하지만 document.getElementById('apple')을 실행하는 시점에는 아직 id가 'apple'인 HTML 요소를 파싱하지 않았기 때문에 DOM에는 id가 'apple'인 HTML 요소가 포함되어 있지 않다. 따라서 위 예제는 정상적으로 동작하지 않는다.

이와 같이 자바스크립트 파싱에 의한 DOM 생성이 중단되는 문제를 근본적으로 해결하기 위해 HTML5부터 script 태그에 async와 defer 어트리뷰트가 추가되었다.

async와 defer 어트리뷰트는 src 어트리뷰트를 통해 외부 자바스크립트 파일을 로드하는 경우에만 사용할 수 있다. 즉, src 어트리뷰트가 없는 인라인 자바스크립트에는 사용할 수 없다.

 

<script async src="extern.js"></script>
<script defer src="extern.js"></script>


async와 defer 어트리뷰트를 사용하면 HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 동시에 진행된다. 하지만 자바스크립트의 실행 시점에 차이가 있다.


async 어트리뷰트


HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 동시에 진행된다. 단, 자바스크립트의 파싱과 실행은 자바스크립트 파일의 로드가 완료된 직후 진행되며, 이때 HTML 파싱이 중단된다.


여러 개의 script 태그에 async 어트리뷰트를 지정하면 script 태그의 순서와는 상관없이 로드가 완료된 자바스크립트부터 먼저 실행되므로 순서가 보장되지 않는다. 따라서 순서 보장이 필요한 script 태그에는 async 어트리뷰트를 지정하지 않아야 한다.

 


defer 어트리뷰트

async 어트리뷰트와 마찬가지로 HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 동시에 진행된다. 단, 자바스크립트의 파싱과 실행은 HTML 파싱이 완료된 직후, 즉 DOM 생성이 완료된 직후 진행된다. 따라서 DOM 생성이 완료된 이후 실행되어야 할 자바스크립트에 유용하다.

 

 

 

 

 

728x90