전체 목록
ReactMedium#15

useCallback과 useMemo의 차이점과 사용 시기를 설명해주세요.

#React##최적화#메모이제이션
힌트

함수 메모이제이션과 값 메모이제이션의 차이를 생각해보세요.

정답 및 해설

useCallback과 useMemo의 차이점과 사용 시기를 설명해주세요.

useCallbackuseMemo는 모두 메모이제이션(Memoization)을 위한 React 훅으로, 불필요한 재계산이나 재렌더링을 방지합니다. useMemo는 계산 비용이 큰 값을 캐싱하고, useCallback은 함수 참조를 캐싱합니다. 두 훅 모두 의존성 배열의 값이 변경될 때만 새 값이나 함수를 생성하며, 남용하면 오히려 성능이 저하될 수 있습니다.


메모이제이션이 필요한 이유

React 컴포넌트는 상태나 props가 변경될 때마다 함수 전체가 재실행됩니다. 이 과정에서 모든 변수와 함수가 새로 생성됩니다.

function Parent() {
  const [count, setCount] = useState(0);

  // 매 렌더링마다 새로운 함수 참조 생성
  const handleClick = () => {
    console.log('클릭됨');
  };

  // 매 렌더링마다 새로운 객체 참조 생성
  const config = { color: 'blue', size: 'large' };

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>증가</button>
      {/* count가 변경될 때마다 Child도 리렌더링됨 */}
      <Child onClick={handleClick} config={config} />
    </div>
  );
}

useMemo

개념

useMemo는 계산 결과 값을 메모이제이션합니다. 의존성 배열의 값이 변경되지 않으면 이전에 계산된 값을 반환합니다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

기본 사용법

import { useState, useMemo } from 'react';

function ProductFilter({ products, searchTerm, sortBy }) {
  // searchTerm 또는 sortBy가 변경될 때만 재계산
  const filteredProducts = useMemo(() => {
    console.log('필터링 실행'); // 실제로 실행되는 시점 확인
    return products
      .filter(p => p.name.includes(searchTerm))
      .sort((a, b) => {
        if (sortBy === 'price') return a.price - b.price;
        if (sortBy === 'name') return a.name.localeCompare(b.name);
        return 0;
      });
  }, [products, searchTerm, sortBy]);

  return (
    <ul>
      {filteredProducts.map(p => (
        <li key={p.id}>{p.name} - {p.price}원</li>
      ))}
    </ul>
  );
}

복잡한 계산 최적화

function DataAnalytics({ dataset }) {
  const [selectedYear, setSelectedYear] = useState(2024);

  // 데이터셋 전체에 대한 통계 계산 (비용이 큰 연산)
  const statistics = useMemo(() => {
    const total = dataset.reduce((sum, item) => sum + item.value, 0);
    const average = total / dataset.length;
    const max = Math.max(...dataset.map(item => item.value));
    const min = Math.min(...dataset.map(item => item.value));
    const stdDev = Math.sqrt(
      dataset.reduce((sum, item) => sum + Math.pow(item.value - average, 2), 0) / dataset.length
    );
    return { total, average, max, min, stdDev };
  }, [dataset]); // dataset이 변경될 때만 재계산

  // 선택된 연도 데이터 필터링
  const yearData = useMemo(() => {
    return dataset.filter(item => item.year === selectedYear);
  }, [dataset, selectedYear]); // dataset 또는 selectedYear가 변경될 때만

  return (
    <div>
      <p>총합: {statistics.total}</p>
      <p>평균: {statistics.average.toFixed(2)}</p>
      <select onChange={e => setSelectedYear(Number(e.target.value))}>
        <option value={2024}>2024</option>
        <option value={2023}>2023</option>
      </select>
      <DataChart data={yearData} />
    </div>
  );
}

객체 동일성 유지

function ChildComponent({ config }) {
  // config 객체가 동일한 참조일 때만 useEffect 스킵
  useEffect(() => {
    applyConfig(config);
  }, [config]);

  return <div>...</div>;
}

function Parent() {
  const [theme, setTheme] = useState('dark');

  // useMemo 없이: 매 렌더링마다 새 객체 생성 → 매번 useEffect 실행
  // const config = { theme, size: 'large' };

  // useMemo 사용: theme이 변경될 때만 새 객체 생성
  const config = useMemo(() => ({
    theme,
    size: 'large',
    animations: true
  }), [theme]);

  return <ChildComponent config={config} />;
}

useCallback

개념

useCallback은 함수 자체를 메모이제이션합니다. 의존성 배열의 값이 변경되지 않으면 동일한 함수 참조를 반환합니다.

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

동등 관계

// useCallback과 useMemo의 관계
useCallback(fn, deps) === useMemo(() => fn, deps)

기본 사용법

import { useState, useCallback } from 'react';

const ExpensiveChild = React.memo(({ onSubmit, onCancel }) => {
  console.log('ExpensiveChild 렌더링');
  return (
    <div>
      <button onClick={onSubmit}>제출</button>
      <button onClick={onCancel}>취소</button>
    </div>
  );
});

function Form() {
  const [value, setValue] = useState('');
  const [count, setCount] = useState(0);

  // useCallback 없이: count가 변경될 때마다 새 함수 → React.memo 무효화
  // const handleSubmit = () => submitData(value);
  // const handleCancel = () => setValue('');

  // useCallback 사용: value가 변경될 때만 새 함수 생성
  const handleSubmit = useCallback(() => {
    submitData(value);
  }, [value]);

  const handleCancel = useCallback(() => {
    setValue('');
  }, []); // 의존성 없음 → 항상 동일한 함수 참조

  return (
    <div>
      <input value={value} onChange={e => setValue(e.target.value)} />
      <button onClick={() => setCount(c => c + 1)}>카운트 {count}</button>
      {/* count가 변경되어도 ExpensiveChild는 리렌더링되지 않음 */}
      <ExpensiveChild onSubmit={handleSubmit} onCancel={handleCancel} />
    </div>
  );
}

