새소식

반응형
Front-end

[Front-End] 웹 브라우저 동작 방식 이해 (feat. 렌더링)

2024.09.17
  • -
반응형

1. 들어가며

우리가 컴퓨터를 하면서 가장 많이 사용하는 프로그램은 무엇일까요?

 

단연 웹 브라우저(크롬, 사파리, Edge,...)일 것입니다. 웹 프론트엔드의 근간이라고도 할 수 있는 웹 브라우저가 어떤 방식으로 화면에 요소들을 그리는지에 대해서 알아보도록 하겠습니다.

 

2. 웹 브라우저란?

어떤 내용으로 시작을 할까 고민을 하면서 웹 브라우저의 역사부터 시작해야 하나 생각도 했지만 항상 드는 생각은 너무 많을 것을 담으려다 보니 내용이 고봉밥이 되는 문제가 있었기 때문에 이제는 좀 핵심적인 내용만 담으려고 합니다.

 

https://blog.glosign.com/post/%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%A2%85%EB%A5%98%EC%99%80-%ED%8A%B9%EC%A7%95%EC%9D%80

 

웹 브라우저란 무엇일까요? 우리가 흔히 사용하는 크롬, Microsoft Edge, 사파리 등이 모두 웹 브라우저입니다.

 

인터넷 상에 존재하는 모든 페이지들을 넘나들 수 있다는 특징을 갖고있죠.

 

브라우저의 주요 기능은 사용자가 원하는 자료를 서버에게 요청하여 정해진 형식에 맞춰 화면에 그려 보여주는 것입니다. 여기서 자료는 보통 HTML 문서를 의미하지만, 넓게는 PDF 혹은 이미지까지 다양한 형태일 수도 있습니다.

 

일반적으로 서버는 하나의 자료만을 가지지 않고 여러 자료(페이지)를 제공할 수 있는데 각 자료의 주소를 URI(Uniform Resource Identifier)로 구분하여 사용자로 하여금 가져갈 수 있도록 만들어 놓습니다.

 

현대의 브라우저는 앞서 말했듯 다양한 선택지들이 있지만 그들의 인터페이스는 모두 유사하게 구성됩니다.

  • URI를 입력할 수 있는 주소 표시줄
  • 앞, 뒤로 이동할 수 있는 버튼
  • 북마크
  • 새로고침 버튼 / 문서의 로드를 중단하는 버튼

 

3. 브라우저의 구조

먼저 브라우저가 어떤 구조를 갖고 있는지 살펴보도록 하겠습니다.

