브라우저 렌더링 과정

unzinzanda·2025년 2월 18일
0
post-thumbnail

브라우저 렌더링 과정

웹 페이지가 사용자에게 어떻게 표시되는지 이해하는 것은 성능 최적화사용자 경험 향상에 필수적이기 때문에 브라우저 렌더링 과정은 웹 개발자에게 매우 중요한 주제입니다.

브라우저 렌더링 과정은 크게 다음과 같습니다.

  1. 요청과 응답
  2. HTML, CSS 파싱 및 렌더 트리 생성
  3. 자바스크립트 실행
  4. 레이아웃 계산 및 페인팅

각 단계는 웹 페이지의 성능과 사용자 경험에 중요한 영향을 미칩니다. 이제 각 단계에 대해 자세히 알아보도록 합시다.

1. 요청과 응답

사용자가 URL을 입력하면 브라우저는 HTML, CSS, 자바스크립트, 이미지, 폰트 파일 등 렌더링에 필요한 리소스를 서버에 요청하고 서버로부터 응답을 받습니다.

브라우저의 주소창에 URL을 입력하면 URL의 호스트 이름이 DNS(Domain Name System)을 통해 IP 주소로 변환되고 이 IP 주소를 갖는 서버에게 요청을 전송합니다.

이 과정에서 중요한 개념은 세 가지 입니다.

1. DNS(Domain Name System)

  • 도메인 이름 -> IP 주소로 변환하여 브라우저에 반환합니다.
  • 브라우저는 IP 주소를 통해 웹 사이트를 호스팅하는 웹 서버의 위치를 조회하고, 웹 서버에 연결해야 하기 때문에 DNS 서버에 쿼리를 보내야 합니다.

2. CDN(Content Delivery Network)

  • 콘텐츠를 효율적으로 전달하기 위해 여러 노드를 가진 네트워크에 데이터를 저장하여 제공하는 시스템, 지리적으로 분산된 서버들을 연결하는 네트워크
  • 서버가 멀리 있을 때 응답 시간을 줄이기 위해 캐시 서버를 사용하여 유저가 요청을 보냈을 때, CDN에 캐시된 데이터로로 빠르게 응답을 받을 수 있습니다.

3. HTTP와 HTTPS

  • 웹에서 브라우저와 서버가 통신하기 위한 프로토콜
  • HTTP/1.1커넥션 당 하나의 요청과 응답만 처리하여 요청할 리소스의 개수에 비례하여 응답 시간도 증가한다는 단점이 있습니다.
  • HTTP/2.0커넥션 당 여러 개의 요청과 응답을 처리할 수 있어 HTTP/1.1에 비해 페이지 로드 속도가 약 50% 정도 빠릅니다.
  • HTTPS는 HTTP에 보안 계층을 추가한 프로토콜로 데이터 전송 중에 암호화를 제공하기 때문에 보안이 중요한 웹 애플리케이션은 HTTPS를 사용해야 합니다.

2. HTML, CSS 파싱 및 렌더 트리 생성

브라우저의 렌더링 엔진은 서버로부터 응답된 HTML과 CSS를 파싱하여 DOMCSSOM을 생성하고 이들을 결합하여 렌더 트리를 생성한다.

💡 파싱(구문 분석, Syntax analysis)
프로그래밍 언어의 문법에 맞게 작성된 텍스트 문서를 읽어 들여 실행하기 위해 텍스트 문서의 문자열을 토큰으로 분해(어휘 분석: exical analysis)하고, 토큰에 문법적 의미와 구조를 반영하여 트리 구조의 자료구조인 파스 트리(parse tree/syntax tree)를 생성하는 일련의 과정을 말합니다.

일반적으로 파싱이 완료된 이후에는 파스 트리를 기반으로 중간 언어(intermediate code)인 바이트코드를 생성하고 실행합니다.

2.1. HTML 파싱과 DOM 생성 과정

서버가 응답한 순수한 텍스트인 HTML 문서를 브라우저에 시각적인 픽셀로 렌더링하려면 HTML 문서를 브라우저가 이해할 수 있는 자료구조(객체)로 변환하여 메모리에 저장해야 합니다.

