본문 바로가기
2024/프로젝트

[Mudig] 불필요한 리렌더링 방지로 성능 최적화 하는 방법

by ye-jji 2024. 3. 19.

Intro

React 애플리케이션의 성능을 최적화하는 데 중요한 포인트로 리렌더링이 과도하게 발생하면, 앱의 반응성이 저하될 수 있다는 것을 고려하는 것이다. 실제로 리렌더링에 대한 부분에 리팩토링을 진행하면서 Light House 검사를 통해 성능이 개선되는 것을 확인했다.

 

함수 컴포넌트와 클래스 컴포넌트 모두 불필요한 리렌더링을 방지할 수 있는 방법이 있으며, 각각의 접근 방식이 조금 다르다. 프로젝트에서는 함수형 컴포넌트로 구현했지만 공부하는 의미로 함수 컴포넌트와 클래스 컴포넌트 각각을 정리해 보았다.

 

함수 컴포넌트의 불필요한 리렌더링 방지

  • React.memo: 컴포넌트를 감싸주면, props의 변화가 없다면 리렌더링을 방지하여 성능을 최적화할 수 있다.
const MyComponent = React.memo(function MyComponent(props) {
  // 컴포넌트 구현
});
  • useMemo: 계산 비용이 높은 함수의 결과값을 메모리에 저장해서 의존성이 변경되지 않았다면 재계산하지 않고 이전 결과를 재사용할 수 있다.
  • useCallback: 함수를 메모리에 저장하여, 의존성이 변경되지 않는 한 동일한 함수 인스턴스를 재사용할 수 있다. 자식 컴포넌트에 props로 함수를 전달할 때 특히 유용하다.
//두 가지를 같이 사용한 예시
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

 

클래스 컴포넌트의 불필요한 리렌더링 방지

  • PureComponent: shouldComponentUpdate를 자동으로 구현하여 shallow comparison을 통해 props와 state의 변화를 체크하고, 필요한 경우에만 리렌더링을 수행한다.
class MyComponent extends React.PureComponent {
  render() {
    // 컴포넌트 구현
  }
}
  • shouldComponentUpdate: 개발자가 직접 리렌더링의 조건을 정의할 수 있다. 이 메소드에서 false를 반환하면, 해당 조건에서는 컴포넌트의 업데이트를 방지할 수 있다.
class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 업데이트 조건 로직
    return true; // 또는 false
  }
}

 

프로젝트 실제 예시

 //리팩 전 코드
 useEffect(() => { 
   const fetchRandomMv = async () => { 
     const data = { selectId, page }; 
     // console.log(data); 
     getRandomMv(data, { 
       onSuccess: (newVideoData: IVideoData[]) => { 
         if (newVideoData.length === 0) { 
           setIsEnd(true); 
           return; 
         } 
         //받아온 뮤비들의 id값 갱신하여 [id, setId] 에 저장 
         const dataId = newVideoData.map((video) => video.id); 
         setId((prevId) => [...prevId, ...dataId]); 
         setPage((prevPage) => prevPage + 1); 
         //기존 비디오랑 새로 받아오는 비디오 
         setAllVideos((prevVideos) => [...prevVideos, ...newVideoData]); 
       }, 
       onError: (error) => { 
         console.error('랜덤뮤비 불러오기 실패', error); 
       }, 
     }); 
   };

 

이 코드는 fetchRandomMv함수를 useEffect 안에서 선언했지만 의존성 배열에는 page 밖에 없기 때문에 page 요소에 변화가 있다면 리렌더링 된다. 따라서 fetchRandomMv함수가 변화가 없어도 리렌더링 될때마다 함수가 재정의 되고 있다는 것이다. 성능 최적화를 위해 함수를 외부에서 정의하고 변경사항이 있을 때 리렌더링 되도록 하는것이 필요하다. 위의 방법 중 useCallback을 사용해서 메모이제이션하고 의존성을 추가하면 의존성 값에 변화가 없을 때 같은 함수 인스턴스를 재사용할 수 있어서 불필요한 리렌더링을 줄일 수 있다. 아래는 수정한 코드이다.

  // useCallback을 사용하여 fetchRandomMv 함수를 메모이제이션
  const fetchRandomMv = useCallback(async () => {
    const data = { selectId, page };
    getRandomMv(data, {
      onSuccess: (newVideoData: IVideoData[]) => {
        if (newVideoData.length === 0) {
          setIsEnd(true);
          return;
        }
        const dataId = newVideoData.map((video) => video.id);
        setId((prevId) => [...prevId, ...dataId]);
        setPage((prevPage) => prevPage + 1);
        setAllVideos((prevVideos) => [...prevVideos, ...newVideoData]);
      },
      onError: (error) => {
        console.error('랜덤뮤비 불러오기 실패', error);
      },
    });
  }, [getRandomMv, selectId, page]); // 의존성 추가

  useEffect(() => {
    const observerCallback = ([entry]) => {
      if (entry.isIntersecting) {
        fetchRandomMv();
      }
    };

 

 

결론

프로젝트를 진행하는 중에는 기능 구현이 우선이라 일단 동작하고 문제가 없다면 그 코드를 다시 보는게 쉽지 않다. 그리고 계속 이런 저런 내용을 추가하다보면 결국 처음에 클린했다고 생각한 코드가 어느새 나만 알아볼 수 있는 에일리언 코드가 되어 있기 쉽다. 이번 리팩토링을 진행하면서 그런 점들을 많이 느꼈다. 나는 알아볼 수 있거나 이유가 있었던 코드였지만 히스토리나 설명을 정확하게 주석으로 달아 두거나 하지 않으면 이걸 왜 이렇게 했는지 이해하기 어렵거나 최적화가 필요한 코드가 많았다. 그리고 다른 사람이 작성한 코드를 읽고 이해한 뒤 최적화나 클린코드 측면에서 개선점을 이야기 하다보면 배우는 것들이 많다. 코드리뷰를 프로젝트 팀원끼리 진행하는 것이 생각보다 더 좋은 공부 방식이라는 생각이 들었다.