본문 바로가기

개발/React

Browser Rendering vs React Rendering - (2)

출처 : https://www.telerik.com/blogs/understand-how-rendering-works-react

 

개요

 

이전 글에서는 Browser Rendering에 대해서 살펴보았다. 이번에는 비교하려고 했던 React Rendering을 살펴보고 둘의 차이를 명확하게 정리하면서 마무리하려고 한다.

 

 

 

React Rendering

 

 

이는 브라우저 렌더링에서의 해석과 동일하게 Rendering의 주체가 React인 것이다.

 

리액트의 렌더링은 크게 보면Trigger, Render Phase Commit Phase으로 나누어져 있는데, 이 과정을 하나씩 살펴보자.

 

 

 

 

Trigger

 

이름 그대로 렌더링을 촉발시키는 것이다. 리액트는 기본적으로 렌더링이 두 가지 케이스로 나뉜다.

 

  1. 최초 렌더링
  2. 리렌더링

 

첫 번째로 최초 화면 진입 시에는 당연하게 화면에 그려야 할 결과물이 필요하다. 따라서 렌더링이 일어나게 된다.

최초 렌더링 이후에는 특정 상황이 발생하면 리렌더링을 수행한다. 해당 상황은 아래와 같다.

 

  1. props가 변경될 때
  2. state가 변경될 때
  3. 부모 컴포넌트에서 리렌더링이 일어나는 경우

 

첫 번째 상황 props가 변경되는 경우에는 부모 컴포넌트로 받는 props에 변화가 생겼다는 것으로 인지하여 하위 컴포넌트를 갱신해주어야 한다고 판단한다. 따라서 렌더링이 일어난다. 여담이지만 반복 렌더링을 통해 반환하는 컴포넌트는 성능 최적화를 위해 key prop을 부여하도록 되어있다. 이를 부여하지 않는다고 해서 그려지지 않는 것은 아니지만, 여러 데이터를 간단하게 그려주는 만큼 이에 대한 변화가 발생할 때에 더 빠르게 변화를 인지하기 위해 고유한 key prop을 부여하도록 되어있다.

동일한 형태를 가지는 sibling들 사이에서 반복 렌더링으로 그려낸 내용에 변경이 생긴다면 이를 더 빠르게 인지하기 위해 key prop을 사용한다. 따라서 이전 렌더링의 결과물에서 어떤 부분이 변경되었는지 비교하기 위해 동일한 key를 가진 컴포넌트를 찾고, 해당 컴포넌트와 이전에 동일한 key를 가지고 있던 컴포넌트를 비교하는 방식을 사용한다.

만약 key값에 랜덤 한 값을 넣어 매 렌더링마다 컴포넌트들이 다른 key값을 가지게 된다면 이는 이전 렌더링 결과와 비교할 수 있는 대상이 없기 때문에 새로운 컴포넌트로 간주하고 매번 새로 렌더링 하게 된다.

 

 

두 번째 상황 state의 변경은 useState의 두 번째 인자로 반환받는 setter가 실행되거나 useReducer로부터 반환받는 인자인 dispatch를 실행했을 때 React는 다시 렌더링을 수행해야 한다고 인지하여 위의 플로우를 실행하게 된다. 당연한 이야기이지만 해당 컴포넌트의 관점으로 볼 때, state가 변경되었다면 이를 다루는 내용이 변경되었다는 것이기 때문에 이 내용이 화면에 반영되어야 한다. 따라서 이때 React는 렌더링을 발생시킨다.

 

세 번째 상황 부모 컴포넌트에서 리렌더링이 일어나는 경우에는 당연하게도 하위 컴포넌트들은 리렌더링의 대상으로 간주되어 렌더링을 수행한다.

 

 

 

Render Phase

 

