브라우저 렌더링 과정

정지훈·2020년 12월 14일
0

브라우저가 HTML,CSS, 자바스크립트로 작성된 텍스트 문서를 어떻게 파싱하여 브라우저에 렌더링을 할까?

일단 파싱이란건 무엇일까?

파싱은 프로그래밍 문법에 맞게 작성된 텍스트 문서를 읽어 들여 실행하기 위해 텍스트 문서의 문자열을 토큰으로 분해

토큰은 문법적인 의미를 가지며 문법적으로 더이상 나눌 수 없는 코드의 기본 요소를 의미한다.

하고 토큰에 문법적 의미와 구조를 반영하여 트리 구조의 자료구조인 파스 트리를 생성하는 일련의 과정을 말한다.

요청과 응답

브라우저의 주소창에 주소를 입력하고 엔터를 누르면 루트 요청 만으로 URI에 서버로 전송된다.

서버는 루트 요청에 따라 서버의 루트 폴더에 존재하는 정적 파일 index.html을 클라이언트로 응답한다.

브라우저는 다음과 같은 과정을 렌더링을 수행한다.

  1. 브라우저는 HTML, CSS, 자바스크립트, 이미지, 폰트 파일 등 렌더링에 필요한 리소스를 요청하고 서버로 부터 응답을 받는다.
  2. 브라우저의 렌더링 엔진은 서버로부터 응답된 HTML과 CSS를 파싱하여 DOM과 CSSOM을 생성하고 이들을 결합하여 ㄹㄴ더트리를 생성한다.
  3. 브라우저의 자바스크립트 엔진은 서버로 부터 응답된 자바스크립트를 파싱하여 AST를 생성하고 바이트 코드로 변환하여 실행한다. 이때 자바스크립트는 DOM API를 통해 DOM이나 CSSOM을 변경할 수 있다. 변경된 DOM과 CSSOM은 다시 렌더트리로 결합된다.
  4. 렌더트리를 기반으로 HTML 요소의 레이아웃을 계산하고 브라우저의 화면에 HTML요소를 페인팅한다.

요청과 응답

브라우저의 핵심 기능은 필요한 리소스를 서버에 요청하고 서버로 부터 응답을 받아 브라우저에 시각적으로 렌더링하는 것이다.

서버에 요청을 전송하기 위해 브라우저는 주소창을 제공해서 URL을 누르면 URL의 호스트 이름이 DNS를 통해 IP주소로 변환되고 이 IP 주소를 갖는 서버에게 요청을 전송한다

HTTP 1.1rhk HTTP 2.0

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

팀 버너스 리경이 고안한 HTTP는 직렬 구조로 1:1 한 파일이 요청을하면 응답이 오는 구조라서 동시 전송이 불가능한 구조이고 느렸다.
하지만 HTTP 2.0이 나오면서 다중 요청/응답이 가능해 졌고 여러 리소스의 동시 전송이 가능하므로 HTTP/1.1에 비해 페이지 로드 속도가 약 50% 정도 빠르다고 알려져 있다.

HTML 파싱과 DOM 생성 && CSSOM

브라우저의 요청에 의해 서버가 응답한 HTML 문서는 문자열로 이루어진 순수한 텍스트이다.

만약 index.html이 서버로부터 응답되었다고 해보면 브라우저의 렌더링 엔진은 응답받은 HTML 문서를 파싱하여 브라우저가 이해할 수 있는 자료구조인 DOM을 생성한다.

파싱 도중 link태그를 만나면 잠시 DOM 파싱을 중단하고 link태그를 읽어들여 css파일이면 CSSOM 트리를 만들어 낸다.

그리고 Dom 트리와 CSSOM 트리를 합쳐서 render트리를 만들어내고 렌더트리를 통해 레이아웃이 계산이 되고 페인팅을 시작한다.

이때 맨 마지막으로 자바스크립트를 만나면 파싱을 하고 자바스크립트 엔진이 AST를 생성해 DOM API를 이용해 DOM tree나 CSSOM tree에 접근하고 변경을 할 시 리페인팅, 리플로우가 일어나게 되며 성능에 몹시 않좋다.

만약에 레이아웃이 영향이 없는 변경은 리플로우 없이 리페인트만 실행된다.

이 처럼 웹을 만들때 리페인팅, 리플로우가 일어나지 않도록 개발을 해야하고 대도록 css에서 할 수 있으면 가능한 css를 통해 개발을 하는게 더 좋다고 나는 생각한다.

자바스크립트 파싱에 의한 HTML 파싱 중단

파싱 중 스크립트 태그를 만나면 파싱을 중단하고 자바스크립트 엔진이 파싱 후 실행을 할 것이다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css">
    <script src="app.js"></script>
  </head>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
  </body>
</html>

만약 위에와 같이 코드를 짰다면 script 태그를 만나 파싱을 중단하고 자바스크립트가 실행이 될 것이다. 하지만 app.js에 body에 있는 태그에 접근을 한다면 접근이 불가능 할 것이다.

이유는 아직 DOM tree가 만들어 지지 않았기 때문이다.
그래서 대부분 body 맨 밑에 만들어서 DOM 파싱이 끝나고 실행하게 만들거나
defer나 async 어트리뷰트를 이용할 것이다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css">
    <script src="app.js" defer></script>
  </head>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
  </body>
</html>

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

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

async 어트리뷰트는 IE10이상에서 지원된다.

나는 async보다 defer가 훨 좋다고 생각이 든다.

defer 어트리뷰트는 async와 마찬가지로 로드가 비동기적으로 동시에 진행되지만 HTML파싱이 완료된 직후 즉 DOM생성이 완료된 직후에 진행되서 DOM 생성이 완료된 이후 실행되어야 할 자바스크립트에 유용하다.

defer 어트리뷰트는 IE10이상에서 지원되고 IE6 ~ 9 에서도 지원되기는 하지만 정상적으로 동작하지 않을 수 있다.

즉 defer나 body에 맨 밑에 넣는게 나는 좋다고 생각한다.

출저: https://poiemaweb.com/fastcampus/browser-rendering

0개의 댓글