브라우저 렌더링 과정

Created
April 29, 2024
Tags
JavaScript

브라우저 렌더링 과정

브라우저 렌더링 과정에 대한 이해는 개발자가 웹 애플리케이션을 효과적으로 설계하고 최적화하는 데 매우 중요하다. 브라우저 렌더링은 웹 페이지의 HTML, CSS, JavaScript 등의 리소스를 해석하고 화면에 표시하는 과정이다.

📠 Parsing (HTML, CSS, JavaScript)

브라우저의 렌더링 엔진은 HTML, CSS, JavaScript 코드를 읽어들이고 이를 *파싱하여 DOM, CSSOM, 그리고 JavaScript 실행 환경을 생성한다. 이들은 렌더링 과정에서 발생하는 핵심 단계로, 각각의 *파싱과 실행 방식에는 차이가 있다. 이제 과정들을 자세히 살펴보겠다.

🔍
파싱 브라우저 요청에 의해 서버가 응답한 HTML 문서는 순수 문자열로 이루어져있다. 우리는 브라우저에서 이 텍스트들을 어떻게 시각적인 픽셀로 볼 수 있는 것일까? 그러기 위해서는 HTML 문서를 브라우저가 이해할 수 있는 자료 구조(객체)로 변환하여 메모리에 저장해야 한다. 즉, 파싱은 HTML/XML/JSON과 같은 문자 형식의 데이터를 읽어서 웹 어플리케이션에서 사용될 수 있는 구조로 변환하는 과정을 말한다.

HTML 파싱과 DOM 생성

브라우저는 서버로부터 받은 HTML 문서를 파싱하여 DOM을 생성한다. DOM은 문서의 구조를 표현하는 트리 구조로, 각 HTML 요소가 DOM 노드로 표현된다.

1. 브라우저의 요청과 응답

  • 사용자가 브라우저를 통해 특정 웹 페이지에 접속하면, 브라우저는 서버에 HTML 문서를 요청한다.
  • 서버는 요청을 받고, HTML 문서를 포함한 응답을 브라우저에게 전송한다.

2. HTML 파일의 메모리 저장

  • 브라우저는 서버로부터 받은 HTML 문서를 메모리에 저장한다.

3. 바이트 → 문자열 변환

  • 메모리에 저장된 HTML 문서는 바이트(2진수) 형태로 저장되어 있으므로, 브라우저의 렌더링 엔진이 이해할 수 있는 문자열로 변환해야 한다. 주로 인코딩에 따라 이루어진다.
  • HTML 문서 내 meta 태그의 charset 속성은 문서의 문자 인코딩(예: UTF-8, ISO-8859-1 등) 방식을 지정한다.
  • 브라우저는 응답된 HTML 문서의 바이트를 해당 문서에 선언된 문자 인코딩 방식에 따라 문자열로 변환한다. 즉, 이 단계에서는 바이트를 문자열로 해석하여 각 문자에 해당하는 코드 포인트로 변환한다.
  • 서버는 응답 헤더에 Content-Type 헤더를 포함하여 클라이언트(브라우저)에게 전송한다. 이 헤더에는 응답된 문서의 종류와 문자 인코딩 정보가 포함된다.
  • image
  • 예들 들어, 11010001101001111100001010101101과 같은 바이트가 있다. UTF-8로 디코딩한다면, 각 비트에 대응되는 문자가 결정되고, 그 결과로 안녕하세요 라는 문자열로 변환된다.

4. 문자열 → 토큰화(Tokenization)

  • 변환된 문자열은 HTML 파서에 의해 토큰화(Tokenization) 과정이 시작된다.
  • 토큰화는 문자열을 문법적 의미를 갖는 최소 단위인 토큰(Token)으로 분할하는 과정이다.
  • 아래 HTML 문서에서 발행된 토큰을 알아보자. 시작 태그 토큰 <p>, 텍스트 토큰 Example Page, 종료 태그 토큰 </p> 이 발행된다.
  • <p>Example page</p>

5. 토큰 → DOM 노드 변환

  • HTML 파서는 발행된 각 토큰을 객체로 작성한다. 이는 토큰화된 HTML 코드를 분석하여 문법적으로 올바른 구조를 만드는 과정이다.
    1. HTMLParagraphElement = {
        type: 'element',
        tagName: 'p',
        attributes: [],
        children: [
          {
            type: 'text',
            value: 'Example page'
          }
        ]
      };
    2. HTMLParagraphElement는 HTML 문서의 <p> 요소를 나타내는 객체이다.
    3. type 속성은 요소의 타입을 나타내며, 여기서는 element로 설정되어 있다.
    4. tagName 속성은 요소의 태그 이름을 나타내며, 여기서는 'p'로 설정되어 있다.
    5. attributes 속성은 요소의 속성을 나타내는 배열입니다. 여기서는 속성이 없으므로 빈 배열로 설정되어 있다.
    6. children 속성은 요소의 자식 요소를 나타내는 배열이다. 여기서는 자식 요소로 텍스트 노드가 하나 있으며, 그 값은 'Example page'이다.
  • 이 변환된 객체를 사용하여 DOM 노드를 생성하며, 이는 DOM tree를 구성하는 과정에서도 사용될 수 있다.
    1. const paragraphElement = document.createElement('p');
      const textNode = document.createTextNode('Example page');
      // 시작 태그 노드에 텍스트 노드를 자식으로 추가
      paragraphElement.appendChild(textNode);
    2. element 노드와 text 노드가 생성된다.