리액트의 렌더단계는 흔히 들어본 Reconciliation(재조정) 단계가 이루어지는 단계이다. 이 글은 React 18에서 Reconciliation에 대한 deep dive 글은 아니기에 해당 내용을 기술적으로 자세하게 다루지는 않겠다. 렌더단계에서는 변화가 생긴 부분들을 재귀적으로 탐색하여 다음에 그려내야 할 DOM Tree에 대한 정보를 정리한다. 이 과정은 재귀적이라는 단어에 때문에 오해가 생길 수 있지만 중단이 가능하다. 과거에는 한번 시작하면 중단이 불가능한 Stack Reconciler를 사용했지만, React 17에서부터는 Fiber Reconciler를 활용하여 재조정 단계를 수정한다. 여기서 Fiber 각 컴포넌트에 대한 정보를 담은 객체로 parent, sibling, child와 같은 정보나 해당 컴포넌트의 key, type 등 정보를 가지고 있어 이를 바탕으로 변경 사항을 체크한다. 이를 토대로 Root Fiber에서부터 child가 있는 컴포넌트를 참조 포인터 방식으로 조회하고 모든 child를 비교했다면 다시 돌아오면서 sibiling이 있는지 확인, 있다면 sibiling으로 이동하여 비교 및 해당 fiber에서도 child를 찾아 재귀적으로 비교하는 방식을 반복한다. 이렇게 모든 fiber를 순회하면 다시 root로 돌아오고 모든 변경사항을 정리한 것으로 해당 Phase를 마친다.

 

위와 같이 Fiber를 기준으로 순회하기 때문에 중간에 우선순위가 높은 작업이 발생하면 Reconciliation을 중단하고 마지막으로 작업을 수행하던 Fiber를 기록하여 해당 작업을 먼저 수행하면서도 수행 이후에는 이전에 진행하던 작업으로 돌아올 수 있는 것이다. 여기서 우선순위에 대한 내용은 React 18의 Lane Model을 참고해 보면 좋다.

 

 

출처 : https://www.velotio.com/engineering-blog/react-fiber-algorithm

 

 

 

Commit Phase

 

Commit Phase는 Render Phase에서 정리한 다음 렌더할 DOM에 대한 가상 DOM을 실제 적용하는 단계이다. 이전의 Render Phase 비동기로 동작했지만 Commit Phase는 실제 DOM에 반영되어야 하는 내용을 적용하는 과정이기 때문에 동기로 동작한다. 그렇기 때문에 이는 흔히 말하는 Double Buffering 방식을 사용한다. 

 

Double Buffering 방식은 현재 DOM에 그려져야 하는 Front-Virtual DOM Tree를 두고, 다음에 그려져야하는 내용을 작업하는 Back-Virtual DOM Tree를 두는 방식이다. 실제 DOM은 Front 가상 DOM Tree만을 참조하고 있기 때문에 다음 렌더링되어야 할 DOM Tree가 만들어지는 과정은 현재 DOM Tree에 영향을 주지 못하는 것이다.

 

추가적으로 반드시 렌더링이 일어난다고 해서 변화가 생기는 것은 아니다. 렌더링이 일어났지만 기존과 동일한 경우 해당 Render의 결과는 반영되지 않고 버려진다. 이 역시 Back-Virtual DOM Tree를 다루는 일이기 때문에 현재 보고 있는 화면에는 영향이 없다.

 

 

렌더링 과정에 따른 코드 수행은 위의 그림과 같다.

 

Browser Rendering vs React Rendering

 

 

이제 이전 글부터 이어져 온 두 가지 렌더링에 대한 비교를 이야기해보려고 한다.

 

브라우저 렌더링은 브라우저가 주체로 수행하는 렌더링 과정으로 실제 화면에 그려낼 DOM Tree를 만들고 스타일을 정리하는 과정을 거쳐 화면에 직접적으로 그려지는 내용을 적용하는 과정이고

 

