브라우저 렌더링

chaeruru·2021년 10월 3일
1
post-thumbnail

브라우저의 주요 기능은 사용자가 선택한 자원을 서버에 요청하고 브라우저에 표시하는 것이다. 그럼 브라우저가 사용자에게 화면을 어떻게 보여주는지 알아보자.

브라우저로 정보 보내고 받기

데이터는 인터넷을 통해 바이트 단위의 패킷 크기로 전송이 된다. 즉, HTML 파일을 브라우저에서 열 때 브라우저는 HTML의 바이트 형태의 데이터를 하드 디스크(또는 네트워크)에서 읽는다. 한 마디로 사용자가 작성한 코드의 실제 문자가 아닌 데이터의 원시 바이트를 읽는다. 따라서 데이터의 원시 바이트를 이해할 수 있는 형식으로 변환해야 한다.

HTML 의 원시 바이트에서 DOM으로 변환

브라우저의 렌더링 엔진은 서버로부터 응답된 HTML을 파싱하여 DOM을 생성하는데 자세한 과정은 다음과 같다.

응답된 HTML 파일은 바이트로 구성되어 있기 때문에 데이터의 원시 바이트는 문자로 변환이 된다. 이때 변환되는 문자열은 meta 태그의 charset 어트리뷰트에 의해 지정된 인코딩 받식을 기준으로 문자열로 변환된다.

변환된 문자열은 문법적 의미를 갖는 코드의 최소 단위의 토큰들로 분해가 된다.

토큰화가 완료되면 토큰은 노드로 변환된다. 토큰의 내용에 따라 문서 노드, 요소 노드, 어트리뷰트 노드, 텍스트 노드가 생성된다. 노드는 이후 DOM을 구성하는 기본 요소가 된다.

HTML 요소 간에는 중첩 관계에 의해 부자 관계가 형성된다. 이러한 HTML 요소 간의 부자 관계를 반영하여 모든 노드들을 트리 자료 구조로 구성하는데 이 노드들로 구성된 트리 자료 구조를 DOM(Document Object Model)이라 부른다.

CSS 가져오기

CSS가 있는 일반적인 HTML 파일에는 다음과 같이 stylesheet가 있다.

<!DOCTYPE html>
<html>
	<head>
	  <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
	</head>
	<body>
    
	</body>
</html>

브라우저가 HTML을 해석하기 시작하면 렌더링은 DOM을 생성해 나가다가 CSS 파일에 대한 link 태그를 발견함과 동시에 DOM 생성을 일시 중지하고 main.css 파일을 서버에 요청한다. CSS도 HTML과 마찬가지로 바이트 형태의 데이터를 받는다.

CSS의 원시 바이트에서 CSSOM으로 변환

브라우저가 CSS 파일을 수신할 때도 HTML 때와 비슷하게 프로세스가 진행된다.

바이트 형태의 데이터는 문자열로 변환된 후 토큰화된다. 그리고 노드로 형성되고 마지막으로 노드들로 트리 자료 구조를 형성하는데 이것을 CSSOM(CSS Object Model)이라 부른다.

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

Render tree

DOM과 CSSOM는 두 개의 독립적인 구조다. DOM과 CSSOM트리는 렌더 트리로 결합된다. 렌더 트리는 페이지에 표시되는 모든 DOM 컨텐츠에 대한 정보와 모든 CSSOM정보가 포함된다. 단 CSS에 의해 숨겨진 요소 노드는 렌더 트리에 표시 되지 않는다.(예시 display: none)

Layout

렌더 트리가 구성되면 다음 단계인 레이아웃 프로세스가 수행된다. 현재 렌더 트리를 구축했지만 화면에 보이는 모든 컨텐츠 정보들만 가지고 있을 뿐 실제로 화면에 아무것도 렌더링되지 않았다.

브라우저는 페이지에 있는 각 요소들의 정확한 크기와 위치를 계산한다. 이 레이아웃 단계는 DOM 및 CSSOM에서 수신한 정보를 바탕으로 모든 레이아웃 작업을 수행한다.

Paint

