useMemo와 마찬가지로 함수를 메모이제이션 하기 위해 사용되는 훅이다. 단순히 컴포넌트 안에서 함수가 의미없는 재호출을 하지 않도록 조치하는 데에만 사용되지는 않는다. 더 의미 있게 사용되는 경우를 생각해보아야 한다.

컴포넌트 내 함수를 useEffect의 dependency로 활용할 때

예시로, fetchUser라는 함수를 보자. 자바스크립트에서 함수는 객체이다. 이는 같은 함수를 가리키는 객체일지라도 서로 다른 메모리 주소를 사용하게 된다는 것이다(객체의 메모리 주소 참조). 밑의 코드에서 만약 컴포넌트가 리렌더링 된다면 fetchUser는 다른 메모리 주소값을 참조하는 함수로 바뀌게 된다. 즉 기존의 fetchUser() !== 리렌더링 후 fetchUser()이므로, useEffect의 dependency는 fetchUser가 변경되었다고 인지하고 useEffect 내의 fetchUser가 호출된다. 이는 user라는 state를 변경시키고, 컴포넌트 안의 상태가 변화했으므로 Profile 컴포넌트는 다시 렌더링된다. 그럼 또 다시 fetchUser가 변경되고... 이런 악순환이 계속된다.

import React, { useState, useEffect } from "react";

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = () =>
    fetch(`https://your-api.com/users/${userId}`)
      .then((response) => response.json())
      .then(({ user }) => user);

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

이 때 useCallback을 사용하여 문제를 해결할 수 있다. useCallback을 사용하면 fetchUser에 대한 dependency인 userId가 바뀌지 않는 이상은 fetchUser의 참조값을 그대로 유지한다.

import React, { useState, useEffect } from "react";

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = useCallback(
    () =>
      fetch(`https://your-api.com/users/${userId}`)
        .then((response) => response.json())
        .then(({ user }) => user),
    [userId]
  );

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

React.memo()와 함께 사용

자식 컴포넌트의 불필요한 렌더링을 줄이기 위해 활용할 수 있다.

내가 버튼을 클릭한 방의 Light 컴포넌트 이외의 다른 모든 방에 대한 Light 컴포넌트 함수가 호출되는 현상을 막기 위해 첫 번쨰로 Light 컴포넌트를 React.memo()를 사용해 props가 변화되기 전에는 리렌더링되지 않도록 만들었다.

이래도 똑같이 모든 방의 Light 컴포넌트가 호출되는데, 이는 SmartHome의 상태가 변화하면서(On state들) 컴포넌트가 리렌더링 될 때 계속해서 핸들링 함수들이 다른 주소를 참조하기 때문이다. 따라서 핸들링 함수들을 모두 useCallback으로 감싸서, 의존 배열에 있는 상태가 아닌 다른 상태들이 변경되어 SmartHome 컴포넌트가 리렌더링 되더라도 함수가 변하지 않도록 만들어 주어야 한다.

import "./styles.css";
import React, { useState, useCallback } from "react";

function Light({ room, on, toggle }) {
  console.log({ room, on });
  return (
    <button onClick={toggle}>
      {room} {on ? "💡" : "⬛"}
    </button>
  );
}

/* SmartHome 컴포넌트가 리렌더링 되면서 하위 Light 컴포넌트들이 리렌더링 되므로 */
Light = React.memo(Light);  

export default function SmartHome() {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);

  // const toggleMaster = () => setMasterOn(!masterOn);
  // const toggleKitchen = () => setKitchenOn(!kitchenOn);
  // const toggleBath = () => setBathOn(!bathOn);

  /* 상태 변화로 SmartHome이 리렌더링 될 때  */
  const toggleMaster = useCallback(() => setMasterOn(!masterOn), [masterOn]);
  const toggleKitchen = useCallback(() => setKitchenOn(!kitchenOn), [
    kitchenOn
  ]);
  const toggleBath = useCallback(() => setBathOn(!bathOn), [bathOn]);

  return (
    <div className="App">
      <Light room="침실" on={masterOn} toggle={toggleMaster} />
      <Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
      <Light room="욕실" on={bathOn} toggle={toggleBath} />
    </div>
  );
}

useCallback과 useMemo, React.memo의 차이점