https://dev.to/itswillt/the-components-of-a-browser-23mn

  • User Interface(유저 인터페이스):
    • 앞서 말했듯 브라우저들은 각각의 유사한 UI를 갖고 있습니다. 하지만 이는 웹 페이지가 생성한 부분이 아닙니다. 즉, 웹 페이지 자체를 제외한 브라우저 창에서 사용자가 상호 작용할 수 있는 요소의 레이아웃을 정의한 것입니다. 상호 작용을 위한 요소로 "주소 표시줄", "새로 고침", "뒤로, 앞으로 버튼", "북마크 표시줄" 등이 포함됩니다.
  • Browser Engine(브라우저 엔진):
    • 브라우저 엔진이 하는 역할은 앞선 UI렌더링 엔진 간의 상호작용을 중간에서 관리하는 역할을 합니다. 브라우저 엔진은 UI로부터 입력을 받아 렌더링 엔진에 명령을 내릴 수 있도록 처리하는 부분으로 볼 수 있습니다. 따라서 UI와 렌더링 엔진 사이에서 두 부분을 연결하는 중간자 역할을 하고 있다고 보면 됩니다.
  • Rendering Engine(렌더링 엔진):
    • 렌더링 엔진은 요청된 자료를 브라우저 창에 렌더링하는 부분으로, 브라우저에서 가장 비용이 많이 드는 작업 중 하나로 굉장히 중요한 요소 중 하나로 자리잡고 있습니다. 예를 들어 HTML 페이지가 요청되면 HTML과 CSS를 파싱하고 파싱 및 형식이 지정된 내용을 그대로 화면에 표시하는 역할을 담당합니다. 아래와 같이 각 브라우저 별로 렌더링 엔진을 갖고 있습니다.
      • Blink - Chrome, Microsoft Edge, Opera
      • Webkit - Safari
      • Gecho - Mozilla Firefox
  • Networking(통신/네트워킹):
    • 네트워킹 부분은 HTTP 호출을 다루며 다른 네트워크 관련 작업을 처리합니다.
  • JavaScript Interpreter(자바스크립트 해석기):
    • 웹 페이지에서 채워진 내용들을 동적으로 만들어주는 것은 자바스크립트의 역할인데, 이러한 자바스크립트 코드를 파싱하고 실행하는 데 사용되는 요소입니다. 각 브라우저마다 분석, 파싱 및 실행에 사용하는 JS 엔진의 유형은 각기 다릅니다. 널리 알려진 엔진들은 다음과 같은 것들이 있습니다.
      • V8 (C++) - Chrome과 Microsoft Edge에서 사용되는 엔진으로 Node.js를 탄생시킨 엔진입니다.
      • SpiderMonkey(C/C++) - Mozilla Firefox에서 사용됩니다.
      • JavaScriptCore(Nitro) - Safari에서 사용됩니다.
  • UI Backend(UI 백엔드):
    • 이 장치는 기본 운영 체제(OS)의 UI 방식을 사용하고 있습니다. 주로 선택 상자(select box), 입력 상자(input box), 체크 상자(check box) 등과 같은 기본 위젯을 그리는 데 사용하게됩니다.
  • Data Persistence(자료 저장소):
    • 꽤 많은 상황에서 웹 브라우저는 특정 데이터를 컴퓨터가 종료된 이후에도 갖고 있어야 하는 경우가 존재합니다. 이 요소는 쿠키, 로컬 스토리지, 로컬 캐시 등 다양한 유형의 데이터를 각 시스템에 맞게 로컬에 저장하는 역할을 합니다. 따라서 브라우저는 WebSQL, IndexedDB, FileSystem 등과 같은 데이터 저장 메커니즘과 호환될 수 있어야 합니다.

 

이 포스팅에 주로 다루는 내용은 렌더링 엔진에 관한 내용들입니다.

 

4. https://cdragon.tistory.com 에 접속해보자. (Critical Rendering Path)

webkit 렌더링 과정 (critical rendering path)

 

4.1 문서 요청

주소창에 URL을 입력하여 엔터를 누르면 서버로부터 리소스를 응답 받게 됩니다.

 

사실 이러한 리소스를 요청하고 응답받는 과정 속에서도 HTTP, TCP, DNS, TLS 핸드쉐이크 등 복잡한 과정을 거쳐서 받아오게 되지만 이번 포스팅에서는 그러한 과정은 생략하고 받아왔다는 가정부터 출발하겠습니다.

 

브라우저에 어떤 사이트의 주소를 입력하게 되면 그 주소에 해당하는 서버가 약속된 HTML 파일을 우리 브라우저로 전송을 하게 됩니다.

 

개발자 도구를 열어 특정 페이지에서 새로고침을 하면 위 사진과 같이 HTML 문서를 가장 먼저 받아오는 것을 확인할 수 있습니다.

 

4.2 HTML 파싱 및 DOM  트리 구축

이제 받아온 문서를 파싱하는 과정이 이루어지게 됩니다. 파싱은 렌더링 엔진에서 매우 중요한 과정이고, 파싱 결과는 보통 문서 구조를 나타내는 노드 트리입니다.

 

이때 네트워크 상으로 전달되는 HTML 코드는 문자 형태일 수 없기 때문에 0과 1로 이루어진 8비트의 바이너리 데이터 형태인데, 이를 '바이트 스트림'이라고 합니다.

 

이러한 바이트 스트림은 HTTP 응답의 본문(body)으로 전송되며, 서버는 위 사진과 같이 Content-Type 헤더를 통해 콘텐츠의 MIME 타입과 문자 인코딩 방식을 같이 명시하여 보내게 됩니다.

 

HTML Parser

위 과정은 파싱 과정을 도식화한 것으로, 브라우저가 첫 번째 데이터의 청크를 바이트 형태로 받았으면, 수신된 정보를 구문 분석(parsing)하기 시작합니다. 구문 분석은 브라우저가 네트워크를 통해 받은 데이터를 DOM이나 CSSOM으로 바꾸는 단계입니다.

  • 대부분의 브라우저는 웹 표준화 기구인 W3C의 명세에 따라 HTML과 CSS를 해석합니다.

 