따라서 브라우저의 렌더링 엔진은 HTML 문서를 파싱하여 브라우저가 이해할 수 있는 자료구조인 DOM(Document Object Model)을 생성합니다.

  1. 서버에 존재하던 HTML 파일이 브라우저의 요청에 의해 응답됩니다.
    이때, 서버는 브라우저가 요청한 HTML 파일을 읽어 메모리에 저장한 다음, 메모리에 저장된 바이트(2진수)를 인터넷을 경유하여 응답합니다.

  2. 브라우저는 응답된 바이트 형태의 HTML 문서를 meta 태그의 charset attribute에 의해 지정된 인코딩 방식을 기준으로 문자열로 변환합니다.
    참고로, meta 태그의 charset attribute에 선언된 인코딩 방식은 응답 헤더에 담겨 응답되고 브라우저는 이를 확인하여 문자열로 변환합니다.

  3. 문자열로 변환된 HTML 문서를 읽어 문법적 의미를 갖는 코드의 최소 단위인 토큰들로 분해합니다.

  4. 토큰들을 객체로 변환하여 토큰의 내용에 따라 문서 노드, 요소 노드, 어트리뷰트 노드, 텍스트 노드생성합니다.
    노드는 이후 DOM을 구성하는 기본 요소가 됩니다.

  5. HTML 문서는 HTML 요소들의 집합으로 이루어지며 HTML 요소는 중첩 관계를 갖습니다. HTML 요소 간의 부자 관계를 반영하여 모든 노드들은 트리 자료구조를 형성하고 이를 DOM이라 부릅니다.

이 과정을 통해 HTML 문서를 파싱한 결과물인 DOM을 생성합니다. 이러한 DOM 트리는 HTML 문서의 구조를 나타내며, 각 HTML 요소는 노드로 표현됩니다.

만약, 브라우저가 HTML 파싱 중에 외부 리소스(ex: CSS 파일, 자바스크립트 파일)를 만나면 이를 다운로드하고, 이 리소스가 DOM 트리 생성에 영향을 미칠 수 있기 때문에 파싱을 일시중지합니다.

2.2. CSS 파싱과 CSSOM 생성

앞서 언급한 것처럼 렌더링 엔진은 DOM을 생성하다가 CSS를 로드하는 link 태그나 style 태그를 만나면 DOM 생성을 일시 중단합니다.

link 태그의 href attribute에 지정된 CSS 파일을 서버에 요청하여 로드한 CSS 파일이나 style 태그 내의 CSS를 HTML과 동일한 파싱 과정을 거치며 해석하여 CSSOM(CSS Object Model)을 생성합니다.

CSSOM은 CSS의 상속을 반영하여 생성됩니다. 따라서 예시 그림처럼 body 요소에 적용한 스타일과 ul 요소에 적용한 스타일이 하위에 있는 모든 li 요소에 상속됨을 볼 수 있습니다.

이후 CSS 파싱을 완료하면 HTML 파싱이 중단된 시점부터 다시 HTML을 파싱하여 DOM 생성을 재개합니다.

2.3. 렌더 트리 생성

생성된 DOM과 CSSOM은 렌더링을 위해 렌더 트리(render tree)로 결합됩니다.

렌더 트리는 렌더링을 위한 트리 구조의 자료구조이기 때문에 브라우저 화면에 렌더링되는 노드만으로 구성됩니다. 따라서 브라우저 화면에 렌더링 되지 않는 노드(예: meta 태그, script 태그 등)와 CSS에 의해 비표시(예: display: none)되는 노드들은 포함되지 않습니다.

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

지금까지의 브라우저 렌더링 과정은 한 번만 실행되는 것이 아니라 다음과 같은 경우에 반복해서 실행될 수 있습니다.

  • 자바스크립트에 의한 노드 추가 또는 삭제
  • 브라우저 창의 리사이징에 의한 뷰포트 크기 변경
  • HTML 요소의 레이아웃(위치, 크기)에 변경을 발생 시키는 width/height, margin, padding, border, display, position, top/right/bottom/left 등의 스타일 변경

레이아웃 계산과 페인팅을 다시 실행하는 리렌더링성능에 악영향을 주는 작업이기 때문에 가급적 빈번하게 발생하지 않도록 주의할 필요가 있습니다.


3. 자바스크립트 실행

브라우저의 자바스크립트 엔진은 서버로부터 응답된 자바스크립트를 파싱하여 추상적 구문 트리(AST: Abstract Syntax Tree)를 생성하고 바이트코드로 변환하여 실행합니다.
이때 자바스크립트는 DOM API를 통해 DOM이나 CSSOM을 변경할 수 있고, 변경된 DOM과 CSSOM은 다시 렌더 트리로 결합됩니다.

3.1. 자바스크립트 파싱과 실행

DOM은 HTML 문서의 구조와 정보뿐만 아니라 HTML 요소와 스타일 등을 변경할 수 있는 DOM API를 제공합니다. 즉, 자바스크립트 코드에서 DOM API를 사용하여 이미 생성된 DOM을 동적으로 조작할 수 있습니다.

