프로젝트를 진행하면서 불필요한 렌더링이 너무 많이 일어나는 것을 알게 되었다.
예를 들어, 폼에 값이 입력될 때마다 다른 컴포넌트가 렌더링된다거나, 헤더와 메뉴 탭도 불필요하게 계속 렌더링되는 문제가 발생했다.
리액트의 성능을 최적화할 수 있는 방법에는 여러가지가 있지만, 먼저 리렌더링이 발생하는 과정부터 이해한 후, 이를 어떻게 적용할지 고민해볼 필요가 있다.
React 는 컴포넌트의 state 나 부모로부터 받은 props 가 변경되는지 감시한다.
변경이 감지되면, React 는 "새로운 가상 DOM"을 만들어서 기존의 DOM과 비교하는데, 이 때 변경된 부분만 실제 DOM에 업데이트하려는 과정이 일어난다. 이를 Reconciliation(조화단계)
이라고 한다. (공식문서)
렌더링이 반복될수록 Virtual DOM 연산이 많아질 것이며, 이는 최적화되지 않은 DOM 업데이트가 많아지면서 브라우저 성능에도 영향을 줄 수 있다.
불필요한 연산 증가
컴포넌트에서 필터링, 정렬, 데이터 변환, 등등 이런 연산을 수행하는데 리렌더링될 때마다 이런 연산이 다시 일어난다. 즉, 같은 결과를 매번 계산하는 비효율이 발생한다.
자식 컴포넌트도 함께 렌더링
자식 컴포넌트의 변화가 없더라도 부모 컴포넌트가 리렌더링되면, 함께 렌더링될 수 있다.
특히, props로 객체, 배열, 함수가 전달되면 새로운 값으로 인식되어 리렌더링된다.
가상 DOM 비교 비용이 증가
Virtual DOM 비교(diffing) 과정에서 변경된 부분을 감지하기 위한 비용이 증가하여 성능이 저하될 수 있다.
이러한 리렌더링을 언제, 어떻게 피할 수 있을지 자세하게 알아보자.
붎필요한 연산을 방지하고 싶을 때
메모이제이션
, 최초로 한번 계산했을 때 결과값을 메모리 어딘가에 보관하고, 이 연산이 필요해지면 저장되어 있는 값을 돌려주는 방법이다.const expensiveFunction = (num: number) => {
console.log("비용이 큰 연산...");
return num * 2;
};
const MyComponent = ({ number }: { number: number }) => {
const memoizedValue = useMemo(() => expensiveFunction(number), [number]);
return <div>{memoizedValue}</div>;
};
불필요한 함수 생성을 방지하고 싶을 때,
자식 컴포넌트에게 props로 함수를 넘길 때,
useEffect
의 종속성 배열에서 함수가 계속 바뀌는 것을 막고 싶을 때
[]
)이 바뀔 때만 새로운 함수를 만든다.// 함수 생성 방지
const MyComponent = () => {
const handleClick = useCallback(() => {
console.log("Button clicked!");
}, []);
return <button onClick={handleClick}>Click Me</button>;
};
부모 컴포넌트가 리렌더링될시, 자식 컴포넌트도 함께 렌더링되는 것을 막고 싶을 때
고차 컴포넌트(Higher Order Component, HOC)
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child />
</div>
);
};
// React.memo 사용
const Child = React.memo(() => {
console.log("🔄 Child Re-render");
return <div>I'm a child component</div>;
});
렌더링될 때마다 초기화되는 것을 막고 싶을 때,
DOM 요소에 직접 접근해야 할 때,
const MyComponent = () => {
const [count, setCount] = useState(0);
const renderCount = useRef(0);
renderCount.current++; // 값이 유지됨
return (
<div>
<p>Render Count: {renderCount.current}</p>
<button onClick={() => setCount(count + 1)}>Click</button>
</div>
);
};
위의 함수를 적절히 사용하는 것도 중요하지만, 컴포넌트 구조 자체를 최적화하는 과정도 중요하다.
상태를 최소한의 컴포넌트에서만 관리하기
Context API
사용하기
컴포넌트를 memo
+ useCallback
으로 최적화하기
리스트의 key를 최적화하여 Virtual DOM 비교 비용 줄이기
렌더링에 대해 다시 정리하면서, 단지 리렌더링을 방지하는 방법만 알기 보다 virtual DOM과 조화 단계가 뭔지 이해하는 것이 중요한걸 알게 되었다.
추후에는 프로젝트에서 직접 적용해보며 어떻게 개선되는지 정리해봐야겠다.
최적화 할 수 있는 방법에 이런 방법도 있군요!