Encoding

첫 번째 과정으로 바이트 코드를 지정된 문자 인코딩 방식(UTF-8)에 따라 해석하여 다시금 텍스트(문자)로 변환하는 과정이 이루어지게 됩니다.


그리고 일종의 1차 해석 과정인 토큰화(tokenization)가 이루어지는데, 여기서는 입력 문자를 토큰 단위로 파싱하게 됩니다.

  • 토큰(token): 브라우저에 저장된 HTML의 시작과 종료 태그, 속성과 속성값 등 약속된 여러가지 값들을 의미합니다.

토큰화는 토큰을 인지해서 트리 생성자로 넘기고 다른 토큰을 확인하기 위해 다음 문자를 확인합니다. 그리고 이 과정을 입력의 마지막까지 반복하게 됩니다.

이러한 알고리즘은 결과물은 HTML 토큰입니다.

 

이해를 위해 아래와 같은 예제를 준비했습니다.

 <html>
   <body>
      Hello world
   </body>
</html>

 

초기 상태는 "자료 상태"입니다. "<" 라는 문자를 만나면 상태가 "태그 열림 상태"로 변합니다. 이후 a-z까지의 문자를 만나면 "시작 태그 토큰"을 생성하고 상태는 "태그 이름 상태"로 변하는데 이 상태는 ">"문자를 만날 때까지 유지하게 됩니다. 각 문자에는 새로운 토큰 이름이 붙는데 이 경우의 생성된 토큰이 html 토큰입니다.

 

">" 문자에 도달하면 바로 토큰이 발행되고, 상태는 다시 "자료 상태"로 바뀝니다. 이렇게 태그가 동일한 절차에 따라 처리됩니다. 이 과정을 한 번 더 반복하면 html 태크와 body 태그를 발행한 것이고 다시 "자료 상태"로 돌아온 상황이 됩니다.

 

다음 문자인 Hello World의 "H" 문자를 만나면 문자 토큰이 생성되고 발행 되며, 이는 종료 태그의 "<" 문자를 만날 때까지 진행됩니다. "d"문자까지 반복하여 각 문자에 대한 문자 토큰을 발행합니다.

 

이제 "<"를 만나 다시 "태그 열림 상태"가 되고, "/" 문자는 종료 태그 토큰을 생성하며 "태그 이름 상태"로 변경됩니다. 이 상태는 ">" 문자를 만날 때까지 유지되다가 새로운 토큰을 발급한 후 다시 "자료 상태"가 되어 이후에 동일한 과정을 거치며 처리됩니다.

토큰화가 완료된 모습

 

 

이러한 과정을 도식화 하면 아래와 같습니다.

 


자 그럼 이렇게 만들어진 토큰들을 의미있는 단위(노드)로 다시 만들어주어야 하는데 각각을 노드라고 합니다.

 

각 노드는 객체(Object) 형태로 만들어집니다.

 


만들어진 노드들은 그들의 관계를 반영한 하나의 DOM 트리를 만들게 됩니다.

 

DOM 트리

이 과정을 진행하다가 파서가 이미지 태그나 링크 태그와 같은 논블로킹 자원을 발견하면, 브라우저는 해당 자원에 대한 다운로드 요청을 하고 이어서 파싱을 계속합니다. CSS 파일을 만났을 때도 마찬가지로 파서는 지속적으로 동작하는데, async나 defer 같은 설정이 되어 있지 않은 <script> 태그를 만나게 되면 쓰레드는 자바스크립트 코드를 해석하는 데 돌입하기 때문에 파서를 중지시키게 됩니다.

  • 브라우저가 DOM 트리를 생성할 때 link와 img와 같은 태그를 만나게 되면 해당 태그 안에 명시된 리소스를 다운로드 받게됩니다.
  • script는 DOM 트리 생성을 중단하고 script 코드를 해석합니다.

 

브라우저의 Preload scanner가 이 작업을 가속화하는 데 도움을 주지만, 과도한 script 태그는 여전히 주요한 병목구간이 될 수 있기에 유의합니다.

 

