본문 바로가기
프로그래밍/React

[React] useCallback

by YuminK 2023. 11. 28.

 

useCallback

useCallback is a React Hook that lets you cache a function definition between re-renders.

 

const cachedFn = useCallback(fn, dependencies)

 

Parameters 

fn: The function value that you want to cache. It can take any arguments and return any values. React will return (not call!) your function back to you during the initial render. On next renders, React will give you the same function again if the dependencies have not changed since the last render. Otherwise, it will give you the function that you have passed during the current render, and store it in case it can be reused later. React will not call your function. The function is returned to you so you can decide when and whether to call it.

 

dependencies: The list of all reactive values referenced inside of the fn code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3]. React will compare each dependency with its previous value using the Object.is comparison algorithm.

 

의존성 값이 변하지 않으면 계속 해당 값을 이용한다. 초기에 등록한 함수를 반환한다.

 

초기에 선언한 함수를 얻는다. 리액트에서는 의존성 값들을 비교하여 변경이 일어나지 않으면 동일한 함수를 반환한다. 아니라면, 해당 렌더링에서 넘겨지는 함수를 반환한다. 다른 말로, useCallback은 리-렌더 사이에서 함수를 캐싱한다. 

 

기본적으로 리액트에서 리렌더링이 일어날 때, 모든 자식을 재귀적으로 다시 그린다. 

 

function ProductPage({ productId, referrer, theme }) {

  // Every time the theme changes, this will be a different function...

  function handleSubmit(orderDetails) {

    post('/product/' + productId + '/buy', {

      referrer,

      orderDetails,

    });

  }

  

  return (

    <div className={theme}>

      {/* ... so ShippingForm's props will never be the same, and it will re-render every time */}

      <ShippingForm onSubmit={handleSubmit} />

    </div>

  );

}

 

자바스크립트에서 function () {}, () => {} 형태는 항상 새로운 함수를 생성한다. 마치 json 객체가 매번 오브젝트를 생성하는 것과 동일하다.  useCallback으로 함수를 감싸서 리렌더링 상황에서도 해당 함수가 같음을 명확하게 할 수 있다. 

 

useMemo와 useCallback은 컴포넌트 최적화에서 같이 사용된다. useMemo는 해당하는 값을 캐싱하고, useCallback은 함수 자체를 캐싱한다. 

 

많은 변경이 불필요한 앱의 경우 memoziation은 불필요하다. 만약 앱이 편집기거나 도형을 움직여야 하는 경우 메모이제이션은 도움이 된다. 함수를 캐싱하는 경우는 오직 다음의 경우에서 가치가 있다.

 

1. 메모로 둘러싸인 컴포넌트의 prop으로 함수를 넘길 때, 오직 의존성이 변경되는 상황에서만 재렌더링 되도록 만든다.

2. 다른 훅의 의존성으로 함수가 사용되는 경우, 예를 들면 useCallback에서 해당 함수를 의존하는 경우나 useEffect에서 해당 함수에 의존할 때

 

다른 경우에는 딱히 이득을 보는 상황이 없다. 크게 이득을 취하는 상황이 아니다. 만약 모든 함수에 대한 메모이제이션을 처리하는 경우, 코드를 알아먹기 힘들어질 수 있다. 그리고 딱히 효율적이지도 않다.

 

useCallback은 함수의 생성을 막는 것이 아니다. 항상 새로운 함수를 생성 한 이후에(이건 괜찮다) 리액트는 이를 무시하고 캐시된 함수를 주는 것이다. 실제로, 다음 원칙을 따르면 많은 메모이제이션을 불필요하게 만들 수 있다.

 

1. 컴포넌트가 시각적으로 다른 컴포넌트에 래핑되면, jsx를 자식으로서 처리해라. 만약 wrapper 컴포넌트의 상태가 변경되면 리액트는 자식이 다시 그려질 필요가 없다는 것을 인지하고 있다. 

 

2. 로컬 상태를 선호하고 필요 이상으로 상태를 올리지 마라. forms 같은 임시 상태를 유지하지 마라. 항목의 트리 상단이나 글로벌 상태 라이브러리에 호버되어 있는지 여부를 따지지 마라.

 

3. 렌더링 로직을 순수하게 유지하라. 만약 컴포넌트 리렌더링이 문제 혹은 시각적 문제를 만든다면 컴포넌트에 버그가 있는 것이다. 메모이제이션을 추가할 것이 아니라, 고쳐라. 

 

4. 상태를 업데이트하는 불필요한 효과를 없애라. 리액트 내에서 주요 퍼포먼스 이슈는 구성요소가 계속 업데이트 되도록 하는 효과로부터 처리되는 일련의 동작 때문이다.

 

5. 이펙트에서 불필요한 의존성을 없애라. 예를 들면 메모이제이션 대신에 오브젝트나 함수를 Effect나 다른 컴포넌트 내부 혹은 외부로 옮기는게 더 단순하다.

 

