[React] useCallback

0

React

목록 보기
6/6

useCallback

useCallback은 리렌더링 사이에 함수 정의를 캐시하는 React Hook입니다.

const cachedFn = useCallback(fn, dependencies)

Reference

useCallback(fn, dependencies)

리렌더링 사이에 함수 정의를 캐시하기 위해 컴포넌트의 최상위 레벨에서 useCallback을 호출하세요.

import { useCallback } from 'react';

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

Parameters

  • fn: 캐시하려는 함수 값입니다. 임의의 인수를 받고 임의의 값을 반환할 수 있습니다. React는 초기 렌더링 중에 해당 함수를 반환할 것입니다(실행하지는 않습니다!). 다음 렌더링에서는 dependencies가 마지막 렌더링 이후 변경되지 않은 경우에는 동일한 함수를 다시 제공할 것입니다. 그렇지 않으면 현재 렌더링 중에 전달한 함수를 제공하고, 나중에 재사용할 수 있도록 저장합니다. React는 함수를 호출하지 않습니다. 함수는 호출 여부와 시점을 결정하기 위해 반환됩니다.

  • dependencies: fn 코드 내에서 참조하는 모든 반응형 값의 목록입니다. 반응형 값은 props, state, 그리고 컴포넌트 본문에서 직접 선언된 모든 변수와 함수를 포함합니다. React에서 린터를 사용하는 경우, 모든 반응형 값이 올바르게 의존성으로 지정되었는지 확인할 것입니다. 의존성 목록은 항상 동일한 개수의 항목을 가져야 하며 [dep1, dep2, dep3]와 같이 인라인으로 작성되어야 합니다. React는 Object.is 비교 알고리즘을 사용하여 각 의존성을 이전 값과 비교합니다.

Returns

초기 렌더링에서 useCallback은 전달한 fn 함수를 반환합니다.

다음 렌더링에서는, 이전 렌더링에서 저장된 fn 함수를 반환할 수도 있습니다(의존성이 변경되지 않은 경우), 또는 현재 렌더링 중에 전달한 fn 함수를 반환할 수도 있습니다.

Caveats

  • useCallback는 Hook이므로 컴포넌트의 최상위 레벨 또는 자체 Hook에서만 호출할 수 있습니다. 반복문이나 조건문 내에서 호출할 수 없습니다. 이를 필요로 하는 경우, 새로운 컴포넌트를 추출하고 상태를 해당 컴포넌트로 이동시킬 수 있습니다.

  • 리액트는 특정한 이유가 없는 한 캐시된 함수를 버리지 않습니다. 예를 들어, 개발 환경에서는 컴포넌트 파일을 편집할 때 캐시를 버립니다. 개발 환경과 프로덕션 환경에서 모두, 컴포넌트가 초기 마운트 중에 중단되면 캐시를 버립니다. 또한, 리액트는 미래에 캐시를 버릴 수 있는 기능을 추가할 수도 있습니다. 예를 들어, 가상화된 리스트에 대한 내장 지원이 추가된다면, 가상화된 테이블 뷰포트에서 스크롤되는 항목에 대해 캐시를 버릴 수 있습니다. 이는 useCallback을 성능 최적화 목적으로 사용한다면 기대하는 동작과 일치할 것입니다. 그렇지 않을 경우, 상태 변수나 ref가 더 적합할 수 있습니다.


Usage

Skipping re-rendering of components

렌더링 성능을 최적화할 때, 때로는 자식 컴포넌트에 전달하는 함수를 캐시해야 할 수 있습니다. 먼저, 이를 수행하는 구문에 대해 살펴보고, 어떤 경우에 유용한지 알아보겠습니다.

컴포넌트의 재렌더링 사이에서 함수를 캐시하기 위해, 해당 함수 정의를 useCallback Hook으로 감싸세요.

import { useCallback } from 'react';

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

useCallback에 두 가지를 전달해야 합니다.

  1. 재렌더링 사이에 캐시하고자 하는 함수 정의.
  2. 함수 내에서 사용되는 컴포넌트 내의 모든 값들을 포함하는 의존성 목록.

초기 렌더링에서 useCallback에서 반환하는 함수는 전달한 함수와 동일한 함수입니다.

다음 렌더링에서 React는 useCallback의 의존성과 이전 렌더링에서 전달한 의존성을 비교합니다. 의존성이 변경되지 않았다면(Object.is로 비교), useCallback은 이전과 동일한 함수를 반환합니다. 그렇지 않은 경우에는 useCallback은 현재 렌더링에서 전달한 함수를 반환합니다.

다시 말해, useCallback은 의존성이 변경되기 전까지 함수를 재렌더링 사이에서 캐시합니다.

예제를 통해 언제 이것이 유용한지 살펴보겠습니다.

ProductPage에서 handleSubmit 함수를 ShippingForm 컴포넌트로 전달하고 있다고 가정해봅시다.

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

