본문 바로가기

개발/web

Browser Rendering vs React Rendering - (1)

 

 

개요

 

React로 개발을 계속하다 보면 '렌더링'이라는 단어는 당연하게도 React에서의 렌더링으로 이해하는 경우가 많은데, 실제로 이는 문맥에 따라 브라우저 렌더링을 가리킬 때도 많이 있다. 실제로 가끔 이러한 상황을 마주하면 혼동하는 경우를 자주 목격하여 이번을 계기로 정확하게 정리하고 가려고 한다.

 

 

Rendering

 

먼저, 두가지 케이스에 공통으로 등장하는 렌더링(Rendering)이 무엇인지부터 정확하게 정리하고 가려고 한다.

 

기본적으로 렌더링(Rendering)이란, 구글 검색을 통해 나오는 정의를 보면 사용자에게 보여지는 시각적 요소들을 생성하고 화면에 표시하는 과정을 의미한다. 하지만 조금 더 단순하게 본다면 컨텐츠를 픽셀로 변경하는 과정이라고 볼 수 있다. (여기서 컨텐츠는 브라우저 내에서 상태표시줄을 제외한 부분을 말한다.)

 

그렇다면 브라우저 렌더링(Browser Rendering)리액트 렌더링(React Rendering)은 결국 특정 상황에서 누가 주체가 되어 사용자에게 보여지는 시각적 요소들을 생성하고, 이를 화면에 표시하는 과정이라고 볼 수 있다.

 

그러면 이제 하나씩 파헤쳐 보겠다.

 

 

 

Browser Rendering

 

브라우저 렌더링이란 기본적으로 위의 정의를 브라우저가 수행하는 것으로 시각적 요소를 생성하고 화면에 표시하기 위해 일련의 과정을 거친다. 이를 흔히 렌더링 파이프라인(Rendering Pipeline)이라고 부른다. 이는 브라우저가 동작의 주체가 되어 실행하기 때문에 당연하게도 브라우저마다 다른 렌더링 엔진을 가지고 있어 큰 틀에서는 동일하나 미세하게 차이들이 존재한다.

 

  Chrome Mozilla Firefox Safari
Rendering Engine Blink  Gecko Webkit

 

 

각 브라우저마자 렌더링 최적화 기법이나 하드웨어 가속 사용에 대한 부분에 차이가 있으나, 해당 글을 이러한 비교를 자세히 하기 위한 글이 아니기에 더 알아보진 않고 가장 많이 사용하는 크롬 브라우저(Blink)를 중심으로 알아보려고 한다.

 

 

 

 

전반적인 흐름은 이와 같다.

 

 

 

1. Parsing 

 

네트워크 연결에서 내려오는 첫 번째 리소스는 일반적으로 HTML이다. CSS, JS나 이미지와 같은 다른 리소스는 보통 HTML에 포함되거나 보조 리소스로 가지고 온다.

 

HTML태그는 문서에 의미 있는 계층 구조를 부여하는 역할을 가지고 있는데, 렌더링의 첫 번째 단계로는 이러한 HTML태그를 토큰화하고, 해당 태그를 전달하여 개체를 연결하는 포인터로 구조를 반영하는 객체 모델을 구축하는 것이다. 이러한 객체 모델을 우리는 DOM (Document Object Model) 이라고 한다.

 

간단하게 말하자면 결국 HTML을 브라우저가 해석할 수 있도록 DOM으로 변경한다는 것이다.

 

추가적으로 이 과정 속에서 DOM에 영향을 줄 수 있는 요소를 만난다면 이는 렌더링 차단 리소스로 판단하여 파싱을 멈추고 해당 리소스를 로드하고 로드가 완료된다면 다시 파싱을 시작한다.

 

- stylesheet link tag

- head태그에 있는 async, defer 속성이 없는 script tag

 

여기서 stylesheet에 대한 link tag를 만날 경우에 파싱을 멈추는 데에는 아래와 같은 이유가 있다고 한다.

