React useCallback 후크

React useCallback Hook은 메모화된 콜백 함수를 반환합니다.

메모화(memorization)는 값을 캐시하는 것으로 간주하여 재계산할 필요가 없습니다.

이를 통해 리소스를 많이 사용하는 함수를 분리하여 모든 렌더링에서 자동으로 실행되지 않도록 할 수 있습니다.

useCallback Hook은 의존관계 중 하나가 갱신되었을 때만 실행됩니다.

이를 통해 성능을 향상시킬 수 있습니다.

useCallbackuseMemo Hooks는 비슷합니다. 주요 차이점은 useMemo는 메모화된 값을 반환하고 useCallback은 메모화된 함수를 반환한다는 것입니다.

문제

useCallback을 사용하는 이유 중 하나는 컴포넌트의 props가 변경되지 않는 한 컴포넌트의 재렌더링을 방지하기 위함입니다.

이 예에서는 Todos 컴포넌트를 변경하지 않으면 Todos 컴포넌트가 다시 렌더링되지 않는다고 생각할 수 있습니다.

index.js
import { useState } from "react";
import ReactDOM from "react-dom";
import Todos from "./Todos";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));
Todos.js
import { memo } from "react";

const Todos = ({ todos, addTodo }) => {
  console.log("child render");
  return (
    <>
      <h2>My Todos</h2>
      {todos.map((todo, index) => {
        return <p key={index}>{todo}</p>;
      })}
      <button onClick={addTodo}>Add Todo</button>
    </>
  );
};

export default memo(Todos);

실행후 count increment 버튼을 클릭합니다.

Todos 컴포넌트는 Todos가 변경되지 않은 경우에도 다시 렌더링됩니다.

memo는 사용 중이기 때문에 카운트가 증가해도 Todos state나 addTodo 함수는 변경되지 않으므로 Todos 컴포넌트는 다시 렌더링되지 않습니다.

이것은 “referential equality”이라고 불리는 것 때문이다.

컴포넌트가 재렌더될 때마다 그 기능이 재현됩니다. 이 때문에 addToDo 함수는 실제로 변경되었습니다.

해결책

이 문제를 해결하려면 useCallback Hook을 사용하여 필요한 경우를 제외하고 함수가 재생성되지 않도록 할 수 있습니다.

useCallback Hook을 사용하여 Todos 컴포넌트가 불필요하게 재렌더되지 않도록 합니다.

index.js
import { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import Todos from "./Todos";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = useCallback(() => {
    setTodos((t) => [...t, "New Todo"]);
  }, [todos]);

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));
Todos.js
import { memo } from "react";

const Todos = ({ todos, addTodo }) => {
  console.log("child render");
  return (
    <>
      <h2>My Todos</h2>
      {todos.map((todo, index) => {
        return <p key={index}>{todo}</p>;
      })}
      <button onClick={addTodo}>Add Todo</button>
    </>
  );
};

export default memo(Todos);

이제 Todos 컴포넌트는 Todos prop가 변경될 때만 다시 렌더링됩니다.