리액트 렌더링은 브라우저 렌더링 과정에서 실행된 react script가 실행된 환경에서 리액트가 주체로 수행하는 렌더링 과정으로 변경이 발생하면 다음에 그려져야 하는 DOM Tree에 대한 내용을 정리하고 정리가 완료된 후에는 해당 내용대로 화면에 그려달라고 브라우저 렌더링을 거치도록 하는 것이다.

 

 

Why React Rendering?

 

그렇다면 왜 React를 통한 렌더링 과정을 거치게 되었을까?

 

결론부터 말하자면 브라우저의 DOM을 조작하는 것은 비용이 많이 들기 때문이다. 실제 DOM 조작은 이전 글에서 살펴보았다시피 여러 가지 과정을 거치고, 그 과정 속에서는 고비용의 작업들이 많다. (Layout, Paint 등등) 이러한 작업들이 고비용의 작업이기에 개발을 하다 보면 가끔 성능 이슈를 마주하고, 이를 해결하기 위해 reflow, repaint에 대한 최적화를 고려할 때가 있는 것이다.

 

이렇게 비싼 브라우저 DOM 조작을 보다 저렴한 비용으로 수행하기 위해 JS로 이루어진 가상 DOM을 사용하는 것이다. 이를 사용함으로써 얻는 이점은 아래와 같다.

 

  1. 변경 사항의 배치 처리
  2. 최소한의 DOM 업데이트
  3. 효율적인 변경 계산

 

가상 DOM을 사용하면 여러 가지 상태의 변경이 일어나는 것을 하나의 변경사항으로 묶어서 처리할 수 있다. 이를 통해 불필요한 reflow, repaint 과정을 줄여 성능을 향상시킬 수 있고, 가상 DOM을 통해 업데이트된 내용과 실제 DOM을 비교하여 최소한의 변경사항만을 실제 DOM에 적용하기 때문에 직접 DOM을 조작하는 것보다 성능을 향상시킬 수 있고, 내부적으로 Render Phase에서는 Diffing 알고리즘을 사용하여 변경 사항을 효율적으로 계산하기 때문에 애플리케이션의 반응성을 유지하면서도 성능을 개선할 수 있는 것이다.

 

(이 글에서 Diffing 알고리즘에 대한 내용은 다루지 않는다. 이 역시도 위에서 언급한 Fiber Reconciler의 우선순위를 다루는 Lane Model과 관련이 있기 때문에 관심이 있다면 이전에 작성한 React Deep Dive 글을 읽어보길 추천한다. https://sangminnn.tistory.com/79 )

 

 

결론적으로는 브라우저 렌더링이 리액트 렌더링과 비교하자면 더 코어한 작업이며 일반적으로 DOM에 대한 조작은 비용이 비싸기 때문에 이를 JS를 기반으로 한 가상의 DOM을 사용하는 리액트 렌더링은 이러한 DOM 조작을 최소한으로 적용하기 위해 스크립트로 이루어진 렌더링으로 결국 화면에 그리기 위해서는 리액트 렌더링을 통해 계산된 내용을 브라우저 렌더링에게 위임하여 실제 DOM에 적용하는 것이라고 보면 된다.

 

 

 

Re-cap

 

이전부터 작성해 오던 React Deep Dive 시리즈에 이어 브라우저 렌더링과 React의 렌더링을 정확하게 비교하기 위해 브라우저 렌더링도 깊게 파헤쳐보았고 이를 동일선상에 두고 직접 비교하면서 명확한 차이를 이해했다. 해당 시리즈를 통해 리액트를 사용하여 개발하면서 상황에 따라 다른 주체를 가진 것을 동일하게 렌더링이라 표현하는 상황을 마주하더라도 동작을 보고 정확하게 파악할 수 있기를 기대해 본다.

 

 

 

 

참고자료

 

모던 리액트 Deep Dive - https://product.kyobobook.co.kr/detail/S000210725203

React 공식 문서 - https://react-ko.dev/learn/render-and-commit

https://pozafly.github.io/react/declarative-meaning-of-react-rendering-process/