Skip to content

React 성능 최적화 - useMemo, useCallback, React.memo

1. 공통 개념

React는 state가 바뀌면 컴포넌트를 다시 렌더링합니다. 이 과정에서 값이나 함수가 매번 새로 만들어지는 문제가 생길 수 있는데, 이를 최적화하는 도구가 바로:

  • useMemo → 값(value)을 메모
  • useCallback → 함수(function)를 메모
  • React.memo → 컴포넌트 리렌더링 방지

2. 각각의 역할

🔹 useMemo

값을 기억해둡니다. 계산 비용이 큰 연산(정렬, 필터링 등)이 매번 실행되지 않도록 최적화합니다.

jsx
const sorted = useMemo(() => items.sort(), [items]);

👉 items가 안 바뀌면 이전 정렬 결과를 재사용합니다.

🔹 useCallback

함수를 기억해둡니다. 렌더링 때마다 새 함수가 만들어져서 자식 컴포넌트가 불필요하게 리렌더링되는 걸 방지합니다.

jsx
const handleClick = useCallback(() => {
  console.log("clicked");
}, []);

👉 의존성이 안 바뀌면 같은 함수 객체를 유지합니다.

🔹 React.memo

props가 바뀌지 않으면 컴포넌트 리렌더링을 건너뜁니다.

jsx
const Child = React.memo(({ value }) => {
  console.log("렌더링됨");
  return <div>{value}</div>;
});

👉 props가 그대로면 렌더링을 생략합니다.

3. 함께 쓰는 예시 (리스트 최적화)

✅ 최적화 적용

jsx
function App() {
  const [search, setSearch] = useState("");
  const items = ["apple", "banana", "cherry"];

  // 값 메모
  const filtered = useMemo(() => {
    return items.filter((item) => item.includes(search));
  }, [items, search]);

  // 함수 메모
  const handleClick = useCallback((item) => {
    console.log(item);
  }, []);

  return (
    <>
      <input value={search} onChange={(e) => setSearch(e.target.value)} />
      {filtered.map((item) => (
        <Item key={item} item={item} onClick={handleClick} />
      ))}
    </>
  );
}

// 자식 컴포넌트 최적화
const Item = React.memo(({ item, onClick }) => {
  console.log("렌더링됨:", item);
  return <div onClick={() => onClick(item)}>{item}</div>;
});

👉 결과:

  • search 안 바뀌면 필터링 안 됨 (useMemo)
  • handleClick 같은 함수 재사용 (useCallback)
  • Item 불필요한 리렌더링 방지 (React.memo)

4. 비유

  • useMemo: 📦 "값을 상자에 담아 기억"
  • useCallback: 📞 "전화번호를 메모지에 적어두고 같은 종이 계속 보여줌"
  • React.memo: 🚪 "집에 변화 없으면 굳이 문 열고 안 들어감"

5. 안 써야 하는 상황

🚫 useMemo

  • 계산이 가벼운 경우 → 메모하는 비용이 오히려 더 비쌈
  • 예: const doubled = num * 2; 같은 단순 계산

🚫 useCallback

  • 자식 컴포넌트에 함수를 props로 넘기지 않을 때
  • 의존성 배열이 자주 바뀌어서 결국 매번 새 함수가 생성될 때
  • 아주 간단한 함수 (굳이 메모할 필요 없음)

🚫 React.memo

  • props가 자주 바뀌는 컴포넌트 → 리렌더링 막을 효과가 없음
  • 오히려 비교 비용(얕은 비교)이 추가돼서 성능 손해

✅ 정리

  • useMemo → 값 최적화 (비싼 계산만)
  • useCallback → 함수 최적화 (자식 props로 내려갈 때만)
  • React.memo → 컴포넌트 리렌더링 최적화 (props가 자주 안 바뀔 때)

👉 세 가지 다 "무조건 쓰는 게 최적화"는 아닙니다. 필요한 곳에만 선택적으로 써야 진짜 최적화가 됩니다 🚀

추가 예시

복잡한 계산 최적화

jsx
function ExpensiveComponent({ data }) {
  // 비싼 계산을 useMemo로 최적화
  const processedData = useMemo(() => {
    return data
      .filter((item) => item.active)
      .sort((a, b) => a.priority - b.priority)
      .map((item) => ({
        ...item,
        displayName: `${item.name} (${item.category})`,
      }));
  }, [data]);

  return (
    <div>
      {processedData.map((item) => (
        <div key={item.id}>{item.displayName}</div>
      ))}
    </div>
  );
}

이벤트 핸들러 최적화

jsx
function TodoList({ todos, onToggle, onDelete }) {
  // 각각의 핸들러를 useCallback으로 최적화
  const handleToggle = useCallback(
    (id) => {
      onToggle(id);
    },
    [onToggle]
  );

  const handleDelete = useCallback(
    (id) => {
      onDelete(id);
    },
    [onDelete]
  );

  return (
    <div>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

// 자식 컴포넌트도 React.memo로 최적화
const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
  return (
    <div>
      <span onClick={() => onToggle(todo.id)}>
        {todo.completed ? "✅" : "⭕"} {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>삭제</button>
    </div>
  );
});

성능 측정 팁

jsx
import { Profiler } from "react";

function onRenderCallback(id, phase, actualDuration) {
  console.log("렌더링 시간:", actualDuration);
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <YourComponent />
    </Profiler>
  );
}

이렇게 React의 성능 최적화 도구들을 적절히 활용하면
불필요한 리렌더링을 줄이고 앱의 성능을 크게 향상시킬 수 있습니다.

MIT 라이선스에 따라 릴리즈되었습니다.