[FE] 브라우저 렌더링 과정

선영·2022년 12월 31일
0

Front-End

목록 보기
4/4
post-thumbnail

💡 웹 브라우저의 구조


  • 유저 인터페이스: 주소 표시줄, 이전/다음/새로고침 버튼 등 웹 페이지를 제외하고 사용자와 상호작용 하는 사용자 인터페이스
  • ⭐️ 렌더링 엔진: HTML과 CSS, js로 작성된 문서를 파싱하여 요청한 웹 페이지를 표시(즉, 시각적으로 출력 = 렌더링)
  • 브라우저 엔진: 유저 인터페이스와 렌더링 엔진을 연결
  • 네트워킹: 각종 네트워크 요청을 수행
  • UI Backend: 체크박스나 버튼같은 기본적인 위젯
  • 저장소: localStorage, SessionStorage, Cookie와 같이 보조 기억장치데이터를 저장하는 파트
  • 인터프리터: 자바스크립트 코드를 실행하는 엔진(크롬의 경우 V8), 소스코드를 머신코드로 번역하는 컴파일러와 다르게 인터프리터는 소스코드를 바로 실행하는 수퍼두퍼!

💡 브라우저의 렌더링 엔진

웹 브라우저마다 렌더링 엔진은 다르다. 사파리는 웹킷, 파이어폭스는 게코, 크롬은 블링크 등이 있다. 렌더링 엔진의 공통적인 목표는 다음과 같다.

  • HTML, CSS, JS, 이미지 등 웹 페이지에 포함된 모든 요소들을 화면에 보여준다.
  • 업데이트시 효율적으로 렌더링 할 수 있도록 자료구조를 생성한다.
    사용자 동작으로 인해서 입력발생, 스크롤, 애니메이션 동작, 비동기요청으로 인한 데이터 로딩 등이 있다.

렌더링 엔진의 동작 과정 - CRP(Critical Rendering Path)

브라우저가 렌더링을 수행하는 전체적인 과정은 아래와 같다.

  • 브라우저는 html, css, js, 이미지, 폰트 등.. 렌더링에 필요한 리소스를 요청하고 서버로부터 응답받는다.

  • 브라우저의 렌더링 엔진은 서버로부터 응답한 html과 css를 파싱하여 DOM, CSSOM을 생성하고, 이를 결합하여 렌더 트리를 생성한다.

  • 브라우저의 js엔진은 서버로부터 응답된 js를 파싱하여 AST(Abstract Syntax Tree)를 생성하고 바이트코드로 변환하여 실행한다. 이때 js는 DOM API를 통해 DOM이나 CSSOM으로 변경할 수 있다. 변경된 DOM, CSSOM은 다시 렌더 트리로 결합된다.

  • 렌더 트리를 기반으로 html요소의 레이아웃(위치와 크기)를 계산하고, 브라우저 화면에 html요소를 페인팅한다.

요청과 응답

브라우저의 핵심 기능은 필요한 리소스(html, css, js, 이미지, 폰트 등.. 의 정적 파일 또는 서버가 동적으로 생성한 데이터)를 서버에 요청하고, 서버로부터 응답받아 브라우저에 시각적으로 렌더링하는 것이다. 즉, 렌더링에 필요한 리소스는 모두 서버에 존재하므로 필요한 리소스를 서버에 요청하고 서버가 응답한 리소스를 파싱하여 렌더링하는 것이다.
파싱: 구문 분석. 즉 프로그래밍 언어의 문법에 맞게 작성된 텍스트 문서를 읽어 들여 실행하기 위해 텍스트 문서의 문자열을 토큰으로 분해, 토큰에 문법적 의미와 구조를 반영하여 트리구조의 자료구조인 파스트리를 생성하는 일련의 과정을 말한다.

서버에 요청을 전송하기 위해 브라우저는 주소창을 제공한다. 브라우저의 주소창에 url을 입력하고 엔터 키를 누르면 url의 호스트 이름이 dns(domain name system)를 통해 ip주소로 변환되고, 이 ip주소를 갖는 서버에게 요청을 전송한다. 구체적인 동작과정은 주소창에 naver.com을 치면 일어나는 일을 참고하자. 아래 그림은 SPA(Single Page Application) 기본지식에서도 참고했었다.
DNS(domain name system): 일반적으로 컴퓨터의 식별용 주소로 12자리의 숫자로 구성된 ip주소가 필요하다. 도메인 주소는 이런 ip주소를 사람들이 외우기 쉽게 문자로 표현한 것이다. 그런데 도메인 주소는 사람들의 편의성을 위한 것이므로 실제론 컴퓨터가 이해하기 위해 ip주소로 변환하는 작업이 필요하고, 이럴때 ip+도메인 주소를 쌍으로 모아놓은 데이터베이스인 dns를 사용한다.

HTML파싱과 DOM생성

아래와 같은 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 src="app.js"></script>
  </body>
</html>

브라우저 렌더링 엔진은 아래 그림과 같은 과정을 통해 응답받은 html문서를 파싱하여 브라우저가 이해할 수 있는 자료구조인 dom(document object model)을 생성한다. 자세한 과정은 다음과 같다.

  • 브라우저는 서버가 응답한 html문서를 바이트(2진수) 형태로 응답받는다. 참고로 <meta charset="UTF-8">의 인코딩 방식(UTF-8)을 기준으로 문자열로 변환된다.

  • 문자열로 변환된 html문서를 읽어 들여 문법적 의미를 갖는 코드의 최소한의 단위토큰으로 분해한다.

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

  • html문서는 html요소들의 집합으로 이루어지며 html요소는 중첩 관계를 갖는다. 이에 따라 요소 간에는 부자 관계가 형성된다. 이를 반영하여 모든 노드들을 트리 자료구조로 구성한다. 이 노드들로 구성된 트리 자료구조를 dom이라 부르는 것이다.

