React

[ React ] React Hooks - useCallback

yebeen 2024. 8. 19. 14:55

 

 

 

useCallback Hook이란 ? 

useMemo와 마찬가지로 메모제이션 기법을 이용한 Hook으로 함수의 재사용을 위한 Hook이다.

리렌더링 간에 함수 정의를 캐싱해준다.

 

아래의 코드를 보면 첫번째 인자는 리렌더링 간에 캐싱할 함수 정의이고 두번째 인자는 반응형 값들이 들어간다.

여기서 반응형 값이 변경(의존성이 변경)되었을 때만 다시 메모제이션된 버전이 변경된다.

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

 

 

 

또 다른 예시로 이 코드의 경우 x,y값이 바뀌지않는 경우

기존 함수를 계속 반환하고 다음 렌더링 때 기존 함수를 다시 사용한다.

만약 이어지는 렌더링에서 의존성을 비교했을 때 전과 달라지면 이번 렌더링에서 전달한 함수를 반환한다.

import React, { useCallback } from "react";

function Calculator({x, y}){

	const add = useCallback(() => x + y, [x, y]);

	return 
  <>
      <div>
      	{add()}
      </div>
  </>;
}

 

 

 

React는 js문법을 따라가고 js에서 함수는 객체이다.

객체는 메모리에 저장할 때, 값이 아닌 주소를 저장하기 때문에

반환 값이 같더라도 메모리 주소가 다르기에 같다고 볼 수 없다.

즉 새로 만들어 호출된 함수는 기존의 함수와 같은 함수가 아니다.

 

그러나 useCallback을 이용해 함수 자체를 저장해서

재사용 시 메모리 주소 값을 저장했다가 재사용하는 것과 같다고 볼 수 있다.

→ 참조 동등성에 의존한다.

 

 

 

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

사용하는 경우

1. react 컴포넌트 함수 내에서 다른 함수의 인자로 함수를 넘길 때

 

2. 자식 컴포넌트의 prop으로 함수를 전달할 때

→ 의존성이 변하기 전까진 함수를 재사용하기때문에 재실행이 일어나는 횟수가 줄어들게 되고

    이로 인해 예상치 못한 성능 문제를 방지할 수 있다.

 

    ex_handleSubmit 함수를 ProductPage에서 ShippingForm 컴포넌트로 전달한다고 가정한다.

   기본적으로, 컴포넌트가 리렌더링할 때 React는 이것의 모든 자식을 재귀적으로 재렌더링한다.

   그렇기에 ProductPage가 다른 theme 값으로 리렌더링 할 때, ShippingForm 컴포넌트 또한 리렌더링된다.

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


  만약 리렌더링이 느려지는 경우 ShippingForm 을 memo로 감싸면

  마지막 렌더링과 동일한 props일 때 리렌더링을 건너뛰도록 할 수 있다.

import { memo } from 'react';

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

 

 

문제는 js에선 function () {} 나 () => {}은 항상 다른 함수를 생성한다는 것이다.

그렇기때문에 ShippingForm props는 절대 같을 수 없고 memo 최적화는 동작하지않는다.

이럴 때 ! useCallback을 활용하는 것이다.

function ProductPage({ productId, referrer, theme }) {
  // theme이 바뀔때마다 다른 함수가 될 것입니다...
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      {/* ... 그래서 ShippingForm의 props는 같은 값이 아니므로 매번 리렌더링 할 것입니다.*/}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

 

 

용법 2. Effect가 너무 자주 실행되는 것을 방지하기 

가끔은 useEffect안에서 함수를 호출해야 할 수도 있다.

아래의 코드의 경우  createOptions를 의존성으로 선언하면 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]); // 🔴 문제점: 이 의존성은 매 렌더링마다 변경됩니다.
  // ...

 

 

이를 해결하기 위해  useEffect에서 호출하려는 함수를 useCallback으로 감쌀 수 있다.

이렇게 되면 리렌더링 간에 roomId가 같다면 createOptions 함수는 같다는 것을 보장한다.

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가 변경될 때만 변경됩니다.
  // ...

 

 

지만, 함수 의존성을 제거하는 것이 더 좋다. 함수를 Effect 안으로 이동시키는게 best...

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은 Hook이기에 컴포넌트의 최상위 레벨 또는 커스텀 Hook에서만 호출할 수 있다.

반복문이나 조건문 내에서 호출 불가능

    → 이러한 작업이 필요하면 새로운 컴포넌트로 분리해 state를 새 컴포넌트로 옮겨야한다.

 

불가능한 코드

function ReportList({ items }) {
  return (
    <article>
      {items.map(item => {
        // 🔴 이렇게 반복문 안에서 useCallback을 호출할 수 없습니다.
        const handleClick = useCallback(() => {
          sendReport(item)
        }, [item]);

        return (
          <figure key={item.id}>
            <Chart onClick={handleClick} />
          </figure>
        );
      })}
    </article>
  );
}

 

 

가능한 코드 1.  새로운 컴포넌트로 분리

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

function Report({ item }) {
  // ✅ useCallback을 최상위 레벨에서 호출하세요
  const handleClick = useCallback(() => {
    sendReport(item)
  }, [item]);

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

 

 

가능한 코드 2.  memo 사용

→ 함수가 아닌 item 변수이기에 memo만으로 가능하다.

function ReportList({ items }) {
  // ...
}

const Report = memo(function Report({ item }) {
  function handleClick() {
    sendReport(item);
  }

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

 

 

useMemo VS useCallback VS memo

useMemo

- 값이 계산에 의해 의존하며 계산이 자주 변경되지 않거나 복잡한 경우에 사용

- 불필요한 재계산을 피하는 것

 

useCallback

- 함수가 종속성 배열 내의 값이 변경되지 않는한 동일한 참조를 유지하도록 하기위해 사용

- 자식 컴포넌트에 함수를 props로 전달할 때  유용 [ 그렇지않으면 부모가 렌더링될 때 자식도 함께 렌더링 ] 

 

memo

memo는 컴포넌트의 불필요한 재렌더링을 방지하기 위해 사용

     → 해당 컴포넌트의 props가 변경되지 않으면 그 컴포넌트는 다시 렌더링되지 않는다.

 

 

 

 

 

참조

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

https://velog.io/@ko9612/React-useCallback