본문 바로가기

개발👩‍💻/프론트엔드

브라우저의 기능과 렌더링 엔진 동작 과정

728x90

사용자가 브라우저를 사용할 때 어떠한 과정을 통해 사용자에게 자원이 보여지는지 원리를 알아보고자 한다.

🔑 키워드

브라우저 동작 방식, 렌더링 순서

 

 브라우저의 기능

브라우저의 역할

브라우저는 하나의 소프트웨어로 사용자의 요청에 의해 자원을 서버에 요청하여 해당 자원을 사용자에게 보여준다.
이 때 자원은 주로 HTML 파일을 의미하나, PDF나 이미지와 같은 파일이 될 수도 있다.

사용자는 해당 자원의 주소를 URI(Uniform Resource Identifier)로 명시할 수 있다.

브라우저의 호환성

브라우저는 기본적으로 HTML과 CSS를 해석하여 이를 사용자에게 보여주게 된다.
이와 같은 파일의 해석을 위한 명세가 있고, 이는 웹의 표준을 정리해 놓은 것인데, W3C(World Wide Web Consortium)에서 관리한다. 이를 통해 브라우저의 호환성을 높일 수 있었다.
예전엔 여러 브라우저들이 해당 명세를 부분적으로만 따르고 나머지는 자체적으로 확장해서 개발하였다. 이와 같은 부분이 호환성 문제를 야기했으나 최근에는 대부분의 브라우저가 표준 명세를 따른다.

브라우저의 기본 구조

브라우저의 주요 구성 요소는 아래와 같다.

  1. 사용자 인터페이스 - 주소 표시줄, 이전/다음 버튼, 북마크 메뉴 등. 요청한 페이지를 보여주는 창을 제외한 나머지 모든 부분이다.
  2. 브라우저 엔진 - 사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어.
  3. 렌더링 엔진 - 요청한 콘텐츠를 표시. 예를 들어 HTML을 요청하면 HTML과 CSS를 파싱하여 화면에 표시함.
  4. 통신 - HTTP 요청과 같은 네트워크 호출에 사용됨. 이것은 플랫폼 독립적인 인터페이스이고 각 플랫폼 하부에서 실행됨.
  5. UI 백엔드 - 콤보 박스와 창 같은 기본적인 장치를 그림. 플랫폼에서 명시하지 않은 일반적인 인터페이스로서, OS 사용자 인터페이스 체계를 사용.
  6. 자바스크립트 해석기 - 자바스크립트 코드를 해석하고 실행.
  7. 자료 저장소 - 이 부분은 자료를 저장하는 계층이다. 쿠키를 저장하는 것과 같이 모든 종류의 자원을 하드 디스크에 저장할 필요가 있다. HTML5 명세에는 브라우저가 지원하는 ‘ 웹 데이터 베이스 ‘가 정의되어 있다.

렌더링 엔진

렌더링 엔진은 HTML 및 XML 문서와 이미지를 표시할 수 있다. 물론 플러그인이나 브라우저 확장 기능을 통해 PDF 등의 다른 유형도 표시할 수 있긴 하다.
여기서는 HTML과 이미지를 CSS로 표시하는 사용 패턴에 초점을 맞춘다.

렌더링 엔진으로는 파이어폭스는 게코, 사파리와 크롬은 웹킷 엔진을 사용한다.

렌더링 엔진 동작과정

1) 렌더링 엔진은 HTML을 파싱하여 내부 태그(elements)를 DOM(Document Object Model) 노드로 변환한다. CSS도 외부 파일과 함께 포함된 스타일 요소도 파싱하여 CSSOM(Cascading Style Sheet Object Model)로 변환한다.

2) 스타일 정보와 HTML 표시 규칙은 렌더 트리(Render Tree)라 하는 또다른 트리를 생성한다. 그리고 이 렌더트리는 색상 또는 면적(demention)의 정보를 포함한 시각적 속성이 있는 사각형을 포함하며 이는 정해진 순서대로 화면에 표시된다.

3) 화면에 각 노드가 정확한 위치에 위치하도록 렌더 트리를 배치한다.