6. DOM 트리 구축

HTML 파서는 파싱된 토큰들을 바탕으로 DOM tree를 구축한다. 요소 간에 중첩 관계를 기반으로 트리 구조로 연결되고, 이 자료구조를 DOM이라 부르는 것이다. 이는 웹 페이지의 계층 구조를 표현한다.

CSS 파싱과 CSSOM 생성

CSS 파싱은 CSS 코드를 이해 가능한 구조로 변환하고, CSSOM(CSS Object Model) 생성은 이를 통해 HTML 요소들에 대한 스타일 정보를 표현하는 객체 모델을 생성하는 과정이다. 이 과정을 거치면 브라우저는 각 요소의 최종적인 스타일을 계산하고, 사용자 화면에 렌더링할 준비가 된다. 위의 HTML 파싱과 DOM 생성 과정 유사하다.

1. CSS 파싱

브라우저의 렌더링 엔진은 HTML을 파싱하면서 link 태그를 만나면 해당 CSS 파일을 다운로드하고 파싱한다. 또, style 태그를 만나면 내부 CSS 코드를 파싱한다.

먼저 CSS 코드를 토큰 단위로 나누는 토큰화 과정이 시작된다. 일반적으로 토큰은 선택자(selector), 속성(property), 값(value), 구분자(delimiter) 등의 종류로 나뉜다. 예를 들어 h1, color, blue, font-size, 16px 같은 토큰들이 발행된다. 그리고 CSS 파서는 발행된 토큰들을 CSS 문법 규칙에 따라 순서대로 분석하여, 객체로 작성한다. 이는 토큰화된 CSS 코드를 분석하여 문법적으로 올바른 구조를 만드는 과정이다.

2. CSSOM 생성

CSS 파서는 파싱된 토큰들을 바탕으로 CSSOM(CSS Object Model)을 구축한다. CSSOM은 웹 페이지의 각 요소에 적용되는 스타일 정보를 표현하는 객체 모델이다. CSSOMDOM과 유사한 트리 구조로 표현되며, 각 노드는 스타일 규칙을 나타낸다. 뿐만 아니라, 해당 규칙이 적용되는 요소와 우선순위 등을 고려한다.

2-1. Cascade

CSS 특징 중 하나는 속성의 상속이다. 예를 들어, 부모 요소에 정의된 일부 CSS 속성은 자식 요소에 상속된다. 상속을 받지 않는 속성도 있지만, font-size, font-weight과 같은 일부 속성은 자식 요소에게 상속되어 부모의 스타일을 이어받는다.

JavaScript 파싱, AST 생성 그리고 실행

1. JavaScript 파싱

CSS 파싱 과정과 마찬가지로 브라우저의 렌더링 엔진은 HTML을 한 줄씩 순차적으로 파싱하며 DOM을 생성해나가다 script 태그를 만나면 JavaScript 코드를 다운로드하고 파싱한다. (이때 중요한 사실은 script 파일을 다운로드하는 동안 HTML 파싱은 중단된다는 것이다. JavaScript는 DOM 조작이 가능해 DOM tree에 영향을 주기 때문인데 이 내용은 아래에서 더 자세하게 다루겠다.)

파싱 과정은 역시 유사하다. JavaScript 코드를 토큰 단위로 나누는 토큰화 과정을 거친다. 변수, 키워드, 연산자, 객체 식별자 등의 종류로 나뉜다. 예를 들어 let, x, =, 5, console 같은 토큰들이 발행된다. 그리고 JavaScript 파서는 발행된 토큰들을 구문적으로 분석한다. 이 과정은 코드가 JavaScript 문법에 맞는지 여부를 확인하고, 구문의 오류를 찾아내게 된다.

2. AST 생성

JavaScript 파서는 파싱된 토큰들을 바탕으로 AST(Abstract Syntax Tree)라고 불리는 계층 구조의 트리를 구축한다. 이 트리는 코드의 추상적인 구조를 나타내며, JavaScript 코드의 실행 흐름을 파악하는 데 도움이 된다.

3. JavaScript 실행(Execution)

위 과정까지 JavaScript 코드가 실행될 수 있도록 준비하는 것이다. 이제 브라우저는 파싱된 JavaScript 코드를 JavaScript 엔진에게 전달하여 실행한다. 이 과정은 실행 컨텍스트를 참고하면 된다.

✂️ JavaScript 파싱에 의한 HTML 파싱 중단

브라우저는 위에서 한 줄씩 순차적으로 HTML, CSS, JavaScript를 파싱하고 실행한다. 동기적(synchronous)으로 동작한다는 말이다.

