useCallback 정리 (+ React.memo)

현수·2023년 6월 2일
0

React Hooks

목록 보기
2/4
post-thumbnail

역할

리렌더링이 일어날때 필요에 따라 함수 정의를 반복하지 않고 캐시해서 값을 재사용하여 성능을 개선한다.

사용법

const cachedFn = useCallback(fn, dependencies)
  • fn: 캐시하려는 함수 정의. 먼저 처음 렌더링에서 함수 정의를 반환한다.(호출이 아님!) 그리고 다음 렌더링에서 마지막 렌더링 이후 dependencies 가 변경되지 않은 경우 캐시된 함수 정의를 재사용한다. 그렇지 않으면 현재 렌더링 중에 전달한 함수로 새롭게 정의하고 나중에 재사용할 수 있도록 저장한다. 함수를 호출하지 않고 함수 정의가 반환되므로 언제 함수를 실행할지는 추후 조절할 수 있다.

  • dependencies: fn 코드 내부에서 참조되는 모든 변수값 목록이고 이는 state, props 그리고 컴포넌트 내부에서 선언된 변수와 함수가 해당한다. 여기에 속한 값의 변경이 감지되면 fn 함수를 다시 정의하고 캐시한다.

사용예

1. 하위 컴포넌트의 리렌더링 방지 - React.memo

// useCallback 사용예
function ProductPage({ productId, referrer, theme }) {
  // 함수 객체를 캐시...
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // ...이 종속값이 변하지 않는 이상 참조값이 변하지 않는다...

  return (
    <div className={theme}>
      {/* ...ShippingForm은 같은 props를 받았을 경우 리렌더링을 건너뛴다 */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

자식 요소 ShippingForm 에게 함수 객체 handleSubmit 를 전달하는 상황이다. 만약 ShippingForm 컴포넌트의 실행 비용이 높아 부모 컴포넌트가 렌더링 될때마다 자식 컴포넌트를 리렌더링 시키는 것이 부담스럽다면 useCallbackReact.memo 기능을 이용해 자식 컴포넌트의 리렌더링을 막을 수 있다.

import { memo } from 'react';

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

React.memo 로 컴포넌트를 래핑하면 ShippingForm 가 받아오는 propshandleSubmit 가 변하지 않았다면 ShippingForm 컴포넌트가 리렌더링 되지 않고 만약 변했다면 리렌더링된다

// useCallback 미사용예
function ProductPage({ productId, referrer, theme }) {
  // 매실행마다 다른 참조 값의 함수 객체를 반환하므로 값이 항상 변한다...
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      {/* ... 그래서 ShippingForm의 props는 절대 같을 수 없고 항상 리렌더링 된다... */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

이때 만약 useCallback 을 사용하지 않는다면 React.memo 기능은 동작하지 않는다. 왜그럴까?

자바스크립트에서 함수 선언(function() {} or () => {}) 또한 객체 리터럴({})을 생성하는 것처럼 매실행마다 새로운 참조값을 가진다. 따라서 handleSubmit 함수 객체는 매실행마다 새로운 참조값을 가지고 이는 ShippingForm 에 전달되는 props 값을 매번 변화시켜 ShippingForm 함수 내부의 변화가 없을 때에도 React.memo가 적용된 자식 컴포넌트를 리렌더링 시킨다.

따라서 useCallback으로 props 로 전달할 함수를 래핑하면 종속된 값이 변경되지 않는한 함수의 참조값이 변하지 않게 할 수 있다.

2. 메모이제이션된 콜백에서 상태 업데이트

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]);
  }, []); // ✅ todos 종속이 필요하지 않다.
  // ...

useCallback 내부에서 state 를 업데이트할 경우 state 를 종속 리스트에 등록하는 것보다 setState업데이트 함수를 전달하는 것이 더 직관적일 것이다.

3. useEffect 가 너무 자주 발생하지 않도록 방지

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

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

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🔴 Problem: This dependency changes on every render
    // ...

useEffect 내부에서 외부 함수를 호출하고 해당 함수 객체를 종속 관계로 등록하면 렌더링 될때 마다 함수의 참조값이 바뀌기때문에 useEffect를 계속 실행시킬 것이다.

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

  const createOptions = useCallback(() => {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }, [roomId]); // ✅ 오직 roomId가 바뀔 때만 변경

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // ✅ 오직 변경createOptions가 바뀔 때만 실행
  // ...

이경우 위의 코드처럼 useCallback 으로 함수를 래핑하면 동일한 createOptions 일때 같은 참조값을 보장해주어서 useEffect의 콜백 함수 실행을 줄일 수 있다.

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

  useEffect(() => {
    function createOptions() { // ✅ useCallback과 함수 종속성이 필요없다.
      return {
        serverUrl: 'https://localhost:1234',
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [roomId])<; // ✅ 오직 roomId가 바뀔 때만 변경
  // ...

하지만 useEffect 외부에 선언된 함수를 종속성에 등록해서 사용하는 것보다 외부의 함수 선언을 useEffect 내부로 옮겨서 사용하는 것이 더 효율적이다.

4. Custom Hooks 최적화

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,
  };
}

커스텀 Hooks를 작성하는 경우 반환되는 모든 함수를 useCallback 으로 래핑하는 것이 좋다.

레퍼런스

리액트 공식문서 - useCallback

0개의 댓글