4) UI 백엔드 레이어를 사용하여 렌더 트리의 각 노드를 순회하며 그리는 과정이다.

이는 점진적인 과정임을 이해하는 것이 중요하다. 더 나은 사용자 경험을 위해 렌더링 엔진은 가능한 빨리 화면에 콘텐츠를 보여줄 수 있도록 할 것이다. 그렇기 때문에 모든 HTML 이 파싱되어 렌더 트리가 만들어질 때까지 기다렸다가 생성한 후 배치하고 하는 것이 아니다.
네트워크로부터 나머지 내용이 전송되기를 기다리는 동안 먼저 받은 콘텐츠의 일부를 파싱하고 화면에 표시하는 것이다.

 

 

최근 브라우저의 렌더링 동작 과정

기본적인 동작 과정은 파싱 -> render tree 생성 -> 배치 -> 패인트 를 따른다.

여기서 최근 브라우저 (크롬기준) 는 레이어를 나누고 페인트를 합성하는 과정이 추가되어 별도의 레이어를 분리해 레스터화하고 컴포지터 스레스로 작업을 넘겨 레스터화(요소의 속성 정보들을 화면의 픽셀로 변환하는 작업) 되어 있는 프래임들을 합성하게 된다. 이렇게 함으로써 합성의 과정을 메인스레드와 별개로 작동하게 할 수 있다. 컴포지터 스레드는 JS실행이나 스타일 계산을 기다리지 않아도 된다. (reflow, repaint의 경우는 메인 스레드가 관여한다)

 

HTML 문서 파싱

일반적인 파싱은 어휘적으로 구문적으로 정해진 규칙을 따라야 하고 이는 문맥 자유 문법이라고 한다. 그렇기 때문에 규칙에 있는 어휘(IF, FOR..)와 구문({..}) 등을 따라야 하고 그렇지 않으면 Exception error를 만나게 될 것이다.
CSS와 JS 또한 이러한 일반적인 파싱을 따른다.

HTML 파서

그러나 HTML 파서는 일반적인 파싱 방법을 따르지 못하는데 이유는 다음과 같다

  1. 언어의 너그러운 속성
  2. 잘 알려져 있는 HTML 오류에 대한 브라우저의 관용
  3. 변경에 의한 재파싱, 일반적으로 소스는 파싱하는 동안 변하지 않지만 HTML에서 document.write를 포함하고 있는 스크립트 태그는 토큰을 추가할 수 있기 때문에 실제로는 입력 과정에서 파싱이 수정된다.

그렇기 때문에 브라우저는 HTML 파싱을 위해 별도의 파서를 생성한다.
알고리즘은 토큰화트리구축 이렇게 두단계로 되어 있다.

토큰화는 어휘 분석으로서 입력 값을 토큰으로 파싱한다. (토큰: 시작태그, 닫는태그, 속성 이름, 속성 값)
토큰화는 토큰을 인지하여 트리 생성자로 넘기고 다음 토큰을 확인하기 위해 다음 문자를 확인한다. 그리고 입력의 마지막까지 이 과정을 반복한다.

 

메인 스레드에서 해당 DOM을 구축하기 위해 파싱하는 동안 다른 리소스가 필요하다면 메인 스레드가 멈추가 하나씩 만날 때 마다 요청할 수도 있겠지만 그렇기에는 시간이 너무 오래 걸릴 것이다. 그렇기 때문에 프리로드(Preload) 스캐너가 동시에 실행된다. 프리로드 스캐너는 HTML 문서에 <img> 또는 <link>와 같은 태그가 있으면 프리로드 스캐너는 HTML 파서가 생성한 토큰을 확인하고 브라우저 프로세스의 네트워크 스레드에 요청을 보낸다.

 

파싱이 끝난 후

파싱 이후에 브라우저와 문서가 상호작용 가능하다.

파싱 이후 '지연(deferred)' 스크립트 파싱을 시작한다. 

해당 스크립트 파싱은 문서가 모두 파싱된 후에 실행된다.

문서 상태는 완료(complete)가 되고 로드(load) 이벤트가 발생한다.

 