다시 말해, HTML 파싱 중 script 태그를 만나면 HTML 파싱은 중단(HTML 파일 블로킹)된다. JavaScript 파일이 다운로드되고 파싱이 완료 되어야 HTML 파싱은 다시 시작된다. 이때까지 웹 페이지의 나머지 내용은 로드 및 렌더링 되지 않는다. 이는 브라우저가 JavaScript 파일을 가져오는 동안 웹 페이지의 렌더링이 지연(DOM 생성 지연)될 수 있음을 의미한다.

또한, script 태그가 HTLM 문서 상단에 위치하고 있을 때 DOM이 완성되지 않은 상태이므로 예상치 못한 결과가 발생할 수 있다.

<!DOCTYPE html>
<html>
<head>
    <title>Delayed DOM Example</title>
    <script>
        // HTML 문서가 파싱되기 전에 실행되는 코드
        document.getElementById("output").innerText = "Hello, World!";
    </script>
</head>
<body>
    <div id="output"></div>
</body>
</html>

위 예제에서 script 태그가 div 태그보다 상단에 위치하고 있다. JavaScript는 페이지가 로드되면 즉시 실행되는데 아직 <div id=”output”>의 요소가 생성되지 않은 상태이다.

따라서 document.getElementById("output")null을 반환하게 되고, 이후의 코드에서 innerText에 값을 할당하려고 시도할 때 오류가 발생한다.

🔑 해결 방법

이러한 문제를 해결하기 위해서는 JavaScript를 DOM이 완성된 후에 실행하도록 보장해야 한다.

  • 일반적으로 body 요소의 가장 아래에 JavaScript를 위치시키는 것이 좋다. HTML 파싱이 완료된 후에 JavaScript가 실행되므로 DOM 조작에 관련된 문제를 방지할 수 있으며, 렌더링 지연 시간도 단축할 수 있다.
  • script 태그에 async, defer 속성을 사용한다. JavaScript를 비동기적으로 다운로드하고 실행하도록 만들어주는 방법이다.
    • async: JavaScript가 다운로드되는 동안 HTML 파싱이 중단되지 않으며, JavaScript 다운로드가 완료되면 즉시 실행된다. 이는 DOM이 완성된 후에 스크립트가 실행되는 것을 보장하지 않는다. 그만큼 웹 페이지의 로딩 시간을 줄이는 데 도움은 되겠다.
    • defer: HTML 파싱이 완료된 후에 JavaScript가 실행되도록 한다. 따라서, JavaScript가 DOM이 완성된 상태에서 실행되어야 하는 경우에는 defer 속성을 사용하는 것이 더 적합하다.
💡
JavaScript의 파싱과 실행 과정은 HTML과 CSS와 함께 웹 페이지를 동적으로 만드는 데 필수적인 부분이다. 따라서 JavaScript의 성능과 페이지 로딩 시간을 최적화하기 위해서는 이러한 과정을 이해하고 적절히 관리하는게 중요하다.

🌳 렌더 트리 생성

브라우저는 DOMCSSOM을 결합하여 렌더 트리(Render tree)를 생성한다.

  • 시각적 요소 포함: 렌더 트리는 페이지의 시각적 구조를 포함하며, 스타일이 적용된 요소들로 구성된다.
    • 각 요소의 레이아웃과 스타일 정보
  • 비시각적 요소 제외: 이때 렌더링 되지 않는 노드(예: DOMmeta 태그)와 CSS에 의해 비표시되는 노드(display: none)들을 제외된다.

📚 Layout(Reflow)

레이아웃이란 브라우저가 요소들의 크기와 위치를 계산하여 화면에 배치하는 과정이다.

  • 박스 모델 계산: 각 요소의 크기와 위치를 계산한다. 이는 CSS에서 지정된 속성(너비, 높이, 여백 등)을 기반으로 한다.
  • 레이아웃 배치: 계산된 박스 모델에 따라 요소들을 화면에 배치한다. 이 과정에서 요소들의 상대적인 위치와 차원이 결정된다.

레이아웃 계산은 비용이 많이 드는 작업이며, 특히 요소의 크기가 변경되거나 창의 크기가 조정될 때 재계산되어야 한다.

🎨 Paint(Repaint)

페인트는 화면에 실제로 그림을 그리는 과정이다.

  • 렌더 트리 페인딩: 레이아웃 과정에서 계산된 값을 이용해 렌더 트리 각 요소에 대해 실제 화면에 그리는 작업이 이루어진다.
    • 요소의 배경색, 테두리, 글꼴 등 시각적인 속성 적용
  • 레이어 생성: 복잡한 웹 페이지의 경우, 브라우저는 각 요소를 다루기 위해 여러 개의 레이어를 사용할 수 있다. 이 레이어는 각각 독립적으로 관리되며, 병합되어 최종 화면이 완성된다

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

🖼️ Composite

모든 레이어가 합성되어 사용자가 볼 수 있는 최종 화면이 생성된다.