useCallback & React.memo 활용 예제

하태현·2022년 7월 27일
0

React

목록 보기
11/11
post-thumbnail

이 글의 목적은 리렌더링 최적화이다.
사실 최적화의 문제라기 보단 당연한 것인데,
react의 원리를 잘 파악하지 못하거나 신경쓰지 않았다면 놓치기 쉬운 부분이다.

Code

예제 코드는 간단하게 여러장의 카드 중에서 한장을 선택하면 배경색이 바뀌는 로직이다.

// App.js
export default function App() {
  const [selected, setSelected] = useState(null);
  
  return (
    <div className="grid">
      {CARDS.map((c) => (
        <Card
          key={c.id}
          id={c.id}
          isSelected={c.id === selected}
          onSelect={() => setSelected(c.id)}
        />
      ))}
    </div>
  );
}

const CARDS = [
  { id: 1 },
  { id: 2 },
  { id: 3 },
  { id: 4 },
  { id: 5 },
  { id: 6 }
];

// Card.js
const Card = ({ id, isSelected, onSelect }) => {
  return (
    <div
      className="card"
      onClick={onSelect}
      style={{ backgroundColor: isSelected ? "rgba(0, 0, 0, 0.1)" : "white" }}
    />
  );
};

export default Card;

문제점


카드 한장을 선택 할때 마다 모든 카드가 리렌더링 되고 있다.
예제에선 카드가 6장 밖에 없어 성능상 큰 문제가 없는데
개발자로 일하면서 마주치게 될 실제 상황 예로는 3000개의 Row를 가진 Table, 3000개의 Item을 가진 List등 에서 각각의 인터랙션이 필요한 상황이 있을 수 있다.

한장 선택 했는데 3000장의 카드가 모두 리렌더링 되는 것은 초과비용이 너무 크다.

문제 발생 이유

  • React는 상태가 바뀌면 리렌더링이 된다.
  • 부모 컴포넌트가 리렌더링 되면 자식 컴포넌트가 리렌더링된다.

해결 방법

1. React.memo()

컴포넌트의 props 가 바뀌지 않았다면, 리렌더링을 방지한다.

React.memo를 사용하기 위해 Card 컴포넌트의 Props를 확인 해보자.

<Card id={c.id} isSelected={c.id === selected} onSelect={() => setSelected(c.id)} />

id, isSelected, onSelected 3가지 Props중에 카드를 선택 할때 마다isSelected가 변할 것이다.

카드들의 선택 isSelected 를 배열에 담았다고 가정하면,
1번 카드 선택 - [true,false,false,false,false,false]
2번 카드 선택 - [false,true,false,false,false,false]

위와 같이 1번 카드를 선택한 상태에서 2번 카드를 선택하면 1,2번의 isSelected만 바뀌는 것이다.
그래서 3,4,5,6 번 카드는 props의 변화가 없으니 React.memo()를 적용하면 해결 될 것이다.

//
const Card = ({ id, isSelected, onSelect }) => {
  {...}
};
export default React.memo(Card);

하지만 변화가 없다. 카드를 선택 할때 마다 모든 카드가 렌더링 된다.

이유가 무엇일까?

<Card id={c.id} isSelected={c.id === selected} onSelect={() => setSelected(c.id)} />

문제는 onSelect={() => setSelected(c.id)} 여기서 발생한다.

  • React 컴포넌트 함수 안에 함수가 선언이 되어 있다면 그 함수는 해당 컴포넌트가 렌더링될 때 마다 새로운 함수가 생성된다.
  • Props를 전달 할때 함수를 직접 선언하면 렌더링 될 때 마다 새로운 함수를 생성된다.
  • () => setSelected(c.id) 이 함수가 새롭게 생성되는 것이다. (이 문제의 핵심)

그래서 카드 한장을 선택 할때 마다 selected가 변경되어 리렌더링 되면서
onSelect Props로 전달한 () => setSelected(c.id) 함수가 새로 생성된 함수로 바뀌어
React.memo()가 의도한 대로 동작하지 않은 것이다.

2. useCallback()

메모이제이션(memoization)하기 위해서 사용되는 hook 함수.
첫번째 인자로 넘어온 함수를, 두번째 인자로 넘어온 배열 내의 값이 변경될 때까지 저장해놓고 재사용할 수 있다.
const memoizedCallback = useCallback(func, []);

// App.js
export default function App() {
  const [selected, setSelected] = useState(null);
  
  // useCallback을 이용한 memoization : 렌더링 되어도 새로 생성하지 않는다.
  const onSelect = useCallback((id) => {
    setSelected(id);
  }, []);

  return (
    <div className="grid">
      {CARDS.map((c) => (
        <Card
          key={c.id}
          id={c.id}
          isSelected={c.id === selected}
          onSelect={onSelect}
          // onSelect={() => onSelect(c.id)}
          // props로 화살표 함수를 전달 해도 렌더링 시 새로 생성되기에 onSelect의 인자는 Card(자식)에서 전달하도록 한다.
        />
      ))}
    </div>
  );
}

// Card.js
const Card = ({ id, isSelected, onSelect }) => {
  // onSelect의 인자는 Card(자식) 컴포넌트에서 전달 한다.
  const handleSelect = () => {
    onSelect && onSelect(id);
  };
  return (
    <div
      className="card"
      onClick={handleSelect}
      style={{ backgroundColor: isSelected ? "rgba(0, 0, 0, 0.1)" : "white" }}
    />
  );
};

export default React.memo(Card);

최적화 이후

원하는 대로 이전 선택된 카드, 새로 선택된 카드만 리렌더링 되는 것을 확인 할수 있다.

전체 코드

profile
왜?를 생각하며 개발하기, 다양한 프로젝트를 경험하는 것 또한 중요하지만 내가 사용하는 기술이 어떤 배경과 이유에서 만들어진 건지, 코드를 작성할 때에도 이게 최선의 방법인지를 끊임없이 질문하고 고민하자. 이 과정은 앞으로 개발자로 커리어를 쌓아 나갈 때 중요한 발판이 될 것이다.

0개의 댓글