브라우저 오류 처리

HTML 파서는 너그럽게 오류를 넘어간다.
그렇기 때문에 HTML 페이지에서는 ‘유효하지 않은 구문’이라는 오류를 본 적 없다. 모두 브라우저에서 오류 구문을 교정해주기 때문이다.
파서는 적어도 다음과 같은 오류를 처리해야 한다.

  1. 어떤 태그의 안쪽에 추가하려는 태그가 금지된 것일 때 일단 허용된 태그를 먼저 닫고 금지된 태그는 외부에 추가한다.
  2. 파서가 직접 요소를 추가해서는 안된다. 문서 제작자에 의해 뒤늦게 요소가 추가될 수 있고 생략 가능한 경우도 있다. HTML, HEAD, BODY, TBODY, TR, TD, LI 태그가 이런 경우에 해당한다.
  3. 인라인 요소 안쪽에 블록 요소가 있는 경우 부모 블록 요소를 만날 때까지 모든 인라인 태그를 닫는다.
  4. 이런 방법이 도움이 되지 않으면 태그를 추가하거나 무시할 수 있는 상태가 될 때까지 요소를 닫는다.

이와 같이 HTML 파서는 일반적인 파서와 다르게 조금 더 오류에 대한 아량이 있다.

 

스크립트와 스타일 시트 진행 순서

스트립트

웹은 파싱과 실행이 동시에 진행되는 동기화(synchronous) 모델이다.

우리는 JS 를 통해서 document.write와 같은 메서드를 사용하여 DOM을 직접적으로 변경할 수 있다

그렇기 때문에 기존에는 <script> 태그를 만나면 문서 파싱을 멈추고 스크립트를 즉시 파싱하고 실행한다.

만약 스크립트가 외부에 있다면 스크립트를 통신해서 가져와 파싱하고 실행하는 모든 과정을 거치고 해당 과정동안 기존 문서를 파싱하는 동작은 멈추어있게 된다.

 

그러나 스크립트에 속성을 추가할 수 있게 되었고, 지연(defer) 모드와 비동기(async) 모드로 사용할 수 있다. 그렇게 되면 문서의 파싱을 막지 않고 문서가 끝난 후 스크립트를 파싱하거나, 비동기적으로 로딩하고 실행하면서 파싱을 막지 않을 수 있다.

지연 모드는 모든 문서가 파싱된 후 스크립트가 실행된다.

비동기모드는 별도의 스레드에서 파싱되고 실행된다.

또한 JS 모듈을 사용할 수 있다.

<link rel="preload"> 를 이용해 로드를 위해 반드시 필요한 리소스를 미리 다운로드하여 사용할 수도 있다.

스타일 시트

원래는 스타일 시트는 DOM 트리를 변경하지 않기 때문에 스타일 시트를 읽는데 기존 문서를 파싱하는 것을 중단할 필요가 없다.

그러나 스트립트 문서에서 스타일 관련 정보를 요청하는 경우라면 미리 로드되지 못한 스타일 시트 속성으로 인해 문제가 발생할 수 있다.

그렇기 때문에 아직 로드 중이거나 파싱 중인 스타일 시트가 있다면 모든 스크립트의 실행을 중단하거나(파이어폭스) 스타일 시트 내 속성에서 문제를 야기할 만한 속성이 있을 때에만 스크립트 실행을 중단(웹킷)한다.

 

DOM 트리와 렌더 트리 관계

렌더 객체는 DOM 요소와 부합하지만 1:1 대응 관계는 아니다.

예를 들어 DOM 요소 중 <head>와 같이 비시각적인 요소는 렌더트리에 추가되지 않는다.

또한 display:none; 도 렌더트리에 추가되지 않는다. (다만, visibility: hidden 같은 경우는 트리에 추가된다.)

 

더하여 float 이나 position: absolute와 같이 흐름이 바뀌는 렌더 객체는 DOM 노드에 대응하지만 동일한 위치에 있지 않고 다른 곳에 배치된다. 그러나 자리 표시자가 원래 있어야 할 곳에 배치된다.

 

스타일 계산