브라우저가 DOM 트리를 만드는 프로세스는 메인 쓰레드를 차지합니다. 그렇기 때문에, 프리로드 스캐너(preload scanner)는 사용 가능한 컨텐츠를 분석하고 CSS나 JavaScript, 웹 폰트 같이 우선순위가 높은 자원을 요청합니다. 그 덕분에 파서가 외부 자원에 대한 참조를 찾아 요청하기까지 기다리지 않아도 됩니다. 

  • 프리로드 스캐너가 자원을 뒤에서 미리 요청하기 때문에 파서가 요청 자원에 다다를 쯤 이미 그 자원을 거의 다 전송 받았거나 이미 전송 받은 이후이게 됩니다.
  • 자바스크립트의 분석과 실행 순서가 중요하지 않고 스크립트가 프로세스를 막지 않도록 하려면 async 속성이나 defer 속성을 추가합시다.
  • CSS를 다운로드하는 것은 HTML 분석이나 다운로드를 막지 않습니다. 하지만 자바스크립트는 종종 요소에 영향을 주는 CSS 속성들을 조작하기 때문에 자바스크립트의 실행은 막습니다. 

 

결과물 - DOM Tree

이렇게 만들어진 것을 DOM이라고 하며 Document를 잘 파싱하여 Object로 만든 후 그들 간의 관계를 정립한 Model을 만드는 과정을 거쳐 만들어진 개체이기 때문에 DOM이라고 하는 것이고 그 결과물이 트리 구조를 띄고 있어 DOM Tree라고 합니다.


4.3 CSSOM 구축

렌더링을 위해 이루어지는 작업 중 하나는 CSS를 처리하고 CSSOM 트리를 만드는 일입니다.

 

CSS 파싱 과정은 HTML 파싱 과정과 거의 일치합니다. 

 

CSSOM Tree

마찬가지로 서버로부터 전달받은 CSS 파일을 바이트에서 문자로 변환하고, 토큰화 이후 노드를 만들게 됩니다. 최종적으로 이러한 노드들이 모여 하나의 거대한 CSSOM이 만들어지는 것입니다.

 

즉, 브라우저가 CSS 규칙을 이해할 수 있고 작업을 진행할 수 있도록 스타일 맵으로 변환하는 과정을 거치고, 브라우저는 CSS에 있는 각각의 규칙을 읽고, 마찬가지로 트리 노드를 만듭니다.

 

 


이후 렌더링 과정에는 스타일, 레이아웃, 페인트 그리고 가끔 발생하는 '합성' 과정이 포함됩니다. 

4.4  렌더 트리 구축

이렇게 만들어진 DOM Tree와 CSSOM Tree를 합쳐서 렌더 트리(Render tree)라는 것을 생성하게 됩니다. 진짜 웹 사이트를 그리기 위한 최종 설계도라고 보면 됩니다.

 

렌더 트리에는 내용을 표시해야 할 순서문서의 시각적인 구성 요소를 포함하여 올바른 방식으로 내용들을 그려낼 수 있도록 하기 위한 목적이 있습니다. 

  • Firefox는 이 구성 요소를 frames라고 하며, Webkit은 renderer(or render object)라고 표현합니다.

 

따라서 표시되는(보이는) 각 요소의 레이아웃을 계산하는 데 사용되고 픽셀을 화면에 렌더링하는 paint 프로세스에 대한 입력을 위한 작업들을 합니다.

  • 앞서 DOM 및 CSSOM 트리를 빌드했지만 이 둘은 문서의 서로 다른 측면을 다루는 독립적인 객체입니다.
  • 하나(DOM)는 콘텐츠를 설명하고 다른 하나(CSSOM)는 문서에 적용되어야 하는 스타일 규칙을 설명합니다.

 

렌더 트리를 생성할 때 브라우저는 대략적으로 다음과 같은 작업을 수행합니다.

  1. DOM 트리의 루트에서 시작하여 표시되는 각 노드를 traverse(순회)합니다.
    • 일부 노드는 표시되지 않는데,(ex. script, meta 등 화면에 표시되지 않는 태그들) 렌더링된 출력에 반영되지 않으므로 생략되게 됩니다.
    • 일부 노드는 CSS를 통해 숨길 수 있는데, 이 경우 렌더 트리에서도 생략시킵니다. 예를 들어, style에 'display: none;'이 지정된 태그는 렌더 트리에서 누락됩니다.
  2. 표시되는 각 노드에 대해 적절한 CSSOM 규칙을 찾아 적용합니다.
  3. 콘텐츠 및 계산된 스타일과 함께 표시되는 노드를 내보냅니다.

 