렌더링 엔진은 DOM을 생성하다가 자바스크립트 파일을 로드하거나 자바스크립트 코드를 담은 script 태그를 만나면 DOM 생성을 일시 중단합니다.

HTML과 CSS 파싱을 렌더링 엔진이 담당한다면 자바스크립트 코드 파싱과 실행은 자바스크립트 엔진이 담당합니다.

따라서 서버에 요청하여 로드한 자바스크립트 코드나 script 태그 내의 자바스크립트 코드를 파싱하기 위해 자바스크립트 엔진에 제어권을 넘깁니다. 그리고 이를 블로킹(blocking이 일어났다고도 말합니다.)

렌더링 엔진이 DOM과 CSSOM을 생성하듯이 자바스크립트 엔진은 자바스크립트를 해석하여 AST를 생성하고, 이를 기반으로 인터프리터가 실행할 수 있는 바이트코드를 생성하여 실행합니다.

이후 자바스크립트 파싱과 실행이 종료되면 렌더링 엔진에게 다시 제어권을 넘겨 중단된 시점부터 DOM 생성을 재개합니다.

3.2. 리플로우와 리페인트

DOM API에 의해 변경된 DOM이나 CSSOM은 다시 렌더 트리로 결합되고 이를 기반으로 레이아웃과 페인트 과정을 거쳐 브라우저의 화면에 다시 렌더링하는데, 이를 리플로우(reflow), 리페인트(repaint)라고 합니다.

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

이 두 가지가 반드시 순차적으로 동시에 실행되는 것은 아니며 레이아웃에 영향이 없는 변경이 발생한 경우, 리펜인트만 실행됩니다.


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

렌더링 엔진과 자바스크립트 엔진은 아래와 같이 직렬적으로 파싱을 수행합니다.

이처럼 브라우저는 동기적으로 HTML, CSS, 자바스크립트를 파싱하고 실행합니다.
이는 script 태그의 위치에 따라 HTML 파싱이 블로킹 되어 DOM 생성이 지연될 수 있다는 것을 의미하기 때문에 script 태그의 위치는 매우 중요한 의미를 갖습니다.

4.1. body 태그 위에 script 태그가 있는 경우

자바스크립트 코드에서 DOM이나 CSSOM을 변경하는 DOM API를 사용할 경우 DOM이나 CSSOM이 이미 생성되어 있어야 합니다.

DOM이 완성되기 전 DOM API를 호출하는 예시를 살펴봅시다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css">
    <script>
        const $apple = document.getElementById('apple')
    </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 요소가 포함되어 있지 않은 상태입니다.

따라서 위 예제는 정상적으로 동작하지 않습니다.

이러한 문제를 회피하기 위해 body 요소 가장 아래에 자바스크립트를 위치시키는 것이 좋습니다.

4.2. 닫는 body 태그 바로 위에 script 태그가 있는 경우

자바스크립트를 body 요소의 가장 아래, 즉 닫는 body 태그 바로 위에 위치시켜야 하는 이유는 다음과 같습니다.

  1. DOM이 완성되지 않은 상태에서 자바스크립트가 DOM을 조작하면 에러가 발생할 수 있습니다.
  2. 자바스크립트 로딩/파싱/실행으로 인해 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>
        const $apple = document.getElementById('apple')
    </script>
</body>
</html>

코드를 보면 자바스크립트가 실행될 시점엔 이미 렌더링 엔진이 DOM 생성을 완료한 이후입니다. 따라서 DOM이 완성되지 않아 생기는 문제가 발생할 우려도 없고, 자바스크립트 실행 전에 렌더링되므로 페이지 로딩 시간이 단축됩니다.


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

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

자바스크립트 파싱에 의한 DOM 생성 중단 문제를 해결하기 위해 HTML5부터 script 태그에 외부 자바스크립트 파일을 로드하는 경우에만 사용할 수 있는 async/defer 어트리뷰트가 추가되었습니다.
이를 사용하면 HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 실행됩니다. 두 어트리뷰트의 차이점은 자바스크립트의 실행 시점에 있습니다.

5.1. async 어트리뷰트

async 어트리뷰트를 사용하면 자바스크립트의 파싱과 실행은 자바스크립트 파일의 로드가 완료된 직후 진행되며, 이때 HTML 파싱이 중단됩니다.

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

5.2. defer 어트리뷰트

defer 어트리뷰트를 사용하면 자바스트의 파싱과 실행은 HTML 파싱이 완료된 직후, 즉 DOM 생성이 완료된 직후(DOMContentLoaded 이벤트가 발생하는 시점) 진행됩니다.
-> DOM 생성이 완료된 이후 실행되어야 할 자바스크립트에 유용합니다.