렌더링 엔진은 스타일 시트를 파싱하고 다단계(cascading) 방식으로 해당 스타일을 적용한다. 

스타일 객체를 만들고 해당 객체를 참고하여 빠르게 스타일을 적용할 수 있도록 한다.

규칙트리와 스타일 문맥 트리를 만들어 중복 계산을 줄이기도 한다.

 

다단계 순서와 규칙

 

  • 브라우저 선언 (browser declarations)
  • 사용자 일반 선언 (user normal declarations)
  • 저작자 일반 선언 (author normal declarations)
  • 저작자 중요 선언 (author important declarations)
  • 사용자 중요 선언 (user important declarations)

위로 갈 수록 중요도가 낮고 아래로 갈 수록 중요도가 높다.

 

선택자 특정성

스타일 시트에서 선택자에 따라 중요도가 달라지고 케스케이딩 되어 최종 스타일을 적용하는데,

우선 순위를 계산한다.

 

  • 선택자 없이 'style' 속성이 선언된 것이면 1을 센다. 그렇지 않으면 0을 센다. (=a)
  • 선택자에 포함된 아이디 선택자 개수를 센다. (=b)
  • 선택자에 포함된 속성 선택자(클래스 선택자와 속성 선택자)와 가상 클래스 선택자의 숫자를 센다. (=c)
  • 선택자에 포함된 요소 선택자와 가상 요소 선택자의 숫자를 센다. (=d)

네 개의 연결된 숫자 a-b-c-d (큰 진법의 숫자)를 연결하면 특정성의 값이 된다.

사용할 진법은 분류 중에 가장 높은 숫자에 의해서 정의된다. 

 

 

배치(Layout)

레이아웃은 요소의 크기(size)와 위치(position) 정보를 계산하는 것을 이야기한다 (reflow)

 

단일 경로(single pass)를 통해 요소의 배치를 왼쪽에서 오른쪽으로(left to right) 혹은 위에서 아래로(top to bottom)으로 흐른다.

(표는 하나이상의 경로(pass)를 필요로 함으로 예외)

 

<html> 최상위 태그서 부터 시작하여 프레임를 계산한다.

 

자신이나 자기 자식이 변화하며 새롭게 배치해야하는 경우 그것을 '더티(dirty)'와 '자식이 더티(children are dirty)'라는 플래그를 표시한다.

 

전역 배치(Global)와 점증(Incremental) 배치

전역 배치는 html의 전체 폰트 사이즈가 바뀌거나 화면이 리사이즈될 때에 보통 동기적으로 일어난다.

 

점증 배치는 렌더러가 더티일 때 비동기적으로 일어난다.

 

그리기(Paint)

블록을 그리는 순서는 다음과 같다.

  1. 배경 색
  2. 배경 이미지
  3. 테두리
  4. 자식
  5. 아웃라인

동적 변경

동적 변경이 일어났을 때 브라우저는 최소한의 동작으로 반응하려고 노력할 것이다.

그렇기 때문에 visibility, outline, background 변경 등이 일어나면 repaint만 발생하게 한다.

요소의 위치(position)가 바뀌면 요소와 오쇼의 자식 및 형제한테도 reflow와 repaint가 일어난다. 

DOM 노드를 추가하면 노드의 reflow와 repaint 가 발생한다. 

html 요소의 글꼴 크기 변경과 같은 큰 변경은 캐시를 무효화하고 트리 전체의 배치와 리페인팅이 발생한다.

 

레이어 트리 만들기

레이아웃 과정을 통해 레이아웃 트리를 만들고 나면 메인 스레드는 레이아웃 트리를 순회하며 여러 레이어 드리를 만든다. (will-change, transformZ 를 사용해서 요소를 자체 레이어로 사용자가 승격시킬 수 있다, will-change 속성을 사용하면 해당 레이어는 GPU에 업로드 된다.)

하지만 레이어가 너무 많으면 합성 비용이 올라갈 뿐아니라 메모리 비용도 높아지는 부담이 있다.

크롬은 레이어가 과도하게 많아지는 것을 막기 위해 특정 경우 레이어를 생성하지 않거나 합치지도 한다.

 

