useCallback 파헤치기

강석우's avatar
Mar 05, 2024
useCallback 파헤치기

useCallback 이란?

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

useCallback, 언제 사용해야 할까

간략하게 정리하자면 리렌더링으로 인한 함수의 재정의를 하고 싶지 않을 때 사용한다.
아래의 리스트에 대한 자세한 설명은 사용법 파트에 나와있다.

  • 컴포넌트의 리렌더링 건너뛰기

  • 메모된 콜백에서 state 업데이트하기

  • Effect가 너무 자주 발동되지 않도록 하기

  • 커스텀 훅 최적화하기

useCallback 의 구성

useMemo는 calculateValue와 dependencies로 나뉘어진다.
fn : 캐시하려는 함수.
어떤 인자도 맏을 수 있고 어떤 값이라도 반환할 수 있다. 만약 dependencies가 변경되지 않았다면 동일한 함수를 제공한다. 변경되었다면 렌더링 중에 fn를 제공하고 재사용할 수 있도록 저장한다.

dependencies : calculateValue 에 참조되는 값들의 목록.
Object.is 비교 알고리즘을 사용하여 각 dependencies를 이전 값과 비교한다.

useMemo(calculateValue, dependencies)

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}

사용시 주의사항

  • useCallback은 훅이기 때문에 컴포넌트의 최상위 레벨 또는 커스텀 훅에서만 호출이 가능.

사용법

1. 컴포넌트의 렌더링 건너뛰기

function ProductPage({ productId, referrer, theme }) {
  // ...
  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
  // ...
});

위와 같은 상황에서 JavaScript에서 function () {} 또는 () => {} {} 객체 리터럴이 항상 새 객체를 생성하는 것과 유사하게 항상 다른 함수를 생성하기 때문에 ShippingForm의 props는 결코 동일하지 않으며 memo최적화가 작동하지 않는다.
따라서 우리는 아래와 같이 handleSubmit 함수를 변경 시켜줄 필요가 있다.

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

위 처럼 함수를 useCallback 으로 감싸면 리렌더링 사이에 동일한 함수가 되도록 할수 있다.

예시는 리액트 공식문서에 잘 나와있다.

2. 메모된 콜백에서 state 업데이트하기

일반적으로 메모화된 함수는 가능한 적은 의존성을 갖기원하기 때문에 업데이터 함수를 전달하여 의존성을 제거할 수 있다.

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos([...todos, newTodo]);
  }, [todos]);
  // ...
function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos(todos => [...todos, newTodo]);
  }, []); // ✅ No need for the todos dependency 
          // ✅ todos에 대한 의존성이 필요하지 않음
  // ...

3. Effect가 너무 자주 발동되지 않도록 하기

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  function createOptions() {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    return () => connection.disconnect();
  }, [createOptions]);

위와 같이 useEffect에 의존성을 함수로 선언해주면 렌더링시마다 의존성이 변경되기 때문에 문제가 발생한다.
위와 같은 경우에도 createOptions를 useCallback 으로 감싸주면 해결이 된다.

const createOptions = useCallback(() => {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }, [roomId]);

이렇게 감싸주면 roomId가 변경되었을 때에만 함수가 변경되게 된다.
하지만 가장 좋은 방법은 createOptions 함수 자체를 useEffect 안으로 넣는 방법이다.
그러면 함수가 계속 변경될 일도 없고 useCallback 도 사용할 필요가 없기 때문에 best다!

4. 커스텀 훅 최적화하기

커스텀 훅을 작성하는 경우 반환하는 모든 함수를 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,
  };
}

이렇게 작성해 놓으면 훅의 소비자가 필요할 때 자신의 코드를 최적화할 수 있다.

그래서 useMemo 와 useCallback의 차이가 뭐야?

둘의 차이점은 캐시 할 수 있는 항목에 있다.
useMemo는 호출한 함수의 결과를 캐시하고 useCallback은 함수자체를 캐시한다.

import { useMemo, useCallback } from 'react';

function ProductPage({ productId, referrer }) {
  const product = useData('/product/' + productId);

  const requirements = useMemo(() => { 
     // 함수를 호출하고 그 결과를 캐시합니다.
    return computeRequirements(product);
  }, [product]);

  const handleSubmit = useCallback((orderDetails) => { 
     // 함수 자체를 캐시합니다.
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

  return (
    <div className={theme}>
      <ShippingForm requirements={requirements} onSubmit={handleSubmit} />
    </div>
  );
}

useMemo는 위의 예제에서 product가 변경되지 않는 한 computeRequirements를 호출한 결과를 캐시한다. 이렇게 하면 ShippingForm을 리렌더링할 필요 없이 requirements를 전달할 수 있다.

useCallback은 위의 예제에서 제공한 함수를 호출하지 않는다. 대신 제공한 함수를 캐시하여 handleSubmit 자체가 변경되지 않도록 한다. 이렇게 하면 ShippingForm을 리렌더링할 필요없이 handleSubmit 함수를 전달할 수 있다.

Share article

석우의 개발블로그