Web-Frontend/React.js

[React.js] 렌더링 최적화 - useMemo, React.memo

서노리 2023. 1. 20. 19:39
반응형

Re-Rendering

리렌더링은 렌더링이 다시 일어나는 경우를 말하며 리액트에서 리렌더링은 다음과 같은 상황에 일어난다.

< Re-Rendering이 되는 경우 >
1. Props가 변경
2. State가 변경
3. 부모 컴포넌트가 re-render
4. Context value가 변경되었을 때

 

규모가 큰 프로젝트에서 불필요한 리렌더링이 자주 일어나면 성능 저하 문제를 발생시키기 때문에 최적화라는 작업이 필요하다. 즉, 최적화는 위 4가지 상황이 일어나는 횟수를 줄이는 작업이며 먼저 부모 컴포넌트의 리렌더링 횟수를 줄이는 것을 기본으로 하며, 그리고 주로 props의 변경을 줄이는 방법을 사용한다. 

 

최적화가 필요한 경우 예제

const getDiaryAnalysis = () => {
    console.log("일기 분석 시작");
    const goodCount = data.filter((it) => it.emotion >= 3).length;
    const badCount = data.length - goodCount;
    const goodRatio = (goodCount / data.length) * 100;
    return { goodCount, badCount, goodRatio };
  };

  const { goodCount, badCount, goodRatio } = getDiaryAnalysis();

  return (
    <div className="App">
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 개수: {data.length}개 </div>
      <div>기분 좋은 일기 개수: {goodCount}개 </div>
      <div>기분 나쁜 일기 개수: {badCount}개 </div>
      <div>기분 좋은 일기 개수 비율: {goodRatio}% </div>
      <DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
    </div>
  );
};

getDiaryAnalysis 함수는 일기의 감정 점수를 바탕으로 기분 좋은 일기 개수, 기분 나쁜 일기 개수, 기분 좋은 일기 개수의 비율을 객체로 리턴하는 함수이다. 

※ 함수가 두 번 실행된 이유

  • App.js 컴포넌트가 처음 마운트될 때 data는 빈 배열 상태이며 이 때 getDiaryAnalysis 함수가 호출됨
  • getData 함수를 통해 API를 불러와 data가 바뀌게 되면서 리렌더링이 일어나고, 이 때 getDiaryAnalysis 함수가 한 번 더 호출됨
  • 즉, 리렌더링이 일어날 때마다 getDiaryAnalysis가 호출될 것이므로 일기의 개수에 영향을 주지 않는 부분이 업데이트 되어도 getDiaryAnalysis가 불필요하게 호출되며 이를 최적화할 필요가 있음

useMemo

useMemo는 리액트의 렌더링 성능 최적화를 위한 Hook이다. 기본적으로 Memoization 기법을 사용하는데, Memoization이란 이미 수행된 연산에 대한 결과를 기억해두었다가 동일한 연산이 다시 수행될 경우 그 값을 반환하는 방법이다. 

위의 코드를 useMemo를 사용하여 최적화한 코드는 다음과 같다.

const getDiaryAnalysis = useMemo(() => {
    console.log("일기 분석 시작");
    const goodCount = data.filter((it) => it.emotion >= 3).length;
    const badCount = data.length - goodCount;
    const goodRatio = (goodCount / data.length) * 100;
    return { goodCount, badCount, goodRatio };
  }, [data.length]);
  
  const { goodCount, badCount, goodRatio } = getDiaryAnalysis;

useMemo의 두 번째 인자인 의존성 배열에 data.length를 넣어줌으로써, data.length가 변할 경우에만 해당 콜백함수를 수행하도록 할 수 있다. 

 

※ useMemo 사용 시 주의할 점

useMemo를 사용하면 해당 함수는 더 이상 함수 역할을 하지 않고 값의 역할을 하게 된다. 따라서 마지막 부분에서 getDiaryAnalysis(); 가 아닌 getDiaryAnalysis만 적어줘야한다.


React.memo

  • React.memo는 고차 컴포넌트(High Order Component)
  • 고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수
  • React.memo는 props가 변화하지 않으면 반환되지 않는 강화된 컴포넌트를 반환함
  • props 혹은 props의 객체를 비교할 때 얕은 비교를 한다.
// 방법 1
const MyComponent = React.memo(({props}) => {
  /* props를 사용하여 렌더링 */
});

// 방법 2
export default React.memo(OptimizeTest);

 

※ useMemo와의 차이점

  • useMemo는 React Hooks인 반면, React.memo는 HOC(고차 컴포넌트)
  • React Hooks인 useMemo는 컴포넌트 내부에서만 사용이 가능하다는 차이점이 있음.

 

React.memo 사용 예제

위와 같은 컴포넌트 구조를 갖는 프로젝트에서 부모 컴포넌트인 App은 count와 text 두 가지 state를 가지고 이를 각각 CountView와 TextView의 prop으로 전달하고 있다. 만약 setCount()이 실행되면 count state의 값이 변해 App 컴포넌트가 리렌더링 되기때문에 자식 컴포넌트인 CountView와 TextView 또한 리렌더링된다. 하지만 TextView의 경우 count state와 아무 관련이 없는 컴포넌트이기 때문에 리렌더링 될 필요가 없다. 반대로 setText()가 실행되는 경우에도 CountView는 리렌더링 될 필요가 없다. 이런 경우 React.memo를 통해 리렌더되지 않도록 해주어야한다.

 

import React, { useEffect, useState } from "react";

const TextView = React.memo(({ text }) => {
  useEffect(() => {
    console.log(`Update :: Text : ${text}`);
  });
  return <div>{text} </div>;
});

const CountView = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`Update :: Count : ${count}`);
  });
  return <div>{count}</div>;
});

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count</h2>
        <CountView count={count} />
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>

      <h2>text</h2>
      <TextView text={text} />
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
};

export default OptimizeTest;

 


 

반응형