래스터화와 합성

레이어 트리를 만들고 페인트 순서가 결정되면 메인스레드가 컴포지터 스레드로 정보를 넘긴다.

그러면 컴포지터스레드는 각 레이어를 타일 형태로 나눠 타일을 래스터 스레드로 넘긴다.

래스터 스래드는 각 타일을 래스터화해 GPU 메모리에 저장한다.

 

컴포지터 스레드는 래스터 스레드의 우선순위를 지정할 수 있다.

컴포지터 스레드는 여러 레이어 트리를 가지며, 

메인 스레드가 넘긴 레이어 트리는 펜딩 트리로 넘어가고

새로운 정보로 화면을 갱신할 때는 기존 액티브 트리(이전 프레임을 그린)와 펜딩트리와 교체한다.

 

이렇게 합성된 프레임은 GPU로 전송되어 화면에 표시되고,

스크롤 이벤트와 같은 새로운 화면을 표시할 이벤트가 나타나면 컴포지터 스레드는 GPU로 보낼 새로운 프레임을 만든다.

🍯  요약

브라우저는 사용자가 요청하는 자원을 서버에서 받아 사용자에게 보여주는 소프트웨어이고 해당 자원은 HTML, PDF, 이미지 등이 있으며 자원의 주소는 URI로 지정된다.

 

브라우저 기본 구조: 사용자 인터페이스, 브라우저 엔진, 렌더링 엔진, 통신, 자바스크립트 해석기, UI 백엔드, 자료 저장소

 

렌더링 엔진: 자원을 받아 화면에 그리는 역할

렌더링 엔진 동작 방식: 1️⃣HTML 파싱 -> 2️⃣DOM+CSSOM=Render Tree 생성 -> 3️⃣렌더트리 배치 -> 4️⃣렌더트리 페인트

 

새로운 브라우저 추가된 렌더링 동작 방식: 레이어 트리 만들기, 합성하기

이점: 렌더링 과정을 좀더 분할된 스레드로 처리

 

파싱: 일반적인 파싱(언휘분석 + 구문 분석) => 문맥 자유 문법 (ex: CSS, JS)

1️⃣HTML 파싱: 너그러운, 예전버전모두지원, JS로 추가 태그 생성 가능 => 구문적 오류 자동 수정 (구문 오류 x)

     HTML 파싱: ① 토큰화 + 트리 구축 (반복) -> ② 지연 스크립트 파싱

 

HTML 파싱 중 script 를 만나면 문서 파싱을 중단하고 스크립트를 읽는다. 기존에는 스크립트를 동기적(sync)으로 파싱 + 실행

스크립트 속성(지연, 비동기)을 변경하여 문서 파싱 중단을 막을 수 있다

 

렌더 객체와 DOM 요소는 부합하나 1:1 대응관계는 아니다 (예: display: none; )

 

선택자 우선순위: style 속성이 선언된 요소 > 아이디 선택자 (#footer1) > 클래스 선택자 (.footer), 속성 선택자 ([title]), 가상 클래스 선택자 (:link, :hover, :nth-child) > 요소 선택자 (h1, div), 가상 요소 선택자 (::after, ::before)

 

🔖 참고자료

https://d2.naver.com/helloworld/59361

https://web.dev/howbrowserswork/#parsing-general

https://wit.nts-corp.com/2019/02/14/5522

https://d2.naver.com/helloworld/5237120

https://www.slideshare.net/deview/125-119068291/

https://velog.io/@yrnana/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%A0%8C%EB%8D%94%EB%A7%81%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4%EC%99%80-%EC%B5%9C%EC%A0%81%ED%99%94

반응형

'개발👩‍💻 > 프론트엔드' 카테고리의 다른 글

정규식  (0) 2022.11.14
브라우저 렌더링 최적화(Reflow, Repaint, Composite)  (0) 2022.11.14
JS for 반복문(foreach, for..in, for..of)  (0) 2022.09.23
Babel (2): macro  (0) 2022.08.01
Babel (1): Babel과 plugin  (0) 2022.08.01