프론트엔드 개발에서 중요한 요소 중 하나는 성능이다. 상태를 "효율적"으로, "불필요한" 작업이 발생하지 않도록 신경써야하며, CSS 애니메이션 작업 시 width, top 대신 transform을 사용하는 등 최적화가 필요하다. 이런 여러 가지 고려사항을 깊게 파고들다 보면, 결국 렌더링의 문제에 도달하게 된다.
상태를 효율적으로 관리하고, CSS를 통해 최적화된 애니메이션을 구현하려면, 브라우저가 렌더링을 어떻게 처리하는지 이해하는 것이 필수적이다. 단순히 문제를 해결하는 것을 넘어서, 그 문제가 발생하는 원인을 정확히 알아야 진정한 해결책을 마련할 수 있기 때문이다.
그래서 우선 렌더링에 대한 깊은 이해를 바탕으로, 상태 관리와 CSS 최적화에 대해 다시 한번 살펴보려고 한다. 브라우저의 렌더링 과정이 무엇인지 정확히 이해하면, 성능을 최적화해야 하는 이유와 방법이 더욱 명확해질 것이다.
렌더링 과정
렌더링 처리는 우리가 브라우저를 통해 웹 사이트를 열면 시작이 된다. 이 처리 과정은 페이지의 코드를 화면에 시각적으로 표시하기 위해 작동하는 여러 단계로 구성되어 있다.
- Parsing HTML
- Parsing CSS
- Constructing the Rendering Tree
- Layout
- Painting
- Compositing
사용하는 브라우저에 따라 약간씩 다를 수 있지만 기본적인 단계는 동일하게 작동한다.
1. Parsing HTML
파싱은 코드의 구조와 의미를 파악하기 위해서 코드를 분석하는 것이다.
HTML에서 파싱은 HTML 코드를분석하여 페이지의 콘텐츠와 요소를 구조적으로 표현하는 작업이 포함된다.
브라우저에서 HTML 코드 상단에서 시작해서 각 줄을 읽어내며, HTML 태그, 속성 및 콘텐츠를 찾는다.
코드를 읽으면서 Document Object Model(DOM)이라는 트리 형식의 구조를 구축한다.
2. Parsing CSS
다음 단계로 브라우저는 페이지와 연결된 CSS 코드를 가지고 와서 CSS Object Model(CSSOM)이라고 불리는 트리 구조를 만든다. CSSOM은 CSS 코드에 정의되어 있는 모든 스타일 규칙을 포함하고 있다.
3. Constructing the Rendering Tree
브라우저가 DOM과 CSSOM을 만들고, 둘을 결합한 렌더링 트리를 생성한다. 렌더링 트리는 웹 페이지에서 컨텐츠와 스타일이 계층적으로 표현되어 있다.
렌더링 트리를 구성하기 위해서 브라우저는 DOM 트리의 각 요소를 CSSOM 트리의 해당 스타일 규칙과 일치시킨다. 그 후 각 요소에 스타일을 적용하여 페이지에서 각 요소의 최종 레이아웃과 위치를 계산한다. 그 결과 렌더링 트리라는 계층 구조가 생성된다. 이때, DOM의 모든 요소가 렌더링 트리에 포함되는 것은 아니다. 예를 들어, <head>, <script>와 같이 화면에 표시되지 않는 요소나 display:none으로 설정된 요소는 렌더링 트리에서 제외된다.
4. Layout ( Reflow )
브라우저가 렌더링 트리에서 요소의 크기와 위치를 계산하는 단계이다. 이때 CSS 코드에 정의된 스타일과 DOM의 콘텐츠를 기반으로 한다. 레이아웃 단계는 화면에서 요소의 위치를 결정하고 페이지가 올바르게 표시되는지 확인하는 중요한 단계이다.
5. Painting
레이아웃 계산이 끝나면 요소의 색상을 채우고 화면에 표시할 페이지의 이미지를 만드는 작업을 진행한다. 브라우저는 스타일 및 레이아웃 정보를 사용하여 렌더링 트리의 각 요소를 나타낸다.
6. Compositing
마지막으로 Painting 단계의 요소들을 최종 이미지로 결합한다. 이 이미지들은 화면에 표시되면서 렌더링 프로세스가 완료된다.
React Re-Render & Re-Evaluate
렌더링 과정에 대해서 알아봤다. React에서 State 값이 변경됐을 때 리렌더링이 발생하는 과정에 어떤 작업들이 일어나는지 알아보자. 우리는 상태가 변경됐을 때 컴포넌트가 Re-rendering 됐다고 한다. 하지만 그게 정말 HTML의 Painting 또는 Layout 작업을 다시 수행하는 것을 이야기할까?
우리의 React는 Context, Props, State 같은 요소에 관심을 가지고 있다. 그래서 Context, State, Props 등 값이 변경되면 함수(컴포넌트)가 다시 실행(Re-executed)되고 해당 컴포넌트는 React에서 위에서부터 아래로 다시 평가(Re-evaluation)된다.
재평가(Re-evaluation)로 업데이트된 정보가 ReactDOM으로 전달되면, ReactDOM은 새로운 변경 사항을 이전 복제된 데이터와 가장 비교를 하고 만약 변경사항이 발견되면 특정 실제 DOM 요소 또는 DOM의 값을 새 요소 또는 값으로 다시 렌더링(Re-render)을 진행한다.
React 렌더링 전 과정을 다시 한번 정리해보자면, 다음과 같다.
React App을 처음 실행할 때마다 함수 컴포넌트를 실행하고 현재 State, Props, Context등을 평가하고 React DOM에 전달하여 실제 DOM을 통해 화면에 표시한다. 정보가 React DOM에 전달되면 실제 DOM과 차이를 비교하는데, 현재 모든 정보가 새롭기 때문에 DOM에 추가하고 UI를 렌더링 한다.
이제 홈페이지를 이용하다가 [버튼 클릭, 호버, ...]같은 특정 행동을 통해서 컴포넌트의 props나 context 또는 state 값이 변경되었다면 함수 컴포넌트는 재실행(Re-executed)되고 재평가(Re-evaluation)이 발생할 것이다. 위 이미지에서 2단계와 3단계가 실행될 것이며, 이후 정보는 React DOM으로 전달되어 기존 요소와 변경사항이 있는지 확인하는 작업을 수행합니다. 변경사항이 있다면 해당 변경 내용을 Real DOM에게 전달하여 다시 렌더링(Re-rendering)을 수행합니다.
정리하면 Re Evaluation, 재평가가 발생하더라도 반드시 Re Rendering이 발생하는 것은 아니다.
코드 로직과 실행 흐름을 이해하기 위해서 위 코드를 요약해보자.
4개의 함수 컴포넌트로 구성되어 있는데, App, Context, Children1, Children2로 구성되어 있다.
App 컴포넌트는 단순하게 Context 기능을 사용하기 위한 환경과 Children1 컴포넌트를 렌더링하고 있다. Context 컴포넌트는 state라는 이름의 상태를 가지고 Provider를 통해서 값을 공유하고 있으며, Children1 컴포넌트는 별다른 기능 없이 Children2 컴포넌트를 렌더링하는 역할을 한다. Children2 컴포넌트는 Context를 통해 전달받은 state를 화면에 렌더링해주며, 버튼을 클릭하면 state 값을 1씩 증가시켜주고 있다.
React Dev Tool을 통해서 컴포넌트가 업데이트가 될 때마다 표시를 했을 때 Children2만 아닌 Children1도 업데이트가 되고 있는 것처럼 보이는 것을 확인할 수 있다. 실제로 업데이트는 되고 있는 것이 맞다. 하지만 Children1은 Re Evaluation까지만 발생하는 것이고 Re Rendering은 발생하지 않는다. 그 이유는 state값이 변경되었을 때 DOM을 실제로 변경하는 요소가 없기 때문이다.
하지만 Children2는 값이 변경되면 state를 렌더링하고 있기 때문에 Re Evaluation까지만 되는 것이 아닌 Re Rendering까지 발생해서 DOM이 변경된다.
이처럼 React에서의 state, context, props 등으로 인한 "리렌더링"은 화면을 다시 그리는 Re Rendering이 될 수 있지만 변경 사항이 있는지 확인하는 Re Evaluation까지만 실행될 수 있다는 점을 유의해야 한다.
그렇다고 Re Evaluation을 남발해도 된다는 것은 아니다. 당연히 성능에 영향을 주고 있지만 우리가 생각하는 Re Rendering과는 다르다는 것을 알아야 한다.
CSS 변환 속성
애니메이션 작업을 할 때 width, top, left 같은 속성을 변경하는 것보다 scale, translate 같은 변환 함수를 사용하는 것이 성능상으로 더 좋다. 그 이유는 width, top, left 같은 속성을 사용하면 레이아웃이 재계산(Layout)을 통해서 많은 리소스를 소모한다.
반대로 scale, translate과 같은 변환 함수를 사용하면 시각적인 표현만 변경되고, 실제 DOM 레이아웃은 유지됩니다.
즉, HTML의 렌더링 과정에서 Layout 단계를 건너뛰고 Painting 단계만 처리하여 바로 렌더링이 발생합니다.
성능상으로 Layout을 다시 계산하는 것은 많은 리소스를 소모하기 때문에 변환 속성을 사용하여 애니메이션을 만드는 것이 좋습니다. 또한 변환 함수는 GPU 가속을 활용할 수 있어 애니메이션을 효율적으로 처리할 수 있어서 부드러운 애니메이션을 구현할 수 있습니다.
위 이미지를 확인해보면 CSS 애니메이션을 두 가지 방식의 성능 비교를 하고 있다.
상단에 보이는 애니메이션은 DOM 레이아웃을 재계산하는 방식으로, CPU 사용량이 급격히 증가하지만 하단 transform을 사용하는 애니메이션은 성능 저하가 없는 것을 확인할 수 있다.
이번 글을 통해서 HTML의 렌더링 과정과 React의 렌더링 메커니즘, 그리고 CSS 변환 속성을 활용한 성능 최적화의 중요성을 알아보았다. 웹 애플리케이션의 성능은 사용자 경험에 직접적인 영향을 주기 때문에, 이러한 이해는 필수적이며, 앞으로 반응성이 뛰어난 웹 애플리케이션을 구축하는 데 큰 도움이 될 것이다.
'React > 실험실' 카테고리의 다른 글
디바운싱 검색 (1) | 2024.10.19 |
---|---|
CSS를 컴포넌트에 중복 호출하면 안되는 EU (1) | 2024.08.17 |
React Query 고려하기 - Request Waterfalls (1) | 2024.08.07 |
Table 컴포넌트 (0) | 2024.07.31 |
useFunnel 만들기 (2) | 2024.06.16 |