Languages/JavaScript

[HUFS/GnuVil] #18 브라우저의 렌더링 과정

성중 2022. 11. 11. 15:01

구글의 V8 엔진으로 빌드된 자바스크립트 런타임 환경인 Node.js의 등장으로 자바스크립트는 브라우저를 벗어나 서버 사이드에서도 사용될 수 있는 범용 개발 언어가 되었으며, 대부분의 프로그래밍 언어는 운영체제나 가상 머신 위에서 실행되지만 자바스크립트는 브라우저에서 HTML, CSS와 함께 실행되어 파싱렌더링 된다

  • 파싱: 프로그래밍 언어의 문법에 맞게 작성된 텍스트 문서를 읽어 들여 토큰으로 분해하고 문법적 의미와 구조를 반영해 트리 자료구조 형태를 생성하는 일련의 과정을 말한다
  • 렌더링: HTML / CSS / JS로 작성된 문서를 파싱해 브라우저에 시각적으로 출력하는 것을 말한다

 

브라우저의 렌더링 과정

브라우저의 핵심 기능은 필요한 리소스(HTML, CSS, JS, 이미지, 폰트 등의 정적 파일, 서버에서 동적 생성한 데이터)를 서버에 요청하고 응답을 받아 브라우저에 시각적으로 렌더링하는 것이다

 

URI(Uniform Resource Identifier)

웹에서 브라우저와 서버가 통신하기 위한 프로토콜인 HTTP/1.1HTTP/2.0이 있다

  • HTTP/1.1: 커넥션당 하나의 요청과 응답만 처리하기 때문에 요청할 리소스의 개수에 비례해 응답 시간도 증가한다. 이를 최소화하며 작업하는 것이 클라이언트 성능 최적화 이슈였다
  • HTTP/2.0: 다중 요청/응답이 가능해 여러 리소스의 동시 전송이 가능하고 로드 속도가 약 50% 더 빠르다

 

HTML 파싱과 DOM 생성
CSS 파싱과 CSSOM 생성

HTML과 CSS를 파싱해 생성된 DOM과 CSSOM은 렌더링을 위해 렌더 트리로 결합된다

* 브라우저 화면에 렌더링되는 노드만으로 구성 (meta 태그, script 태그 등 제외)

 

렌더 트리의 생성

이후 렌더 트리는 레이아웃(위치와 크기) 계산에 사용되며 실제 렌더링하는 페인트가 이루어진다

 

렌더 트리와 레이아웃/페인트

위 렌더링 작업은 다음과 같은 경우 재실행(리렌더링)될 수 있다

  • 자바스크립트에 의한 노드 추가 또는 삭제
  • 브라우저 창의 리사이징에 의한 뷰포트 크기 변경
  • HTML 요소의 레이아웃에 변경을 발생시키는 스타일 변경

 

자바스크립트의 파싱과 실행

자바스크립트 코드에서 DOM이나 CSSOM을 변경하는 DOM API가 사용된 경우, 변경된 렌더 트리를 기반으로 레이아웃과 페인트 과정을 다시 반복하는데, 이를 리플로우/리페인트라 한다

  • 리플로우: 레이아웃 계산을 다시 하는 것으로, 노드 추가/삭제, 요소의 크기/위치 변경, 윈도우 리사이징 등 레이아웃에 영향을 주는 변경이 발생한 경우에 실행
  • 리페인트: 재결합된 렌더 트리를 기반으로 다시 페인트를 하는 것으로, 레이아웃에 영향이 없는 변화라면 리플로우 없이 리페인트만 실행

 

HTML / CSS / JS 직렬적 파싱 (HTML 파싱 중단)

// Bad

<!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>

// Good

<!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>
      /*
      DOM API인 document.getElementById는 DOM에서 id가 'apple'인 HTML 요소를
      취득한다. 아래 코드가 실행되는 시점에는 id가 'apple'인 HTML 요소의 파싱이 완료되어
      DOM에 포함되어 있기 때문에 정상적으로 동작한다.
      */
      const $apple = document.getElementById('apple');

      // apple 요소의 css color 프로퍼티 값을 변경한다.
      $apple.style.color = 'red';
    </script>
  </body>
</html>

파싱은 직렬적으로 수행되기 때문에 script 태그를 body 태그의 최하단에 위치시켜야 한다

 

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

DOM 생성 중단을 방지하기 위해 async와 defer를 사용하는 방법도 있지만 잘 사용되지는 않는다

 

 내용은 위키북스의 '모던 자바스크립트 Deep Dive' 바탕으로 작성되었습니다.