여러 개의 script 태그에 defer 어트리뷰트를 지정하면 선언 순서대로 실행되므로 순서를 보장합니다.


6. 성능 최적화

웹 페이지를 불러오는 데 걸리는 시간은 사용자 경험에 큰 영향을 미칩니다. 따라서, 브라우저 렌더링 성능을 개선하기 위해 다음과 같은 방법을 고려해볼 수 있습니다.

6.1. CSS 및 Javascript 최적화

CSS 와 Javascript는 웹 페이지의 디자인과 동작을 결정하는 데 중요한 역할을 합니다. 하지만 코드가 지나치게 복잡하거나 파일 크기가 크면 렌더링 성능을 저하시킬 수 있습니다. 이를 해결하기 위해서 다음과 같은 방법을 고려해볼 수 있습니다.

  1. CSS 코드를 압축하고 중복된 스타일 코드 제거하기

  2. 리플로우가 발생하는 속성보다는 리페인트만 발생하는 속성 사용하기

  • 리플로우를 발생시키는 속성
    position
    width, height
    top, bottom, left, right
    margin, padding, border, border-width
    clear, display, float, overflow
    font-family, fontsize, font-weight
    line-height, min-height
    text-align, vertical-align

  • 리페인트를 발생시키는 속성
    background, background-image, background-pisition, background-repeat, background-size
    border-radius, border-style, box-shadow
    color, outline-color
    line-style, outline
    clear, display, float, visibility
    font-family, fontsize, font-weight

    opacity 속성은 리페인트가 일어나지 않으므로, visibilty, display 속성을 사용하는 것보다 성능 개선에 도움이 됩니다.

  1. Javascript 코드를 압축하고 불필요한 코드 제거하기

  2. Javascript 코드를 지연 로딩하거나 비동기 로딩을 이용하여 페이지 로딩 시간 최적화하기

6.2. 이미지 최적화

웹 페이지에서 사용하는 이미지는 렌더링 성능에 큰 영향을 미칩니다. 따라서 이미지 최적화를 하여 불필요한 로딩 시간을 줄이는 것이 중요합니다. 이를 위해서 다음과 같은 방법을 고려해볼 수 있습니다.

  1. 이미지 포맷을 최적화하여 파일 크기 줄이기
    (JPEG는 사진에, PNG는 아이콘과 같은 단순 그래픽에 적합)

  2. 불필요한 이미지를 제거하거나 CSS 스프라이트 기술을 이용하여 여러 이미지를 하나의 이미지 파일로 통합

  3. Lazy Loading 기술을 이용하여 페이지 스크롤링에 따라 이미지를 로딩

    ```
    <img loading="lazy" src="..." />
    // 화면에 실제로 이미지가 보여져야 할 때, 이미지 다운로드
    // CSS, JS를 더 앞서 다운로드하게 함
    ```

6.3. 레이아웃 최적화

레이아웃은 브라우저에서 웹 페이지의 요소를 배치하는 과정으로, 렌더링 성능에 큰 영향을 미칩니다. 렌더링을 최적화를 위해 다음과 같은 방법을 고려해볼 수 있습니다.

  1. CSS Flexbox 또는 CSS Grid를 이용하여 요소 배치

  2. 요소의 크기와 위치를 계산할 때, 불필요한 계산을 줄이는 최적화 기술 사용

  3. 불필요한 CSS 요소를 제거하거나 CSS 속성 줄이기

  4. CSS 속성 중 레이아웃을 발생시키는 속성 피하기
    (이 속성들이 매번 렌더 트리를 만들고, 레이아웃이 발생하여 페인팅을 발생시키기 때문)

6.4. 주변 노드에 영향을 주는 노드 줄이기

Javascript와 CSS를 조합한 애니메이션이 많거나, 레이아웃 변화가 많은 요소의 경우, position 속성을 absolute 또는 fixed로 사용해주면 영향을 받는 주변 노드를 줄일 수 있습니다.

fixed는 영향을 받는 노드가 전혀 없는 경우, 리플로우없이 리페인트만 진행합니다.


참고

모던 자바스크립트 DeepDive
브라우저 렌더링 과정 이해하기
웹 브라우저의 리소스 호출, 그리고 CDN
브라우저 렌더링 원리와 순서(+성능 최적화 고려사항)
브라우저 렌더링 과정 & 렌더링 최적화
브라우저 렌더링 과정과 최적화

profile
안녕하세요 :)

0개의 댓글