본문 바로가기

개발 공부

[React] useCallback은 무엇일까?

useCallback은 리렌더링 사이에 함수 정의를 캐시할 수 있게 해주는 React 훅입니다.

즉, 함수가 정의되고, 리랜더링이 일어나고 의존성 배열이 바뀌지 않는다면 함수는 새로 만들어지지 않고 기존의 함수가 반환됩니다.

사용법

import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);
  // ...

 

- 리렌더링 사이에 캐시할 함수, 함수내에서 사용되는 컴포넌트 내부의 모든 값을 포함하는 의존성 배열

- 렌더링 과정: 초기 렌더링 시 처음에 전달했던 함수 반환 -> 리렌더링발생시, 이전 렌더링에서 전달된 의존성과 비교 -> 의존성이 변경되지 않았다면 이전과 같은 함수 반환 -> 의존성이 변경되었다면 이번 렌더링에서 전달할 함수를 반환

이점

- 리랜더링이 되고, 의존성이 바뀌지 않았다면 캐시된 기존의 함수를 반환함으로써 성능을 최적화할 수 있습니다.

- 함수를 새로 만들지 않고, 기존의 함수를 사용하면 좋은 점은 JavaScript는 내부의 똑같은 함수여도 항상 다른 함수를 생성합니다. 그래서, useCallback을 사용하면 동일한 함수가 되도록 할 수 있습니다.(의존성이 변경되지 않았다면)

useCallback은 어떤 곳에서 사용하면 좋을까?

- 그림 편집기 처럼 인터랙션이 많이 일어나는 곳이 좋습니다. (포토샵, 파워포인트, 일러스트, 미리캔버스 등) 

- memo로 감싼 컴포넌트에 prop으로 전달하는 경우, 메모화를 사용하면 의존성이 변경된 경우에만 컴포넌트를 리렌더링할 수 있다.

- 전달한 함수는 나중에 일부 훅의 의존성으로 사용됩니다. (예: useCallback으로 감싼 다른 함수가 이 함수의 의존하거나, useEffect에서 이 함수에 의존하는 경우)

단점

- 코드 가독성이 떨어진다.

- 만약, 항상 새로운 단일 값인 경우 전체 컴포넌트에 대한 메모화가 깨질 수 있다.

- 캐시를 하는 것이기 때문에, 메모리를 사용하게 된다.

메모화할 필요가 없게 만드는 경우는?

1. 컴포넌트가 childern을 이용하여 다른 컴포넌트를 감쌀 경우

- 감싼 컴포넌트 내부에서 state를 업데이트 하더라도, 자식 컴포넌트는 리렌더링이 안 일어난다.

2. 컴포넌트 내부의 state를 선호하고, prop으로 전달해서 사용하는 state는 필요 이상으로 사용하지 않는 경우(state 끌어올리기)

3. 렌더링 로직을 순수하게 유지하는 경우

4. state를 업데이트하는 불필요한 Effect를 제거하는 경우

- React에서 대부분의 성능 문제는 컴포넌트를 반복적으로 렌더링하게 하는 Effect의 업데이트 체인으로 발생한다.

5. Effect에서 불필요한 의존성을 제거한 경우

 

즉, 메모화하기 전에 위에 처럼 컴포넌트 구조를 만드는 게 좋습니다!

 

useCallback을 이용한 커스텀 훅 최적화

- 커스텀 훅을 작성하는 경우 반환하는 모든 함수를 useCallback으로 감싸는 것이 좋다.

- 훅의 소비자가 필요할 때 자신의 코드를 최적화 할 수 있다.

function useRouter() {
  const { dispatch } = useContext(RouterStateContext);

  const navigate = useCallback((url) => {
    dispatch({ type: 'navigate', url });
  }, [dispatch]);

  const goBack = useCallback(() => {
    dispatch({ type: 'back' });
  }, [dispatch]);

  return {
    navigate,
    goBack,
  };
}

 

만약, useCallback을 사용했는데 문제가 발생했다면?

1. 두 번째 인자로 의존성 배열을 지정했는지 확인

- 의존성 배열이 없는 경우 매번 새로운 함수를 반환한다.

2. 루프 안에서 uesCallback을 사용할 때, 개별 항목에 대한 컴포넌트를 추출하고 그 컴포넌트에 useCallback을 사용해야된다.

function ReportList({ items }) {
  return (
    <article>
      {items.map(item =>
        <Report key={item.id} item={item} />
      )}
    </article>
  );
}

function Report({ item }) {
  // ✅ Call useCallback at the top level:
  // ✅ useCallback은 컴포넌트의 최상위 레벨에서 호출하세요:
  const handleClick = useCallback(() => {
    sendReport(item)
  }, [item]);

  return (
    <figure>
      <Chart onClick={handleClick} />
    </figure>
  );
}