4.5 레이아웃(Layout | Reflow)

이러한 렌더 트리가 준비되면 본격적으로 그리는 단계인 '레이아웃' 단계를 진행할 수 있게 됩니다.

 

지금까지 표시해야 할 노드와 계산된 스타일을 사용했지만, 디바이스 표시 영역 내에서 노드의 정확한 위치와 크기를 계산하지는 않았습니다. 이를 '레이아웃(layout)' 단계('리플로우(reflow)'라고도 함)라고 합니다. 이 단계는 렌더 트리에 맞게 요소를 배치하는 단계로 볼 수 있습니다.

  • 리플로우와 리페인트가 반드시 순차적으로 동시에 실행되지는 않으며 레이아웃의 영향이 없는 변경은 리플로우 없이 리페인트만 실행됩니다.

간단한 예시를 통해 이해해보겠습니다.

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

 

 

위 페이지의 body에는 두 개의 중첩된 div 요소가 존재합니다. 첫 번째(상위) div는 노드의 표시 크기를 표시 영역 너비의 50%로 설정하고 두 번째 div(하위)는 너비가 상위 요소의 50%, 즉 표시 영역 너비의 25%로 설정하였습니다.

https://web.dev/articles/critical-rendering-path/render-tree-construction?hl=ko

레이아웃 프로세스의 출력은 '상자 모델'로, 표시 영역 내 각 요소의 정확한 위치와 크기를 세세하게 캡처합니다. 모든 상대적인 측정값은 화면의 절대적인 픽셀로 변환됩니다.

 

Gecko reflow

 

참고로 레이아웃은 글로벌 레이아웃과 로컬 레이아웃으로 나뉘게 됩니다.

  • 글로벌 레이아웃
    • 처음 배치되거나 font와 같이 전역 스타일이 변경되는 경우
    • 창이 resize 되는 경우
  • 로컬 레이아웃
    • 초기 배치 이후 일부 DOM 노드에 변경이 생기는 것처럼, 특정 부분만 재배치가 필요한 경우

처음 노드의 사이즈와 위치가 결정되는 것을 레이아웃이라 부르고, 이후에 노드의 크기와 위치를 다시 계산하는 것은 리플로우라고 부릅니다.

 

이렇게 표시되는 노드와 해당 노드의 계산된 스타일 및 도형을 기반으로 최종 단계에 돌입합니다.

 

4.6 페인트(Paint)

마지막 단계는 각 노드를 화면에 페인팅하는 것입니다. 페인팅이 처음 일어나는 것을 First Meaningful Paint(FMP)라고 부릅니다. 페인팅(혹은 레지스터화) 단계에서, 브라우저는 레이아웃 단계에서 계산된 각 박스를 실제 화면의 픽셀로 변환합니다. 페인팅에서 텍스트, 색깔, 경계, 그림자 및 버튼이나 이미지 같은 대체 요소를 포함하여 모든 요소의 시각적인 부분을 화면에 그리는 작업이 포함됩니다. 브라우저는 이 작업을 매우 빠르게 진행해야 합니다.

 

부드러운 스크롤이나 애니메이션을 위해서, 스타일 계산, 리플로우, 페인팅과 같이 메인 쓰레드를 점유하는 모든 작업은 브라우저를 16.67ms 미만만 차지하도록 권장됩니다.

 

예를 들어 2048 x 1536 화면에서 iPad는 화면에 페인트해야 할 3,145,000 픽셀을 가지고 있습니다. 이는 굉장히 많은 픽셀이며, 이 픽셀들은 매우 빠르게 페인팅 되어야 합니다. 첫 페인팅보다 다시 페인팅하는 것이 더 빨라야 하기에 화면에 그리는 작업은 일반적으로 레이어로 나눠 구분됩니다.

 

paint 과정에서는 stacking contexts라는 스택에 명령을 쌓아 실행하게 됩니다. 스택은 LIFO(Last-In, First-out), 즉 후입선출 구조 이기 때문에 나중에 들어온 명령이 가장 먼저 실행되는 특징을 갖고 있습니다. stack에는 배경 색 - 배경 이미지 - 테두리 - 자식 - 아웃라인 순으로 명령이 쌓이고, 실행은 아웃라인 - 자식 - 테두리 - 배경 이미지 - 배경색 순서로 진행됩니다.

 

