React.js에서의 최적화에 대한 부분은 이전 포스트에서 다룬 적이 있지만, 이때는 useMemo와 useCallback, 메모이제이션 사용의 주의점 등이 포함된 아주 간략한 글이었다. 이번 포스트에서는 더 상세한 내용의 최적화를 다루어보려 한다.
렌더링은 화면에 요소를 그려내는 과정을 말합니다. 웹 브라우저에서의 렌더링은 HTML, CSS, JavaScript 파일을 받아 이를 처리하고 화면에 그리는 과정을 포함합니다. 이때 웹 브라우저는 HTML과 CSS를 파싱하여 각각 DOM 트리와 CSSOM 트리를 생성하게 됩니다. 이 두 트리는 결합되어 렌더 트리를 형성하며, 이 렌더 트리를 기반으로 브라우저 화면에 그려집니다.
기본적인 JavaScript를 사용하여 DOM을 직접 조작하는 것은 성능상의 문제와 코드의 복잡성으로 인해 비효율적입니다. 이러한 문제점을 해결하기 위한 대안으로 리액트 같은 라이브러리나 프레임워크가 등장하게 되었습니다. 이들은 선언형 접근 방식을 통해 개발자가 UI 설계에만 집중하도록 도와줍니다.
리액트에서 UI와 상태를 연동하기 위해서는 주로 state나 props를 사용합니다. 특히 state의 변화가 발생하면, 해당 컴포넌트와 그 하위 컴포넌트들은 재렌더링되게 됩니다. 이때 중요한 것은 상태 값이 변경되었더라도 결과적으로 UI에 변화가 없다면 리렌더링을 최소화하여 성능을 최적화하는 것입니다.
리액트의 렌더링 최적화는 브라우저의 CRP(Critical Rendering Path) 과정을 효율적으로 관리하기 위한 방안입니다. CRP는 웹 페이지의 렌더링을 위해 HTML을 DOM으로, CSS를 CSSOM으로 변환한 후 Render Tree를 만들어 화면에 그리는 과정을 포함합니다. 특히, Layout과 Paint는 렌더링에서 많은 연산을 요구하는 단계입니다.
빈번한 DOM 조작은 CRP를 자주 발생시켜 성능 저하를 가져올 수 있습니다. 리액트는 이 문제를 해결하기 위해 VirtualDOM을 도입했습니다. UI 변화가 일어날 때, 리액트는 실제 DOM을 바로 수정하지 않고 VirtualDOM을 사용하여 변화를 먼저 반영합니다. 그 후, 이전의 VirtualDOM과 새로운 VirtualDOM을 비교하여 실제 변경된 부분만 DOM에 적용합니다. 이로써 CRP의 빈도가 줄어들며, 이것이 리액트의 렌더링 최적화 핵심입니다.
리액트의 렌더링 과정은 Virtual DOM을 기반으로 최적화가 이루어집니다. Virtual DOM은 실제 DOM의 가벼운 복사본으로, 변경사항을 실제 DOM에 직접 반영하기 전에 이를 Virtual DOM에서 먼저 수행하고, 필요한 부분만 실제 DOM에 반영하여 성능을 향상시킵니다. 이 과정은 다음과 같은 단계로 이루어집니다:
리액트는 상태나 props의 변화에 따라 관련 컴포넌트를 리렌더링합니다. 그럼에도 불구하고 실제로 변화가 없는 컴포넌트를 불필요하게 리렌더링하는 것은 비효율적입니다.
React.memo는 이러한 불필요한 리렌더링을 줄이기 위해 제공되는 고차 컴포넌트입니다. 이는 props의 얕은 비교를 통해 리렌더링 여부를 결정합니다. 하지만 이 기법은 참조 타입의 값에는 한계가 있으므로 주의하여 사용해야 합니다.
메모이제이션은 오랜 시간이 걸리는 연산의 결과를 재사용함으로써 연산 시간을 줄이는 최적화 기법입니다. 이는 리액트에서도 중요한 개념으로, 불필요한 연산이나 렌더링을 방지하기 위해 사용됩니다.
useMemo: 값을 메모이제이션하며, 의존성 배열 내의 값에 변화가 있을 때만 재연산을 수행합니다.
useCallback: 함수를 메모이제이션하며, 주로 불필요한 리렌더링을 방지하기 위해 사용됩니다.
메모이제이션은 항상 유용한 것은 아닙니다. 복잡한 연산이나 큰 데이터셋에서 주로 유용하나, 그렇지 않은 경우에는 메모이제이션의 오버헤드가 오히려 성능을 저하시킬 수 있습니다. 따라서, 성능 문제가 발생했을 때 프로파일링을 통해 실제 병목 구간을 파악한 후, 필요한 부분에만 메모이제이션을 적용하는 것이 바람직합니다.