SW 엘리스 2차 프로젝트에서 코드를 개선했던 점을 작성하게 되었다.
- 컴포넌트가 받는 Props가 변경되었을때
- 사용하는 State가 변경되었을때
- Context Value가 변경되었을때
- 부모 컴포넌트가 리렌더링 되었을때
리액트에서의 렌더링 최적화는 불필요한 리렌더링을 줄이는 방향으로 개선한다.
부모컴포넌트가 리렌더링되었을때, 자식 컴포넌트가 prop으로 함수나 값을 받고있는 상황이라고 가정하자.
여기서 prop값 자체가 변경되지 않았더라도 부모 컴포넌트가 리렌더링된다면
전달하는 값이나 함수도 다시 렌더링이 되기 때문에 자식 컴포넌트 또한 불필요하게 렌더링 되는 상황이 발생한다.
이를 방지하기 위해 React에서는 React.memo라는 고차컴포넌트와
useCallback, useMemo와 같은 hook을 이용해 불필요한 렌더링을 막는다.
const Comment = React.memo(function MyComponent(props) { /* prop이 변했을때만 Re-rendering을 실시함*/ });
export default React.memo(Comment); ``` //이런 형태로도 사용.
React.memo는 컴포넌트 로직을 재사용하기 위한 고차컴포넌트이다.
React.memo를 통해 컴포넌트를 래핑하게 되면, 전달받는 prop이 변할때에만 컴포넌트를 리렌더링한다.
만약 전달되는 prop으로 동일한 결과를 렌더링한다면, 컴포넌트를 리렌더링 하지 않고 메모이제이션 된 값을 사용한다
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], ); const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const FoodDetailProfiler = () => { return ( <> <Profiler id="FoodDetail" onRender={(id, phase, actualTime, baseTime, startTime, commitTime, interactions) => console.table({ id, phase, actualTime, baseTime, startTime, commitTime, interactions }) }> <FoodDetail /> </Profiler> </> ); }; export default FoodDetailProfiler;
렌더링속도를 측정해보기위해 Profiler API를 각 컴포넌트에 감싸는 형태와 개발자도구의 Profiler Tab을 활용하였다.
Render함수의 파라미터들의 의미는 각각 다음과 같다.
function onRenderCallback( id, // 방금 커밋 한 프로파일 러 트리의 "id"소품 phase, // "mount"(트리가 방금 마운트 된 경우) 또는 "update"(다시 렌더링 된 경우) actualDuration, // 커밋 된 업데이트를 렌더링하는 데 소요 된 시간 baseDuration, // 메모없이 전체 하위 트리를 렌더링하는 데 걸리는 예상 시간 startTime, // React가 이 업데이트를 렌더링하기 시작했을 때 commitTime, // React가 이 업데이트를 커밋했을 때 interactions //이 업데이트에 속하는 상호 작용 집합 )```
여기서 actualDuration과 baseDuration 을 비교해 메모이제이션으로 얼마만큼의 시간을 절약하는지 알 수 있다.
actualDuration: memo, shouldComponentUpdate, useMemo 등과 같이 사용 중인 모든 최적화를 포함하여 구성 요소가 렌더링하는 데 걸린 실제 측정 시간이다.
baseDuration: baseDuration은 성능 개선 사항(메모이제이션)이 제거된 구성 요소를 렌더링하는 데 걸린 시간이다. 이 값은 메모이제이션을 사용하여 얼마나 많은 시간을 절약하고 있는지 알 수 있다.
총 리렌더링 시간은 14.2ms가 걸렸다. 최적화 작업을 진행하지 않아서 부모 컴포넌트가 리렌더링되어서 자식 컴포넌트가 리렌더링 된 경우나 결과는 같지만, prop이 변경되었기 때문에 불필요하게 리렌더링 된 컴포넌트들이 존재한다.
여기서 막대의 색은 현재 commit에서 컴포넌트를 렌더링하는데 얼마만큼의 시간이 걸렸는지 나타낸다.
노란색 막대 : 상대적으로 더 많은 시간이 걸림
파란색 막대 : 상대적으로 더 적은 시간이 걸림
회색 막대 : 현재 commit에서 렌더링하지 않음
console에 찍힌 결과도 확인해보았다.
메모이제이션을 안한 상태이므로, 댓글 하나를 삭제했을때 불필요하게 다른 컴포넌트들 또한 전부 re-rendering되는 상황이 발생한다. baseDuration과 actualDuration의 차이가 거의 없었다.
이제 React.memo, useCallback, useMemo를 통해 최적화 작업을 진행해보았다.
동일하게 댓글을 하나 삭제한 후 컴포넌트를 프로파일링 해보았다.
총 렌더링 시간은 1.9ms로 최적화를 진행하지 않았을 때의 시간 14.2ms보다 약 86% 빨라졌다.
CommentList는 댓글을 담고있는 배열이므로, 댓글이 삭제되는 로직이 진행되었을때, FoodDetail 컴포넌트가 리렌더링 되면서, CommentList 컴포넌트만 리렌더링 되야한다.
Profiler 컴포넌트를 통해 정밀한 시간을 측정해보았다.
actualTime값이 0인것을 보아, 메모이제이션 된 값을 사용해 리렌더링이 되지 않았음을 알 수 있다.
ref) 공식문서참조