4.7 합성 (Compositing)

앞서 paint 과정에서 사실은 한 페이지에 다 그리는 것이 아니라 여러 개의 레이어로 나누어 그리게 됩니다.

 

그 이유는 한 페이지에 전부 그리게 되면 작은 부분만 바뀌더라도 해당 페이지 전체를 다시 paint 해야하기 때문에 비효율적인 작업이 발생하는데, 여러 레이어로 나누어 그리면 바뀐 레이어만 다시 paint 할 수 있기 때문입니다.

 

문서의 각 섹션이 각기 다른 레이어에서 그려질 때, 합성 과정에서는 이러한 섹션들을 모두 겹치는 과정을 수행하여 그들이 올바른 순서로 화면에 그려지는 것과 정확한 렌더링을 보장하는 역할을 합니다.

 

레이어는 포토샵에 사용하는 레이어와 비슷하게 페인팅할 영역을 나누어놓은 것을 의미합니다. 크롬의 경우 레이아웃 과정 이후에 정해진 기준이나 필요에 의해 브라우저가 레이어를 생성합니다. 그리고 렌더 트리에 있던 노드 객체들이 생성된 레이어에 포함됩니다. 이 레이어 역시 트리 형태로 구성이 되는데, 렌더링 엔진이 각 레이어를 페인팅 과정에서 각각 그려준 다음에 하나의 비트맵으로 합성하여 페이지를 완성합니다.

 

4.8 반복!

렌더링을 마치게 되면 그냥 끝나는 것이 아닙니다.

 

만약 CSS의 속성이 바뀌는 등 변경 사항이 발생하면 브라우저는 렌더 트리를 다시 생성하여 레이아웃 -> 페인트 -> 합성 단계를 거쳐 재렌더링하는 과정이 지속적으로 반복 됩니다.

 

레이아웃이 다시 진행되는 것을 reflow라고 하며 해당 과정이 발생하는 경우는 다음과 같습니다.

  • 페이지 초기 렌더링 시 (최초 Layout 과정)
  • 윈도우 리사이징 시 (Viewport 크기 변경 시)
  • 노드 추가 또는 제거
  • 요소의 위치, 크기 변경 (left, top, margin, padding, border, width, height, 등...)
  • 폰트 변경(텍스트 내용)과 이미지 크기 변경(크기가 다른 이미지로 변경 시)

리플로우는 비용이 큰 작업이기 때문에 리플로우가 자주 발생하지 않도록 하는 것이 웹 최적화를 시키는 방법 중에 하나이기도 합니다.

 

레이아웃을 거치지 않고 Paint부터 다시 발생하는 경우도 존재합니다.

  • 주로 배경 이미지나 텍스트 색상, 그림자 등 레이아웃의 수치를 변화시키지 않는 스타일의 변경이 일어났을 때 발생

 

또한 레이어의 합성(compositing)만 다시 발생하는 경우도 존재합니다.

  • Layout과 Paint를 수행하지 않고 레이어의 합성만 발생하기 때문에 성능상으로 가장 큰 이점을 가짐

 

UI가 변경되는 경우는 위 세 가지이며 css 속성에 따라 어떤 경우에 해당하는지를 정리한 사이트는 아래와 같습니다.

  • https://csstriggers.com

 

5. 정리

 

브라우저가 어떻게 화면에 페이지를 그려나가는지에 대한 과정을 알아보았습니다.

 