커스텀 훅과 useCallback

function useSearch(items) {
  const [query, setQuery] = useState('');

  // 외부에 노출되는 함수를 useCallback으로 안정적으로 유지
  const search = useCallback((newQuery) => {
    setQuery(newQuery);
  }, []); // 의존성 없음, 항상 동일한 참조

  const results = useMemo(() =>
    items.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    ),
    [items, query]
  );

  return { results, search, query };
}

function SearchPage({ products }) {
  const { results, search } = useSearch(products);

  return (
    <div>
      <SearchBar onSearch={search} /> {/* search 참조가 안정적 */}
      <ResultList items={results} />
    </div>
  );
}

useEffect 의존성과의 관계

function DataFetcher({ userId }) {
  const [data, setData] = useState(null);

  // fetchUser를 useCallback으로 감싸지 않으면
  // 매 렌더링마다 새 함수 → useEffect 매번 실행 → 무한 루프 위험
  const fetchUser = useCallback(async () => {
    const response = await fetch(`/api/users/${userId}`);
    const json = await response.json();
    setData(json);
  }, [userId]); // userId가 변경될 때만 새 함수

  useEffect(() => {
    fetchUser();
  }, [fetchUser]); // fetchUser가 안정적이면 불필요한 재실행 없음

  return <div>{data?.name}</div>;
}

useMemo vs useCallback 비교

function Example({ items, onSelect }) {
  // useMemo: 값(배열)을 반환
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => a.name.localeCompare(b.name));
  }, [items]);

  // useCallback: 함수를 반환
  const handleSelect = useCallback((id) => {
    onSelect(id);
    console.log('선택됨:', id);
  }, [onSelect]);

  // 동일한 효과 (useMemo로 useCallback 구현)
  const handleSelectAlt = useMemo(() => {
    return (id) => {
      onSelect(id);
      console.log('선택됨:', id);
    };
  }, [onSelect]);

  return (
    <ul>
      {sortedItems.map(item => (
        <li key={item.id} onClick={() => handleSelect(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

언제 사용해야 하는가

사용해야 할 때

// 1. React.memo와 함께: 자식 컴포넌트에 전달하는 함수/객체
const MemoizedChild = React.memo(({ onClick, config }) => <div>...</div>);

function Parent() {
  const onClick = useCallback(() => { /* ... */ }, []);
  const config = useMemo(() => ({ key: 'value' }), []);
  return <MemoizedChild onClick={onClick} config={config} />;
}

// 2. useEffect 의존성에 함수가 포함될 때
function DataLoader({ id }) {
  const loadData = useCallback(() => fetch(`/api/${id}`), [id]);
  useEffect(() => { loadData(); }, [loadData]);
}

// 3. 계산 비용이 실제로 큰 경우 (1ms 이상)
const result = useMemo(() => heavyComputation(largeDataset), [largeDataset]);

사용하지 말아야 할 때

// 1. 단순한 값 계산 - useMemo 오버헤드가 더 클 수 있음
const doubled = useMemo(() => count * 2, [count]); // 불필요
const doubled2 = count * 2; // 이것으로 충분

// 2. 자식에게 전달하지 않는 함수
const handleLocalClick = useCallback(() => {
  setCount(c => c + 1); // 이 컴포넌트 내부에서만 사용
}, []); // useCallback이 불필요

// 3. React.memo 없는 자식에게 전달
// React.memo가 없으면 부모가 리렌더링될 때 자식도 항상 리렌더링됨
// → 안정적인 함수 참조가 의미 없음
function RegularChild({ onClick }) {
  return <button onClick={onClick}>버튼</button>;
}
// React.memo 없이는 useCallback이 도움이 되지 않음

성능 측정으로 판단하기

import { useMemo } from 'react';

// 실제 성능 측정 후 결정
function measurePerformance() {
  console.time('계산');
  const result = expensiveOperation();
  console.timeEnd('계산');
  // 1ms 미만이면 useMemo 불필요
  // 1ms 이상이면 useMemo 고려
}

// React DevTools Profiler로 리렌더링 확인 후 최적화
// "measure first, optimize second" 원칙

요약 비교표

항목useMemouseCallback
반환값계산된 값 (any)함수
목적비싼 계산 결과 캐싱함수 참조 안정성
동등 표현useMemo(() => value, deps)useMemo(() => fn, deps)
주요 사용 사례필터링/정렬/통계 계산React.memo 자식에 전달하는 함수
useEffect와 연관의존성으로 전달하는 객체의존성으로 전달하는 함수
남용 시 문제불필요한 메모리 사용불필요한 메모리 사용

핵심 정리

  • useMemouseCallback은 성능 최적화 도구이지만, 항상 최적화가 필요한 것은 아닙니다.
  • React.memo와 함께 사용할 때 가장 효과적이며, 그렇지 않으면 의미가 없는 경우가 많습니다.
  • "먼저 측정하고, 그다음에 최적화" 원칙을 따르고 React DevTools Profiler를 활용합니다.
  • 두 훅 모두 메모리를 추가로 사용하므로, 불필요하게 남용하면 메모리 낭비와 코드 복잡성만 증가합니다.