프로젝트 캠프: Next.js 과정 2기

[유데미x스나이퍼팩토리] 프로젝트 캠프 : Next.js 2기 - 사전직무교육 2주차 - Memoization

hanlabong 2024. 7. 28. 22:07
728x90

학습 목표

memoization을 이해하고 렌더링 최적화 방법을 익힙니다.

memoization이란?

💡 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장하여 사용함으로써 동일한 계산의 반복 수행을 제거하여 성능 향상을 도모하는 기술

 

useCallback

함수형 컴포넌트에서 함수를 메모이제이션하는 데 사용한다.

사용 방법

useCallback(fn, dependencies)

예시

카운트를 증가시키는 함수를 생성하고 이를 집합에 넣어보자.

const functionSet = new Set();

const App = () => {
  const [count, setCount] = useState(0);

  const increament = setCount((prev) => prev + 1);

  functionSet.add(increament);
  console.log(functionSet);

  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={increament}>increase</button>
    </>
  );
};

집합을 출력해보면 카운트를 증가시킬 때마다 increment 함수가 추가되는 것을 확인할 수 있다.

왜 추가될까?

카운트가 증가될 때마다 App컴포넌트가 rerendering 되는데, 이때 함수의 참조값인 increment가 갱싱되기 때문에 js는 이를 다른 객체로 인식해서 집합에 계속해서 쌓이는 것이다.

어떻게 해결할 수 있을까?

useCallback 사용!

const functionSet = new Set();

const App = () => {
  const [count, setCount] = useState(0);

  const increament = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  functionSet.add(increament);
  console.log(functionSet);

  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={increament}>increase</button>
    </>
  );
};

위와 같이 작성하면 useCallback이 함수를 저장하고 있기 때문에 카운트를 증가시키더라도 집합의 개체가 증가하지 않는 것을 확인할 수 있다.

 

주의사항

setState를 할 때 콜백함수를 사용하자

만약 setState를 사용해 상태를 업데이트할 때

const increament = useCallback(() => {
    setCount(count + 1);
  }, []);

이런 방식으로 작성한다면 카운트는 1에서 더이상 증가하지 않을 것이다. 왜냐하면 useCallback은 스냅샷을 찍듯 함수를 저장한다. 따라서 위의 코드에서는 메모이제이션됐을 때 count가 0이었기 때문에 1에서 더이상 증가하지 않게 된다.

dependencies를 확인하자

useCallback에서는 dependencies에 등록된 값의 변화를 인지하고 변화가 일어났을 때 useCallback에 정의된 함수를 실행한다. 따라서 다음과 같이 작성했을 때

const increament = useCallback(() => {
  setCount((prev) => prev + 1);
}, [count]);

카운트를 증가시킬 때마다 카운트를 증가시키는 함수가 실행되기 때문에 카운트가 무한으로 증가되는 문제가 발생하게 된다.

 

useMemo

값을 메모이제이션하는 훅이다.

사용 방법

const cachedValue = useMemo(calculateValue, dependencies)

예시

길이가 매우 긴 배열에서 특정 값을 찾아 렌더링하는 코드가 있다고 하자.

import { useState } from "react";

const initialItems = new Array(29_999_999).fill(0).map((_, i) => {
  return { id: i, selected: i === 29_999_998 };
});

const App = () => {
  const [count, setCount] = useState(0);
  const [items, _] = useState(initialItems);
  const selectedItem = items.find((item) => item.selected);

  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount((prev) => prev + 1)}>increase</button>
      <h2>{selectedItem?.id}</h2>
    </>
  );
};
export default App;

위와 같이 작성하면 카운트가 변경될 때마다 selectedItem은 값이 변하지 않음에도 find함수가 새롭게 실행되어 성능적인 문제가 발생하게 된다.

어떻게 해결할까?

import { useMemo, useState } from "react";

const initialItems = new Array(29_999_999).fill(0).map((_, i) => {
  return { id: i, selected: i === 29_999_998 };
});

const App = () => {
  const [count, setCount] = useState(0);
  const [items, _] = useState(initialItems);

  const selectedItem = useMemo(() => items.find((item) => item.selected), []);

  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount((prev) => prev + 1)}>increase</button>
      <h2>{selectedItem?.id}</h2>
    </>
  );
};
export default App;

useMemo를 사용해 값을 지정해주면 그 값이 메모이제이션되어 값을 찾는 코드를 다시 실행하지 않는다.

 

주의사항

dependencies를 확인하자

useCallback과 동일하게 useMemo 역시 dependencies에 등록된 값의 변화를 인지하고 변화가 일어났을 때 useCallback에 정의된 함수를 실행한다. 따라서 다음과 같이 작성했을 때

const selectedItem = useMemo(
  () => items.find((item) => item.selected),
  [count]
);

카운트를 증가시킬 때마다 selectedItem을 찾는 함수가 실행되기 때문에 useMemo를 사용하는 의미가 없어진다.

 

React.memo

컴포넌트를 메모이제이션할 때 사용하는 함수이다.

사용 방법

const MemoizedComponent = React.memo(SomeComponent, arePropsEqual?)

예시

상위 컴포넌트에서 상태변화가 일어난다면 하위 컴포넌트는 모두 재렌더링된다. 이때 React.memo를 사용하면 재렌더링을 막을 수 있다.

import { useState } from "react";
import A from "./components/learn/A";
import B from "./components/learn/B";

const App = () => {
  const [count, setCount] = useState(0);
  return (
    <>
      <h1>App Component: {count}</h1>
      <button onClick={() => setCount((prev) => prev + 1)}>increase</button>
      <A />
      <B />
    </>
  );
};
export default App;
export default React.memo(A);
export default React.memo(B);

 

주의사항

만약 B 컴포넌트에 count를 props로 넘겨주면 React.memo를 지정해줬어도 App에서 count를 증가시킬 때마다 컴포넌트가 재렌더링된다.
그 값을 사용하지 않더라도 props로 전달되는 순간부터 영향을 끼친다.
만약 함수를 넘겨준다면 useCallback으로 함수를 메모이제이션해주면 해결할 수 있다.

 

useCallback vs useMemo vs React.memo

  useCallback useMemo React.memo
참조 대상 함수 컴포넌트
훅 / 함수 함수 (고차 컴포넌트)

*고차 컴포넌트: 컴포넌트를 반환하는 함수

728x90