정리하자면 다음과 같은 과정을 통해 화면에 페이지를 렌더링합니다.

  1. HTML 파싱 및 DOM 트리 생성: 브라우저는 HTML 파서를 사용하여 문서를 해석하고, 각 HTML 태그를 노드로 변환하여 DOM 트리를 구축합니다.
  2. 외부 리소스 처리:
    • CSS 파일: <link> 태그를 통해 CSS 파일을 만나면, 브라우저는 해당 파일을 비동기적으로 다운로드합니다. 그러나 CSS는 렌더링을 블로킹하는 리소스입니다. 브라우저는 CSSOM을 완성하기 전까지 렌더 트리를 생성하지 않으므로, 렌더링이 지연될 수 있습니다. 이는 스타일이 적용되지 않은 콘텐츠가 표시되는 것을 방지하기 위함입니다.
    • 자바스크립트 파일:
      • 기본 <script> 태그: 파서는 <script> 태그를 만나면 HTML 파싱을 중단하고, 스크립트를 다운로드 및 실행한 후에야 파싱을 재개합니다. 이는 스크립트가 DOM 구조나 콘텐츠를 변경할 수 있기 때문입니다.
      • async 속성: <script async>로 지정된 스크립트는 HTML 파싱과 병렬로 다운로드되며, 다운로드가 완료되는 즉시 스크립트를 실행합니다. 이때 파싱은 중단되지 않을 수 있지만, 스크립트 실행 시점에 따라 파싱이 잠시 중단될 수 있습니다.
      • defer 속성: <script defer> 스크립트는 HTML 파싱과 병렬로 다운로드 되며, 문서의 파싱이 완료된 후 스크립트를 실행합니다. 따라서 파싱이 중단되지 않고 진행됩니다.
  3. CSSOM 트리 생성: 다운로드된 CSS 파일을 파싱하여 CSSOM을 생성합니다. CSSOM은 각 요소의 스타일 정보를 나타냅니다.
  4. 렌더 트리 생성: DOM 트리와 CSSOM 트리를 결합하여 렌더 트리를 구성합니다. 렌더 트리는 화면에 표시되어야 할 노드와 그 스타일 정보를 포함하며, 보이지 않는 노드(예: display: none 또는 <head> 등)은 제외됩니다.
  5. 레이아웃 단계: 렌더 트리를 기반으로 각 요소의 정확한 위치와 크기를 계산합니다. 이 과정은 Reflow라고도 불립니다.
  6. 페이트 단계: 계산된 레이아웃 정보를 사용하여 각 요소를 화면에 그릴 준비를 합니다. 색상, 이미지, 텍스트 등 시각적 속성을 처리합니다.
  7. 합성(Compositing) 단계: 페이지가 복잡한 경우 여러 개의 레이어로 분할하여 처리합니다. 각 레이어를 개별적으로 렌더링한 후, 최종적으로 하나의 이미지로 합성하여 화면에 표시합니다.

 

+) Parsing Blocking 자세히 알아보기

HTML을 파싱하다보면 embedded되거나 외부 파일로 연결된 CSS, JS를 만나게 됩니다.(img와 같이 외부 링크를 다운받아야 하는 경우도 마찬가지)

 

DOM parsing을 포함한 대부분의 브라우저 렌더링의 작업들은 메인 쓰레드에서 진행이됩니다. 그렇다면, 중간에 다른 작업이 필요한 경우, 어떠한 우선순위로 작업을 수행할까요?

 

Parser-Blocking Scripts

이름에서부터 알 수 있듯이, 자바스크립트의 파일 또는 코드들을 의미합니다. 기본적으로 <script> 태그를 만나게 되면, 해당 스크립트를 먼저 읽고 실행한 다음 나머지 HTML 파싱을 재개합니다.

 

렌더링 엔진의 메인쓰레드에서 파싱 도중 script 코드를 만나게 된다면, 자바스크립트 해석기에 역할을 넘기고 대기하는데, 여기서 자바스크립트 해석기는 우리가 잘 알고 있는 자바스크립트의 콜스택에서 작업을 수행하게 됩니다.

 

<div>Hello</div>
<script>...</script>
<div>Parse stop</div>

 

따라서 위와 같은 embedded script 들은 모두 메인쓰레드의 HTML 파싱을 중단시키고, 모든 embedded script들은 Parser-Blocking이라고 불립니다.

 

그렇다면, 왜 script 파일을 만나게 된다면, 파싱을 중단하는 것일까요?

 

기본적으로 개발자들은 DOM에 이벤트를 부여하거나, 조작하고 변경할 수 있는데, 이것이 가능한 이유는 브라우저에서 형성된 DOM은 DOM API를 통해 자바스크립트 환경에 노출되기 때문입니다.

  • 다시 말해, Document.write 처럼 자바스크립트의 코드가 이전에 형성되어 있는 DOM의 요소를 바꿀 수 있기 때문입니다.

하지만, DOM의 요소를 바꾸지 않는 script 인데도 불구하고, 다운로드 하는 동안 DOM Parser의 작업이 멈추게 된다는 점은 그만큼의 시간을 더 소모해야 한다는 의미이기도 합니다.

 

async / defer