이후에도 렉이 걸리면, 리액트 개발자 툴 프로파일러를 사용하여 어떤 컴포넌트에서 메모이제이션을 사용해야 할지 파악해라. 어떠한 상황에서도 위 원칙을 따르는 것이 좋다. 리액트 팀에서는 장기적 관점에서 자동으로 메모이제이션을 사용하여 해당 문제를 근본적으로 없애는 방안을 생각하고 있다.

 

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 => [...todos, newTodo] 처리를 사용해라. 생각해보면 너무 당연한 소리다. 의존성이 꼭 필요한 곳에서만 쓰라는 내용.

 

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();

    // ...

 

이런 상황에서는 리렌더링시 createOptions가 매번 변경이 되므로 useEffect의 의존성으로 함수를 넣는 의미가 없다.

 

  useEffect(() => {

    const options = createOptions();

    const connection = createConnection();

    connection.connect();

    return () => connection.disconnect();

  }, [createOptions]); // 🔴 Problem: This dependency changes on every render

  // ...

 

이런 상황에서 useCallback을 사용하여 createOptions를 감싸고 의존성으로 넘길 필요가 있다. 

 

function ChatRoom({ roomId }) {

  const [message, setMessage] = useState('');

 

  const createOptions = useCallback(() => {

    return {

      serverUrl: 'https://localhost:1234',

      roomId: roomId

    };

  }, [roomId]); // ✅ Only changes when roomId changes

 

  useEffect(() => {

    const options = createOptions();

    const connection = createConnection();

    connection.connect();

    return () => connection.disconnect();

  }, [createOptions]); // ✅ Only changes when createOptions changes

  // ...

 

roomId 값이 변하지 않았다면, createOptions의 값도 변경되지 않음을 확신할 수 있다. 그러나 이 경우에는 차라리 함수를 useEffect 내부에 넣는 것이 더 나은 방식이다. 

 

function ChatRoom({ roomId }) {

  const [message, setMessage] = useState('');

 

  useEffect(() => {

    function createOptions() { // ✅ No need for useCallback or function dependencies!

      return {

        serverUrl: 'https://localhost:1234',

        roomId: roomId

      };

    }

 

    const options = createOptions();

    const connection = createConnection();

    connection.connect();

    return () => connection.disconnect();

  }, [roomId]); // ✅ Only changes when roomId changes

  // ...

 

괜히 복잡하게 짜지 말고 역시 간단한 게 최고지. 커스텀 훅을을 작성할 때, 반환하는 함수를 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,

  };

}

 

의존성을 추가하지 않는 경우, 매번 새로운 함수가 반환될 것이다. (하지 말 것)

 

function ProductPage({ productId, referrer }) {

  const handleSubmit = useCallback((orderDetails) => {

    post('/product/' + productId + '/buy', {

      referrer,

      orderDetails,

    });

  }); // 🔴 Returns a new function every time: no dependency array

  // ...

 

function ProductPage({ productId, referrer }) {

  const handleSubmit = useCallback((orderDetails) => {

    post('/product/' + productId + '/buy', {

      referrer,

      orderDetails,

    });

  }, [productId, referrer]); // ✅ Does not return a new function unnecessarily

  // ...

 

이후에도 문제가 발생한다면, 의존성을 로깅하여 체크해보자. 

 

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?

Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?

Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

 

내부에서 memo를 쓰고 있을 텐데 루프내에서 useCallback 사용할 수 없다.

 

function ReportList({ items }) {

  return (

    <article>

      {items.map(item => {

        // 🔴 You can't call useCallback in a loop like this:

        const handleClick = useCallback(() => {

          sendReport(item)

        }, [item]);

 

        return (

          <figure key={item.id}>

            <Chart onClick={handleClick} />

          </figure>

        );

      })}

    </article>

  );

}

 

대신에 이 부분을 따로 컴포넌트로 뺴서 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:

  const handleClick = useCallback(() => {

    sendReport(item)

  }, [item]);

 

  return (

    <figure>

      <Chart onClick={handleClick} />

    </figure>

  );

}

 

혹은 useCallback을 사용하지 않고 Report를 memo하는 것도 가능하다. 만약 item props가 변경되지 않았다면 Report는 재렌더링 되지 않을 것이다. 

 

function ReportList({ items }) {

  // ...

}

 

const Report = memo(function Report({ item }) {

  function handleClick() {

    sendReport(item);

  }

 

  return (

    <figure>

      <Chart onClick={handleClick} />

    </figure>

  );

});

 

https://react.dev/reference/react/useCallback

'프로그래밍 > React' 카테고리의 다른 글

[React] memo  (1) 2023.11.29
[React] useMemo  (0) 2023.11.29
[React] useRef  (0) 2023.11.28
[React] useEffect  (0) 2023.11.28
[React] useState  (0) 2023.11.27

댓글