렌더 트리 생성

렌더링 엔진은 서버로부터 응답된 html, css를 파싱하여 각각 dom, cssom을 생성한다. 그리고 dom, cssom은 렌더링을 위해 렌더 트리로 결합된다. 이때 렌더 트리는 브라우저 화면에 보이는, 즉 렌더링되는 노드만으로 구성된다. 이후 완성된 렌더 트리는 각 html요소의 레이아웃(위치와 크기)를 계산하는데 사용되고, 브라우저 화면에 픽셀을 렌더링하는 페인팅처리에 입력된다. 마지막으로 레이어를 합성하는 컴파짓까지 처리된다.

앞서 본 브라우저 렌더링 과정은 다음과 같은 경우 반복해서 레이아웃 계산(reflow)과 페인팅(repaint), 그리고 컴파짓(composite)이 실행될 수 있다. 즉, 생성된 dom은 html요소와 스타일 등을 변경할 수 있는 프로그래밍 인터페이스로서 dom api를 제공한다. 만약 js코드에서 dom api가 사용된 경우 dom이나 cssom이 변경될 수 있다. 이와 같은 리렌더링은 성능에 악영향을 줄 수 있으므로 주의해야한다.

  • js에 의한 노드 추가 혹은 삭제
  • 브라우저 창의 리사이징에 의한 뷰포트 크기 변경
  • html요소의 레이아웃(위치, 크기)를 변경시키는 스타일 변경

외부 리소스를 로드하는 태그

그럼 html코드를 파싱하는 도중에 외부 리소스를 로드하는 태그, 즉 css파일을 로드하는 link태그, 이미지 파일을 로드하는 img태그, js를 로드하는 script태그 등을 만나면 어떻게 될까? 브라우저의 렌더링 엔진과 자바스크립트 엔진은 동기적, 즉 직렬적으로 파싱을 처리하기때문에 html의 파싱을 일시 중단하고 해당 리소스 파일을 서버로 요청하게 된다.

예를 들어, <link rel="stylesheet" href="style.css">를 만나면, dom생성을 일시중단하고 href어트리뷰트의 css파일을 서버에 요청하고, css파일이 서버로부터 응답됐다고 가정한다면, 렌더링 엔진은 앞서본 html과 동일한 해석 과정(바이트 > 문자 > 토큰 > 노드 > cssom)을 거쳐 css를 파싱해서 cssom을 생성한다. 이때 cssom은 css의 상속관계를 반영하여 생성된다.

그리고 <script src="app.js"></script>을 만나게 되면, dom생성을 일시중단하고 src어트리뷰트의 js파일을 서버에 요청하여 로드한 js코드를 파싱하기 위해 js엔진에게 제어권을 넘긴다. 제어권을 넘겨받은 js엔진은 js코드를 파싱하기 시작한다. 렌더링 엔진이 html, css를 파싱하여 dom, cssom을 생성하듯 js엔진도 js를 해석해서 AST(Abstract Syntax Tree - 추상적 구문 트리)를 생성한다. 그리고 이를 기반으로 인터프리터가 실행할 수 있는 중간 코드인 바이트코드를 생성하여 실행한다. 이후 js파싱과 실행이 종료되면 다시 렌더링 엔진으로 제어권이 넘어가 html파싱이 중단된 지점부터 다시 dom생성을 재개한다. 전체적인 그림은 다음과 같다.
인터프리터: 프로그래밍 언어의 소스 코드를 바로 실행하는 컴퓨터 프로그램 또는 환경을 말한다. 원시 코드를 기계어로 번역하는 컴파일러와 대비된다.

이것은 script태그의 위치에 따라 html파싱이 블로킹되어 dom생성이 지연될 수 있다는 것을 말한다. 따라서 script태그의 위치는 중요하다. 그렇기 때문에 아래와 같이 body요소의 가장 아래에 js를 위치시켜 js가 실행될 시점엔 이미 렌더링 엔진이 html요소를 모두 파싱하여 dom생성을 완료한 이후가 좋다. 이렇게 하면 dom완성전에 js가 dom을 조작해서 에러가 발생할 우려도 없고, js가 실행되기 전에 dom생성이 완료돼 렌더링 되므로 페이지 로딩시간이 단축된다.

<!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 src="app.js"></script>
  </body>
</html>

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

앞서 살펴본 js파싱에 의한 dom생성이 블로킹되는 문제를 근본적으로 해결하기 위해 html5부터 script태그에 asyncdefer어트리뷰트가 추가됐다. 이는 src어트리뷰트를 통해 외부 js 파일을 로드하는 경우에만 사용할 수 있다. 해당 어트리뷰트들을 사용하면 html파싱과 js파일의 로드가 비동기적으로 동시에 진행된다. 하지만 js실행 시점에는 async,defer각각 차이가 있다.

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

async는 js의 파싱과 실행은 js파일의 로드가 완료된 직후 진행되며, 이때 html파싱이 중단된다. 따라서 여러개의 script태그에 async어트리뷰트를 지정하면 script태그의 순서와 상관없이 로드가 완료된 js부터 먼저 실행되므로 순서가 보장되지 않는다.

defer는 js의 파싱과 실행은 html파싱이 완료된 직후, 즉 dom생성이 완료된 직후 진행된다. 따라서 dom생성이 완료된 이후 실행되어야 할 js에 유용하다.

☑️ 참고

🔗 체프의 브라우저 렌더링
📖 js deep dive | 브라우저의 렌더링 과정

profile
Superduper-India

0개의 댓글