브라우저가 HTML을 파싱하면서 script를 만났을 때, 로드하는 방식을 지정해주는 방법이 있습니다.

 

앞서 살펴보았듯이 async 혹은 defer 속성을 할당해주는 것입니다.

  • 아무 속성도 없음: DOM 파싱 중 script를 만나면 DOM 파싱 중단 -> script 파일 다운로드 -> script 실행 -> DOM 파싱 재개
  • async 할당: DOM 파싱과 script 파일 다운로드 및 실행 병렬 진행(따라서 순서가 명확하지 않은 DCL 이전, 이후 모두 가능)
  • defer 할당: DOM 파싱과 script 파일 다운로드 동시 진행 -> script 다운로드 완료되어도 DOM 파싱 진행 -> DOM 파싱 종료 이후 스크립트 실행. 단, DCL 이전

 

Render-Blocking CSS (No Parser-Blocking)

script 요소 이외의 img 라던가 CSS 같은 것들은 DOM 파싱을 막지 않습니다. 즉, CSS는 DOM 파싱을 직접적으로 막지 않는데요.

 

하지만 embedded style이나, inline style의 경우 종종 DOM 파싱을 막는 경우도 있습니다.

 

embedded 스타일의 종료를 알리는 </style>을 만났을 때, DOM 파싱이 멈추고 해당 구간의 embedded 스타일을 파싱하여 CSSOM 트리를 업데이트합니다. 그 이후에 DOM 파싱이 재개가 됩니다.

 

그 이유는 CSSOM은 cascading이라는 규칙을 기준으로 CSSOM 트리를 형성하는데 그 특징 중 하나로 동일한 속성의 스타일이라면 최하단의 스타일이 적용되는 '계단식 스타일'이기 때문에 CSSOM은 DOM 트리와 달리 점진적으로 구성되는 것이 불가능합니다.

 

만약 CSSOM도 점진적으로 구성하는 게 가능하다면, 동일한 요소의 동일한 속성에 다른 값으로 부여되는 코드가 여러 줄에 존재할 수 있고, 하나의 속성 변경이 여러 요소에 영향을 주기 때문입니다.

 

그렇게 되면, 계속해서 CSSOM의 업데이트는 렌더 트리를 여러 번 업데이트 시켜 렌더링을 자주 발생시키기 때문에 많은 비용이 소요되고, 결과적으로 사용자에게 좋지 않은 경험을 줄 수 있습니다.

 

styleSheet의 경우 embedded, inline style과 다르게 백그라운드에서 완전히 다운로드 받을 수 있기 때문에 DOM 파싱을 중단시키지는 않습니다. 다만, styleSheet도 앞선 이유 때문에, styleSheet를 처리하고 딱 한 번만 CSSOM 트리를 수정합니다. 그리고 수정된 CSSOM 트리를 통해 렌더 트리가 업데이트 됩니다.

 

이것이 CSS가 Render-Blocking 자원이라고 불리는 이유입니다. 스타일 요소가 읽히는 동안에는 렌더 트리 구성이 중지됩니다.

하지만, '렌더 트리' 빌드가 멈춘 것뿐이지, DOM을 파싱하는 과정은 멈추지 않습니다. DOM 파싱이 종료되어 DOM 트리가 완성되었다고 하더라도, CSSOM 트리가 아직 완성되어 있지 않다면 렌더 트리를 빌드하지 않습니다.

  • 당연하게도 CSS 작업이 완료되지 않았는데 화면에 보여질 요소를 준비한다는 것은 말이 되지 않습니다.

 

위와 같은 상황은 CRP(Critical Rendering Path)가 멈추었다고 표현합니다.(화면을 그리지 않는 상태)

 

또한, styleSheet를 백그라운드에서 다운로드 받을 수 있기 때문에 HTML을 파싱하면서 script 또한 읽힐 수 있습니다.

 

경우에 따라 읽힌 script는 실행될 수도 있는데, 브라우저는 이전의 styleSheet가 파싱되어 있지 않다면 다운로드된 script를 block합니다. 이를 script-blocking styleSheet 또는 script-blocking CSS 라고 합니다.

  • JS로 DOM 트리 요소의 스타일을 수집한 후에 CSSOM이 업데이트 되어 DOM 트리가 변경된다면 JS는 잘못된 스타일 속성을 가질 수 있기 때문입니다.
반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.