(https://web.dev/articles/preload-scanner?hl=ko#whats_a_preload_scanner)

 

CSS 파일의 경우 스타일이 적용되기 전페이지의 스타일이 지정되지 않은 버전을 잠시 볼 수 있는 스타일이 지정되지 않은 콘텐츠 플래시 (FOUC)를 방지하기 위해 파싱과 렌더링이 모두 차단됩니다.

 

 

다만 이러한 경우에서 파싱이 중단되어 중요한 리소스를 사용하는 시점이 늦어질 수 있는데, 이를 해결하기 위한 방법으로 preload scanner를 활용하는 방법이 있다고 한다. 이 글에서는 이를 자세히 다루지 않기 때문에 직전에 제공한 링크의 내용으로 대신한다.

 

 

 

2. Style

 

DOM Tree를 파싱한 이후 브라우저가 CSS를 파싱하여 각 노드의 스타일을 계산하는 단계이다.

 

모든 CSS 파일, style 태그, 요소의 인라인 style 속성에 적힌 스타일 정보를 모두 읽고, 이를 바탕으로 CSSOM(CSS Object Model)이라는 트리를 생성하는 단계이다. 이는 결국 스타일 시트를 분석하고 어떤 요소를 어떻게 출력해야 할지에 대한 속성 정보를 정리하는 단계라고 볼 수 있다.

 

이후 CSSOM과 DOM트리를 결합하여 각 DOM 노드에 적용될 최종 스타일을 결정하고 이를 바탕으로 Render Tree를 구성한다.

 

최종 스타일을 결정하는 과정에는 먼저 정의된 규칙에 이후에 정의된 규칙이 들어오더라도 이전 규칙이 더 중요한 규칙이라면 이후에 들어온 규칙을 무시하는 CSS 오버라이딩도 고려되어 있다. (!important를 사용할 시에 그렇다.)

 

Render Tree에는 페이지를 렌더링하는 데 필요한 노드만 포함되기 때문에 display: none 속성을 가진 노드는 포함되지 않는다.

 

이렇게 화면에 표시되는 모든 컨텐츠와 스타일 정보를 가진 Render Tree가 준비된다면 이후의 단계인 Layout 단계를 진행할 수 있다.

 

자세하게는 Render Tree와 이후의 Layout단계를 거쳐 나온 Layout Tree가 다른데 이는 다음과 같다.

 

- Render Tree: 화면에 실제로 표시될 요소들만을 포함한 트리 (표시될 요소를 결정)

- Layout Tree : 렌더 트리에 포함된 요소들이 화면상에서 어디에 어떤 크기로 위치할지에 대한 요소를 가지고 있는 트리 (표시될 요소들에 대한 위치 정보를 계산)

 

 

Layout 단계 전에 Style단계에서 display:none은 제외하고 Render Tree를 반환한다.

 

 

 

Layout전에 Render Tree가 완성된다.

 

 

 

3. Layout (Reflow)

 

위의 Style단계까지 거쳤다면 표시해야 할 노드와 노드 별 계산된 스타일은 가지고 있지만, Viewport에서 노드의 정확한 위치와 크기는 계산하지 않았다. 간단하게는 이를 계산하는 과정이 Layout 단계이다. (Reflow라고도 한다.)

 

Layout단계에서는 parse와 style단계에서 만들어진 DOM와 CSSOM을 결합하여 각 요소들의 Box Model을 반환해 주며 이를 통해 Viewport 내에서 각 요소의 정확한 위치와 크기를 알아낼 수 있다. 이를 통해 Layout Object인 Layout Tree가 완성된다.

 

또한 이때 %, em, rem과 같은 상대단위 절대 단위인 px값으로 치환해 주는 과정도 포함되어 있다.

 

하지만 이 과정은 생각보다 단순하지 않은데, Box Model을 반환하게 되면요소의 내부가 해당 Box Model을 넘어가는 overflow 현상이 일어날 수 있는데, 이러한 경우에는 별도의 옵션을 가지고 있지 않다면 특정 단락에서 줄 바꿈을 해주어야 하기 때문에 내부적으로는 폰트의 크기를 계산하고 어떠한 언어로 나타나는지에 대한 정보도 계산하게 된다. (특정 언어는 좌-우로의 글자흐름이 아닌 우-좌로의 글자흐름이기 때문이다.)

 

내부적으로는 HarfBuzz라는 라이브러리를 사용하여 폰트의 크기를 구한다.

 

 

 

4. Pre-Paint

 

하지만 위 단계를 거치면서 처리되지 않는 몇 가지 CSS가 존재한다. 이는 아래와 같다.

 

- transform

- clip

- opacity

- scroll

 

 

 

 

위의 4가지 속성들이 이전 단계를 거치며 처리되지 않은 이유렌더링이라는 과정 자체는 결국 최초에 HTML을 파싱하면서 진행되는 렌더링 이후에 렌더링을 효율적으로 업데이트할 수 있도록 하는 것도 중요한 목표 중 하나인데, 위와 같은 요소들은 합성 아키텍처를 보다 유연하게 만들기 위해서 분리해 둔 것이다. 

 

이러한 속성들은 GPU에서 직접 처리하게 되고, 이를 렌더러 프로레스가 아닌 GPU 프로세스로 넘겨야 하기 때문에 별도의 트리 형태로 만들게 된다.

 

그렇다면 위의 속성들은 왜 GPU에서 처리하는 것일까?

 

이유는 기존에는 이들은 각 레이어에 할당되는 속성이지만 기존에는 각 레이어에 함께 저장되어 있었기 때문에 특정 노드의 속성이 변경되면 불필요하게 해당 노드의 하위 노드에도 반영을 위해 순회하는 과정이 있었는데 위의 속성들이 해당 페이지 내에서 요소의 크기와 위치를 변경하지 않기 때문브라우저의 렌더링 성능 최적화를 위해 이들은 별도의 레이어를 가지도록 하고, 이후에 각 레이어를 합성(Compositing)하여 최종 이미지를 정하는 방식으로 구현되어 있기 때문이다.

 

따라서 transform이나 opacity와 같은 속성을 가지고 있다면 이는 별도의 레이어로 승격하고, 이로 인해 특정 액션으로 인해 다시 브라우저 렌더링이 일어나더라도 Layout이나 Paint단계를 거치지 않으면서도 GPU의 빠른 속도를 활용하여 부드러운 효과를 구현할 수 있는 것이다.

 

이 단계에서는 위와 같은 속성들로부터 Property Tree라는 객체를 만들어 반환하게 된다. 이는 직전에 언급한 바와 같이 나중에 합성과정에서 사용하게 된다.

 

5. Paint

 

이 단계에서는 이름을 보고 헷갈릴 수 있지만 실제로 Paint하는 것이 아니라, 어떻게 그려야 하는지에 대한 정보를 정리하는 단계이다.

 

Layout단계에서 만들어진 Layout Tree에는 Paint라는 메서드가 존재하는데, 이를 활용하여 drawRect와 같은 명령어를 생성한다.

 

이곳에는 아래의 3가지 정보가 포함된다.

 

- Aciton : Layout Tree에서 가지고 있는 Paint 메서드를 활용한 drawRect와 같은 명령어 (실제로 화면에 그려주는 명령어이다.)

- Position : Element의 위치

- Style : Element의 색상과 같은 값

 

이를 통해 아직 실행하지 않지만 나중에 실행할 수 있는 페인트 작업 기록을 구축하는 단계이다.

 

또한 페인트는 내부적으로 여러 단계로 실행되고 각 페인트 단계가 Stacking Context(쌓임 맥락)라고 부르는 루트 아래 하위 트리를 자체 탐색하는데 그렇기 때문에 이 단계에서 z-index에 대한 계산이 이루어진다.

 

 

6. Layerize

 

 

브라우저는 효율적으로 화면을 보여주기 위해 이전단계에서 만든 결과물들을 토대로 Layer라는 층을 만들고, 각 층들을 조합하여 사용자가 마주하는 화면을 그려주는 방식을 사용하는데, 이를 수행하기 위해 기존에 만든 Tree를 순회하여 특정 속성을 가지고있는 노드들을 별도의 Layer로 분리하는 과정을 거친다. 따라서 LayerizePaint과정의 결과물을 토대로 화면을 쪼개는 과정이다.

 

 

Layout단계로부터 반환된 Layout Tree에서는 일부 속성을 가지고 있다면 별도의 Paint Layer로 분리되는데 해당 조건은 다음과 같다.

 

- position값이 relative거나 absolute인 경우

- translateZ와 같은 3D변환을 위한 속성을 가지고있는 경우

- will-change를 사용하는 경우

- video태그나 canvas 태그를 사용하는 경우

 

 

이렇게 생성된 Paint Layer 중에서도 Compositing Trigger를 가지고 있거나 스크롤 가능한 컨텐츠가 있다면 별도의 Graphics Layer가 생성된다.

 

- position값이 fixed인 경우

- video, canvas, iframe 태그를 사용하는 경우

- translateZ와 같은 3D변환을 위한 속성을 가지고 있는 경우

- transform이나 opacity를 통한 애니메이션을 가지고있는 경우

- will-change 속성을 가지고있는 경우

 

 

이렇게 분리된 Graphics LayerGPU연산이 가능하기 때문에 빠른 스크롤링과 애니메이션이 가능하다.

 

Layerize 단계를 거치게 되면 최종 결과물로 Composited Layer List가 반환된다. 

 

 

 

 

7. Commit

 

 

위와 같은 일련의 과정을 거친 후에는 병렬성의 이점을 가지기 위해 여기까지 진행한 작업은 이제 합성 스레드(Composite Thread)로 넘겨서 처리하고 메인 스레드는 다시 다음 작업으로 자바스크립트를 실행하거나 렌더링 파이프라인을 다시 수행한다. 이때 메인 스레드에서 합성 스레드로 해당 작업을 넘겨주는 이 단계를 Commit이라고 한다.

 

이때는 이전 작업(Layerize)으로부터 나온 최종 결과물인 Composited Layer ListPre-Paint에서 만들었던 Property Tree를 복사하여 합성 스레드로 넘겨주는 작업을 진행한다.

 

 

여기까지가 메인스레드의 작업이었지만 실제 화면에 그려지지는 않았다. 나머지 과정은 합성 스레드에서 담당하기 때문에 다음 단계도 살펴보겠다.

 

 

8. Tiling

 

위에서 언급한 Layer는 말 그대로 화면을 구성하는 하나의 도화지와 같다.

 

화면이 클수록 이러한 Layer는 점점 무거워져 다루기 어려워지는데, 이를 해결하기 위한 아이디어로 Layer를 타일형태로 쪼개서 필요한 부분만 작업하는 방식을 채택했다.

 

따라서 이렇게 컴포지터 스레드에서 Layer를 타일 형태로 분할하는 작업을 Tiling이라고 한다.

 

 

 

9. Raster

 

이렇게 타일 형태로 레이어를 쪼갠 후에는 타일에 저장되어 있는 Paint 과정에서 만들었던 액션인 Draw명령어를 실행한다.

 

Blink엔진에서는 Skia라는 그래픽 라이브러리를 사용하여 Pre-Paint에서 추출한 Property Tree의 결과물을 같이 합성하여 실제로 화면에 어떻게 그려질지에 대한 비트맵을 생성한다. 이렇게 생성한 비트맵은 화면에 그려질 픽셀의 집합으로 GPU 메모리에 저장한다.

 

모든 타일을 Raster화한 후 Quad라는 단위의 데이터를 생성하는데, 이는 타일을 어디에 어떻게 그릴지에 대한 정보를 가지고 있다.

 

 

 

 

10. Activate

 

이 단계는 실제 화면에 그려질 대상을 마지막으로 취합하는 과정이다.

 

래스터 작업은 비동기로 진행되어 컴포지터 스레드에서 작업이 진행중일 때 새로운 커밋이 들어오더라도 새로 들어온 커밋의 결과물이 나오기 전까지는 이전 커밋의 결과물을 보여주어야 한다.

 

이를 위해 Front Buffer와 Back Buffer를 두어 완성된 결과물을 Front Buffer에 담아 실제로 보여주고, 작업하고있는 결과물은 Back Buffer에 업데이트하여 Back Buffer가 완성될 때까지 Front Buffer를 유지하다가, 완성된 후에는 Back Buffer를 Front Buffer로 교체해주는 멀티 버퍼링(Multi Buffering) 패턴을 사용한다.

 

 

이 과정에서 Raster단계에서 만든 Quad를 합성하여 Compositor Frame라는 단위로 묶어 GPU 프로세스로 전달된다. 

 

 

11. Display

 

Compositor Frame에 있는 drawQuad명령어를 하나씩 실행하여 실제로 화면에 나타날 수 있도록 하는 작업이다.

 

 

 

 

Re-cap

 

일반적으로 브라우저 렌더링에 대해 찾아보다보면 parsing -> style -> layout -> paint 까지 정리되어있는 경우도 꽤 있으나, 실제로는 paint까지는 각 노드들에 대한 정보를 취합하는 것까지만 진행하고 이후 Layer라는 계층구조를 만들고 이를 Compositor Thread와 GPU Process를 오가며 합성하여 뷰포트에 나타날 대상을 정리해주는 것이 정확한 내용이기에 이에 대해 다시한번 정리했다. 리액트의 렌더링도 한 글에 담으려 했으나 충분히 내용이 많아 다음 글에서 이어서 정리해보고, 이에 대한 차이를 비교해보겠다.

 

 

참고자료

 

https://dev.to/arikaturika/how-web-browsers-work-the-render-tree-part-7-with-illustrations-24h3

https://so-so.dev/web/browser-rendering-process/

https://www.youtube.com/watch?v=K2QHdgAKP-s

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

https://developer.chrome.com/docs/lighthouse/performance/render-blocking-resources/?hl=ko

https://developer.chrome.com/docs/chromium/blinkng?hl=ko

https://www.youtube.com/watch?v=idgsruQl9f4