주어진 상황에서 theme prop을 토글하면 앱이 잠시 멈추는 것을 관찰하였습니다. 그러나 <ShippingForm />을 JSX에서 제거하면 빠르게 동작한다는 사실을 알 수 있었습니다. 이는 ShippingForm 컴포넌트를 최적화해보는 가치가 있다는 것을 알려줍니다.

기본적으로 컴포넌트가 재렌더링되면 React는 재귀적으로 모든 자식 컴포넌트를 재렌더링합니다. 따라서 ProductPage가 다른 theme으로 재렌더링될 때 ShippingForm 컴포넌트도 재렌더링됩니다. 이는 재렌더링이 계산 비용이 많이 들지 않는 컴포넌트에 대해서는 괜찮습니다. 그러나 재렌더링이 느린 것을 확인한 경우, ShippingForm 컴포넌트가 마지막 렌더링과 동일한 프롭을 가진 경우 재렌더링을 건너뛸 수 있도록 memo로 감싸주는 것이 좋습니다.

import { memo } from 'react';

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

이 변경으로 ShippingForm은 마지막 렌더링과 동일한 모든 프롭을 가질 때 재렌더링을 건너뛰게 됩니다. 이때 함수 캐싱이 중요해집니다! handleSubmituseCallback 없이 정의한 경우를 가정해봅시다.

function ProductPage({ productId, referrer, theme }) {
  // 매번 테마가 변경될 때마다 이는 다른 함수가 됩니다.
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      {/* 그렇기 때문에 ShippingForm의 프롭은 결코 동일하지 않을 것이며, 매번 재렌더링될 것입니다. */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

JavaScript에서는 function () {} 또는 () => {}와 같은 함수는 항상 새로운 함수를 생성합니다. 이는 {} 객체 리터럴이 항상 새로운 객체를 생성하는 것과 유사합니다. 일반적으로 이는 문제가 되지 않을 수 있지만, 이는 ShippingForm 프롭이 결코 동일하지 않을 것이므로 memo 최적화가 작동하지 않게 됩니다. 이럴 때 useCallback이 유용하게 사용됩니다.

function ProductPage({ productId, referrer, theme }) {
  // React에게 함수를 재렌더링 사이에 캐시하도록 알려주세요.
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // 따라서 이러한 의존성이 변경되지 않는 한...

  return (
    <div className={theme}>
      {/* ShippingForm은 동일한 프롭을 받고 재렌더링을 건너뛸 수 있게 됩니다. */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

handleSubmituseCallback으로 감싸면 (의존성이 변경되기 전까지) 재렌더링 사이에서 항상 동일한 함수가 됩니다. 특정한 이유가 없는 한 함수를 useCallback으로 감싸지 않아도 됩니다. 이 예제에서는 memo로 감싼 컴포넌트에 전달하기 때문에 함수를 useCallback으로 감싸는 것이 필요합니다. 이를 통해 함수가 재렌더링을 건너뛸 수 있게 됩니다. useCallback이 필요한 다른 이유는 이 페이지에서 자세히 설명되어 있습니다.

📝 Note


useCallback은 성능 최적화를 위해 사용해야 합니다. 만약 useCallback 없이 코드가 작동하지 않는다면, 먼저 기본적인 문제를 찾고 해결해야 합니다. 그 후에 useCallback을 다시 추가할 수 있습니다.


Updating state from a memoized callback

가끔씩, 메모이즈된 콜백을 기반으로 이전 상태에 따라 상태를 업데이트해야 할 수도 있습니다.

다음 handleAddTodo 함수는 todos를 의존성으로 지정하는데, 이는 todos를 기반으로 다음 todos를 계산하기 때문입니다.

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 => [...todos, newTodo]을 React에 전달합니다.


Preventing an Effect from firing too often

가끔씩, Effect 내부에서 함수를 호출하고 싶을 수도 있습니다.

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

이렇게 하면 문제가 발생합니다. 모든 반응형 값은 Effect의 의존성으로 선언되어야 합니다. 그러나 createOptions를 의존성으로 선언하면 Effect가 채팅방에 지속적으로 재연결됩니다.

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🔴 문제: 이 의존성은 매 렌더링마다 변경됩니다.
  // ...

이를 해결하기 위해, Effect 내부에서 호출해야 하는 함수를 useCallback으로 감싸면 됩니다.

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이 변경될 때에만 변경되는 경우입니다.
  // ...

이렇게 하면 roomId가 동일한 경우 createOptions 함수가 재렌더링 사이에서 동일하게 유지됩니다. 그러나 함수 의존성의 필요성을 제거하는 것이 더 좋습니다. 함수를 효과 내부로 이동시킵니다.

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이 변경될 때에만 변경되는 경우입니다.
  // ...

이제 코드가 더 간단하고 useCallback이 필요하지 않습니다.


Optimizing a custom Hook

만약 사용자 정의 Hook을 작성하는 경우, 해당 Hook이 반환하는 모든 함수를 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,
  };
}

이렇게 하면 Hook을 사용하는 사람들이 필요에 따라 자신들의 코드를 최적화할 수 있습니다.


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

profile
지치지 않는 백엔드 개발자 김성주입니다 :)

0개의 댓글