전체 목록
ReactMedium#92

useCallback과 useMemo의 차이점과 각각의 적절한 사용 시나리오를 설명해주세요.

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

메모이제이션하는 대상(함수 vs 값)의 차이를 생각해보세요.

정답 및 해설

useCallback과 useMemo의 차이점과 각각의 적절한 사용 시나리오를 설명해주세요.

useCallbackuseMemo는 모두 **메모이제이션(memoization)**을 위한 React 훅으로, 불필요한 재계산이나 리렌더링을 방지합니다. useCallback함수 자체를 메모이제이션하고, useMemo함수의 실행 결과 값을 메모이제이션한다는 점이 핵심 차이입니다.

useCallback

기본 개념

// 기본 문법
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]  // 의존성 배열: a 또는 b가 변경될 때만 새 함수 생성
);

// useCallback(fn, deps) === useMemo(() => fn, deps)
// 사실상 useMemo의 특수한 형태

왜 필요한가?

// ❌ useCallback 없이 - 매 렌더링마다 새 함수 참조 생성
function ParentComponent() {
  const [count, setCount] = useState(0);

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

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>카운트: {count}</button>
      {/* count가 바뀔 때마다 ParentComponent 리렌더링 */}
      {/* → handleClick이 새 참조로 변경 */}
      {/* → ChildComponent도 리렌더링 (불필요) */}
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent 렌더링');
  return <button onClick={onClick}>자식 버튼</button>;
});
// ✅ useCallback 사용 - 함수 참조 유지
function ParentComponent() {
  const [count, setCount] = useState(0);

  // count가 변경되어도 handleClick의 참조는 유지됨
  const handleClick = useCallback(() => {
    console.log('클릭!');
  }, []);  // 의존성 없음 - 컴포넌트 생애주기 동안 동일한 참조

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>카운트: {count}</button>
      {/* count 변경 → ParentComponent 리렌더링 */}
      {/* → handleClick 참조 동일 */}
      {/* → ChildComponent 리렌더링 스킵! */}
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

의존성 배열 관리

function SearchComponent({ userId }) {
  const [query, setQuery] = useState('');

  // userId나 query가 변경될 때만 새 함수 생성
  const handleSearch = useCallback(async () => {
    const results = await searchAPI(userId, query);
    setResults(results);
  }, [userId, query]);  // 사용된 외부 값은 모두 의존성에 추가

  // ❌ 잘못된 예: 의존성 누락
  const handleSearch = useCallback(async () => {
    const results = await searchAPI(userId, query);  // query 사용
    setResults(results);
  }, [userId]);  // query 누락 → stale closure 버그!

  return (
    <SearchInput onSearch={handleSearch} />
  );
}

useMemo

기본 개념

// 기본 문법
const memoizedValue = useMemo(
  () => expensiveCalculation(a, b),  // 계산 함수
  [a, b]  // 의존성 배열: a 또는 b가 변경될 때만 재계산
);

비용이 큰 연산에 사용

function ProductList({ products, filter, sortOrder }) {
  // ❌ useMemo 없이 - 렌더링마다 필터링/정렬 재실행
  const processedProducts = products
    .filter(p => p.category === filter)
    .sort((a, b) => sortOrder === 'asc' ? a.price - b.price : b.price - a.price);

  // ✅ useMemo 사용 - products, filter, sortOrder 중 하나가 변경될 때만 재계산
  const processedProducts = useMemo(() => {
    console.log('필터링/정렬 실행');  // 실제로 필요할 때만 실행
    return products
      .filter(p => p.category === filter)
      .sort((a, b) => sortOrder === 'asc' ? a.price - b.price : b.price - a.price);
  }, [products, filter, sortOrder]);

  return (
    <ul>
      {processedProducts.map(product => (
        <ProductItem key={product.id} product={product} />
      ))}
    </ul>
  );
}

객체/배열 참조 안정화

// useMemo로 객체 참조 안정화
function UserDashboard({ userId, theme }) {
  // ❌ 매 렌더링마다 새로운 객체 생성 → 하위 컴포넌트가 불필요하게 리렌더링
  const config = {
    userId,
    theme,
    timestamp: Date.now()
  };

  // ✅ theme나 userId가 변경될 때만 새 객체 생성
  const config = useMemo(() => ({
    userId,
    theme,
    timestamp: Date.now()
  }), [userId, theme]);

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

useCallback vs useMemo 비교

// 둘은 사실 같은 메커니즘
const memoizedFn = useCallback(() => doSomething(a), [a]);

// 위와 동일한 동작
const memoizedFn = useMemo(() => () => doSomething(a), [a]);

// useCallback: 함수 자체를 반환
// useMemo: 함수 실행 결과를 반환
항목useCallbackuseMemo
메모이제이션 대상함수 자체함수 실행 결과 값
반환값메모이제이션된 함수계산된 값
주 사용 목적자식 컴포넌트에 안정적인 함수 참조 전달비용이 큰 연산 결과 캐싱
의존성 변경 시새 함수 생성값 재계산

실제 사용 시나리오

시나리오 1: 커스텀 훅에서 함수 안정화

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

  // 매 렌더링마다 새 함수가 생성되지 않도록 메모이제이션
  const increment = useCallback(() => setCount(c => c + 1), []);
  const decrement = useCallback(() => setCount(c => c - 1), []);
  const reset = useCallback(() => setCount(initialValue), [initialValue]);

  return { count, increment, decrement, reset };
}

// 이 훅의 사용자는 increment 참조가 안정적임을 보장받음
function MyComponent() {
  const { count, increment } = useCounter(0);

  // increment를 useEffect 의존성에 넣어도 무한루프 없음
  useEffect(() => {
    const timer = setInterval(increment, 1000);
    return () => clearInterval(timer);
  }, [increment]);  // increment 참조가 안정적이므로 안전
}

시나리오 2: 데이터 변환 메모이제이션

function AnalyticsDashboard({ rawData, dateRange }) {
  // 비용이 큰 데이터 처리 (집계, 통계 계산)
  const chartData = useMemo(() => {
    return rawData
      .filter(d => isInRange(d.date, dateRange))
      .reduce((acc, item) => {
        const date = formatDate(item.date);
        acc[date] = (acc[date] || 0) + item.value;
        return acc;
      }, {});
  }, [rawData, dateRange]);

  // 파생 통계 계산
  const statistics = useMemo(() => {
    const values = Object.values(chartData);
    return {
      total: values.reduce((a, b) => a + b, 0),
      average: values.reduce((a, b) => a + b, 0) / values.length,
      max: Math.max(...values),
      min: Math.min(...values),
    };
  }, [chartData]);  // chartData가 변경될 때만 재계산

  return (
    <div>
      <Chart data={chartData} />
      <StatsSummary stats={statistics} />
    </div>
  );
}

시나리오 3: Context와 함께 사용

// Context value 안정화
const ThemeContext = React.createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  // ❌ 매 렌더링마다 새 객체 생성 → 모든 Context 소비자 리렌더링
  const value = { theme, setTheme };

  // ✅ theme가 변경될 때만 새 객체 생성
  const value = useMemo(
    () => ({ theme, setTheme }),
    [theme]  // setTheme은 useState의 setter라 항상 동일한 참조
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

과도한 사용의 위험성

// ❌ 불필요한 useMemo - 단순 값에는 오버헤드만 증가
const sum = useMemo(() => a + b, [a, b]);
// → 그냥 const sum = a + b; 가 더 빠름

// ❌ 불필요한 useCallback - 자식이 memo가 아니면 의미 없음
function Parent() {
  const handleClick = useCallback(() => {}, []);
  // ChildComponent가 React.memo로 감싸져 있지 않으면 무의미
  return <ChildComponent onClick={handleClick} />;
}

// 메모이제이션 자체도 비용이 있음:
// - 이전 값 저장을 위한 메모리 사용
// - 의존성 비교 연산 수행
// - 코드 복잡도 증가

언제 사용해야 할까?

useCallback을 써야 할 때:
✅ React.memo로 감싼 자식 컴포넌트에 함수를 prop으로 전달할 때
✅ useEffect, useMemo 등의 의존성 배열에 함수를 포함해야 할 때
✅ 비용이 큰 외부 API 호출을 트리거하는 함수
✅ 커스텀 훅에서 안정적인 API 제공 시

useMemo를 써야 할 때:
✅ 대용량 데이터 필터링/정렬/변환 시
✅ 복잡한 수학적 계산 시
✅ React.memo 자식에 전달할 객체/배열의 참조 안정화
✅ Context value 안정화

사용하지 않아도 될 때:
❌ 단순한 계산 (덧셈, 문자열 연결 등)
❌ React.memo로 감싸지 않은 자식에 전달하는 함수
❌ 렌더링 성능 문제가 실제로 발생하지 않은 경우

React DevTools Profiler로 확인

// 성능 문제가 의심될 때 먼저 프로파일링 후 최적화
// React DevTools → Profiler 탭 → Record

// 컴포넌트가 리렌더링된 이유 확인
// "Why did this render?" 기능 활용

정리

항목useCallbackuseMemo
목적함수 참조 안정화계산 결과 캐싱
반환메모이제이션된 함수계산된 값 (any type)
주 사용처memo 자식에 콜백 전달비용이 큰 연산
함수 실행 여부직접 실행 안 함 (참조만)즉시 실행하여 결과 저장
동등 표현useCallback(fn, deps)useMemo(() => fn, deps)
남용 시 영향메모리 사용 증가메모리 사용 증가

핵심: 두 훅 모두 "먼저 문제가 있는지 확인하고, 있을 때만 최적화"하는 원칙을 지켜야 합니다. React DevTools Profiler로 실제 렌더링 성능 문제를 확인한 후 적용하세요.