useMemo / useCallback

useState, useEffectuse-로 시작하는 함수들을 자주 보게 된다.
이들 모두 React에서 제공하는 Hook이다.

이러한 HookuseMemouseCallback에 대해서 알아보고자 한다.

useMemo

useMemo값을 기억하는 Hook이다.

const memoizedValue = useMemo(() => {
  return 계산값;
}, [의존성]);

특정 값의 계산 결과를 저장해두고 의존성(dependency)이 변경될 때만 다시 계산한다.

불필요한 반복 계산을 막기 위한 Hook

const sortedList = useMemo(() => {
  return [...items].sort((a, b) => a - b);
}, [items]);

위 코드를 보면 items가 바뀔 때만 정렬을 다시 수행한다. 그렇지 않으면 이전에 계산된 결과(sortedList)를 그대로 재사용한다.

여기서 헷갈리기 쉬운 부분이 있다.
의존성 배열[items]는 값을 저장하는 것이 아닌, 다시 계산할지 판단하는 기준이다.

정리하자면

  • useMemo는 계산 결과를 기억하고, 의존성이 변경될 때만 다시 실행된다.
  • 불필요한 계산을 줄이기 위해 사용한다.

남용하면 생기는 문제

const sum = useMemo(() => a + b, [a, b]);

이건 그냥

const sum = a + b;

이렇게 가벼운 계산에는 그냥 쓰는 것이 좋다 -> 코드의 복잡성만 향상

  • 성능 저하
    useMemo도 비용이 있다.
    의존성 비교 비용, 메모리 사용, Hook 실행 비용이 있기에 무조건 빠르게만 해주는 도구가 아니라는 점을 인지해야한다.

  • 의존성 관리 실수

const value = useMemo(() => {
  return count * 2;
}, []);

count가 바뀌어도 값이 안바뀐다.

정리하자면 useMemo

비싼 계산을 줄이기 위한 선택적 최적화

이기에

- 계산 비용이 크거나
- 렌더링마다 실행이 부담되거나
- 실제 성능 문제가 발생했을 때

사용하는 것이 좋다.

useCallback

useCallback함수를 기억하는 Hook이다.

const memoizedFn = useCallback(() => {
  실행할 함수
}, [의존성]);

특정 함수를 저장해두고, 의존성(dependency)이 변경될 때만 새로운 함수를 생성한다

불필요한 함수 재생성을 막기 위한 Hook

천천히 알아보자.

❌ useCallback 없이

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log("클릭");
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <Child onClick={handleClick} />
    </div>
  );
}

여기서 handleClick 함수는 랜더링이 될 때마다 새로 생성된다

버튼을 눌러 `count`증가 -> Parent 다시 렌더링 -> `handleClick` 새로 생성

이렇게 되면 React 기본 구조상 부모 컴포넌트가 렌더링되면 자식 컴포넌트도 함께 렌더링된다.

사실 handleClick이 새로 생성되는 것이 직접적인 문제가 되는 것은 아니다.
하지만 최적화를 했을 때는 문제가 발생한다.

예를 들어, 자식 컴포넌트를 React.memo로 감싸 불필요한 렌더링을 막으려고 한다고 가정해보자.

const Child = React.memo(({ onClick }) => {
  console.log("Child 렌더링");
  return <button onClick={onClick}>Click</button>;
});

이 경우 props인 onClick이 변경되지 않으면 Child는 다시 렌더링되지 않아야 한다.
하지만

const handleClick = () => {
  console.log("클릭");
};

이 함수는 렌더링이 될 때마다 새로 생성되기 때문에 props가 변경된 것으로 인식된다
결과적으로 Child 컴포넌트의 리렌더링을 막을 수 없는 것이다.
이 문제를 해결하기 위해 useCallback를 사용한다.

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("클릭");
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <Child onClick={handleClick} />
    </div>
  );
}

이렇게 하면 렌더링이 다시 발생해도 handleClick함수는 새로 생성되지 않고 유지된다.
Child 컴포넌트의 불필요한 리렌더링을 방지할 수 있는 것이다.


남용하면 생기는 문제

useCallback은 기본이 아닌 최적화 도구

▪️ 코드가 오히려 복잡해진다

const handleClick = useCallback(() => {
  console.log("클릭");
}, []);

단순한 함수라면 굳이 필요가 없다

▪️ 성능이 더 나빠질 수도 있다

  • 의존성 배열을 비교하는 비용 발생
  • 메모이제이션 자체도 비용이 있음
    → 작은 코드에서는 오히려 손해

▪️ 의존성 관리 실수

const handleClick = useCallback(() => {
  console.log(count);
}, []);

count가 바뀌어도 반영되지 않음 (버그)


useMemouseCallback에 대해 정리해보았다. React의 여러 Hook을 알게 될 때마다 어떻게 사용해야 더 좋은 코드가 나오는지 생각해보게 되는 것 같다.

익숙해질 수 있도록 여러 응용을 다뤄보자.

profile
다른 건 노력의 시간

0개의 댓글