각 요소의 정확한 위치에 대한 정보가 계산되었기 때문에 이제 요소를 화면에 보여주는 작업만 남았다. 이 작업을 페인트라하며 픽셀을 채우는 작업이다. 텍스트, 색, 이미지, 경계 및 그림자 등 요소의 모든 시각적인 부분을 그리는 작업을 포함한다.

페인트는 실제로 1. 그리기 호출 목록 생성 2. 픽셀 채우기 이렇게 두 작업으로 나뉘고 후자를 래스터화라고 한다.

자바스크립트 파싱

렌더링 엔진이 HTML을 위에서 아래로 한 줄씩 파싱하여 DOM을 생성해나가다가 script 태그를 만나면 DOM 생성을 중지한다. script 태그의 src 어트리뷰트에 정의된 자바스크립트 파일을 서버에 요청한 후 자바스크립트 코드를 파싱하기 위해 자바스크립트 엔진에 제어권을 넘긴다. 자바스크립트 파싱과 실행이 끝나면 제어권이 다시 렌더링 엔진으로 넘어가고 DOM 생성을 재개한다.

자바스크립트 엔진은 자바스크립트 코드를 파싱하여 CPU가 이해할 수 있는 저수준 언어로 변환하고 실행하는 역할을 한다. 자바스크립트 엔진은 자바스크립트를 해석하여 AST(Abstract System Tree)를 생성한다. AST를 기반으로 인터프리터가 실행할 수 있는 중간 코드인 바이트코드를 생성하여 실행한다.

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

자바스크립트를 통해서 페이지의 컨텐츠와 스타일을 수정할 수 있다. 즉, DOM 트리에서 요소를 추가하거나 제거할 수 있고 CSSOM의 속성을 수정할 수도 있다. 이러한 이유로 자바스크립트가 무엇을 할지 확신할 수 없기 때문에 브라우저는 HTML을 파싱하다가 script 태그를 만나면 스크립트 실행이 완료될 때까지 DOM 생성이 중지된다.

만약 DOM 생성이 완료되지 않은 상태에서 자바스크립트가 DOM을 변경하면 어떻게 될까?

script 태그를 head에 위치시켜보자

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <script>
      const $hello = document.getElementById('hello');
      $hello.textContent = 'hi';
    </script>
  </head>
  <body>
    <div id="hello">hello</div>
  </body>
</html>

에러가 발생하면서 우리가 의도와 다르게 text가 바뀌지 않았다.

브라우저는 위에서 아래로 순차적으로 직렬적으로 파싱을 수행하는데 script 태그가 수행되는 시점에선 아직 body 의 내용을 파싱하지 않아서 DOM에 접근할 수 없기 때문에 에러가 발생했다.

이번엔 script 태그를 body 의 맨밑에 넣어보자

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <div id="hello">hello</div>
    <script>
      const $hello = document.getElementById('hello');
      $hello.textContent = 'hi';
    </script>
  </body>
</html>

에러 없이 우리가 의도한대로 정상적으로 동작이 됐다.

이것은 script 태그의 위치가 굉장히 중요하다는 것을 의미한다. 만약 외부 파일을 가져오는 script 태그가 head 에 있을 때 네트워크가 느리거나 파일의 크기가 커서 시간이 오래 걸린다면 그만큼 HTML 파싱이 지연되어 빈 화면이 오래 보이고 성능에 큰 영향을 끼칠 수 있다.

만약 script 태그를 만났지만 CSSOM이 준비되지 않은 경우엔 CSSOM이 준비될 때까지 자바스크립트 실행이 중단된다.

마무리

브라우저가 사용자로부터 페이지를 표시하는데까지 HTML, CSS, JS 파일들을 어떻게 처리하는지 그리고 script 의 위치의 중요성에 대해 알 수 있었다. 앞으로는 프론트엔드 개발자로서 어떻게 하면 성능에 대한 최적화를 할 수 있을지 공부해야겠다.

Reference

How browser rendering works - behind the scenes - LogRocket Blog

렌더링 트리 생성, 레이아웃 및 페인트 | Web | Google Developers

책 - 모던자바스크립트 Deep Dive

profile
알고리즘과 프론트엔드 부셔버리기

0개의 댓글