icon안동민 개발노트

useCallback과 useMemo 기초


 useCallback과 useMemo는 React의 성능 최적화를 위한 훅으로, 각각 함수와 값의 메모이제이션을 제공합니다.

 이 훅들을 적절히 사용하면 불필요한 리렌더링을 방지하고 애플리케이션의 성능을 향상시킬 수 있습니다.

useCallback

 useCallback은 콜백 함수를 메모이제이션하는 데 사용됩니다.

 이는 불필요한 리렌더링을 방지하고 자식 컴포넌트에 안정적인 함수 참조를 전달하는 데 유용합니다.

 기본 사용법

import React, { useCallback, useState } from 'react';
 
function ParentComponent() {
  const [count, setCount] = useState(0);
 
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // 의존성 배열이 비어있으므로 함수는 한 번만 생성됩니다.
 
  return (
    <div>
      <ChildComponent onClick={handleClick} />
      <p>Count: {count}</p>
    </div>
  );
}

 이 예제에서 handleClick 함수는 컴포넌트가 리렌더링되어도 새로 생성되지 않습니다.

useMemo

 useMemo는 계산 비용이 큰 값을 메모이제이션하는 데 사용됩니다.

 이는 불필요한 재계산을 방지하여 성능을 최적화합니다.

 기본 사용법

import React, { useMemo, useState } from 'react';
 
function ExpensiveCalculation({ list }) {
  const [count, setCount] = useState(0);
 
  const expensiveResult = useMemo(() => {
    return list.reduce((acc, item) => acc + item, 0);
  }, [list]); // list가 변경될 때만 재계산됩니다.
 
  return (
    <div>
      <p>Expensive Calculation Result: {expensiveResult}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count ({count})</button>
    </div>
  );
}

 이 예제에서 expensiveResultlist가 변경될 때만 재계산됩니다.

useCallback과 useMemo의 차이점

  1. useCallback은 함수 자체를 메모이제이션합니다.
  2. useMemo는 함수의 결과 값을 메모이제이션합니다.
// useCallback
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
 
// useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

의존성 배열의 역할

 의존성 배열은 메모이제이션된 값이나 함수가 언제 재생성되어야 하는지를 React에게 알려줍니다.

  • 빈 배열([])은 컴포넌트가 마운트될 때만 함수나 값을 생성합니다.
  • 의존성이 포함된 배열([dep1, dep2])은 해당 의존성이 변경될 때마다 함수나 값을 재생성합니다.
  • 배열이 없으면 매 렌더링마다 함수나 값이 재생성됩니다.

적절한 사용 상황

 useCallback

  1. 자식 컴포넌트에 함수를 prop으로 전달할 때
  2. useEffect의 의존성 배열에 함수를 포함시킬 때
function SearchComponent({ term }) {
  const search = useCallback(() => {
    // 검색 로직
  }, [term]);
 
  useEffect(() => {
    search();
  }, [search]);
 
  return <ChildComponent onSearch={search} />;
}

 useMemo

  1. 계산 비용이 큰 연산을 수행할 때
  2. 객체를 생성하여 자식 컴포넌트에 전달할 때
function DataProcessor({ data }) {
  const processedData = useMemo(() => {
    return data.map(item => /* 복잡한 처리 */);
  }, [data]);
 
  return <ChildComponent data={processedData} />;
}

성능 최적화 전략

  1. 선택적 사용 : 모든 함수나 값에 useCallback과 useMemo를 적용하는 것은 오히려 성능을 저하시킬 수 있습니다. 실제로 성능 향상이 필요한 부분에만 적용하세요.
  2. 의존성 배열 최적화 : 의존성 배열에는 필요한 값만 포함시켜 불필요한 재생성을 방지하세요.
  3. 컴포넌트 분할 : 큰 컴포넌트를 작은 단위로 분할하여 리렌더링 범위를 줄이는 것도 좋은 전략입니다.
  4. React.memo와 함께 사용 : useCallback과 useMemo는 React.memo와 함께 사용할 때 더 효과적입니다.
const MemoizedChild = React.memo(function ChildComponent({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
});
 
function ParentComponent() {
  const handleClick = useCallback(() => {
    // 클릭 처리 로직
  }, []);
 
  return <MemoizedChild onClick={handleClick} />;
}

과도한 사용의 위험성

  1. 코드 복잡성 증가 : 과도한 사용은 코드를 더 복잡하게 만들고 가독성을 떨어뜨릴 수 있습니다.
  2. 성능 오버헤드 : 메모이제이션 자체도 비용이 들기 때문에, 단순한 연산이나 함수에 적용하면 오히려 성능이 저하될 수 있습니다.
  3. 버그 발생 가능성 의존성 배열을 잘못 관리하면 예상치 못한 버그가 발생할 수 있습니다.

성능에 미치는 영향

 useCallback과 useMemo는 메모리 사용량을 증가시키는 대신 CPU 사용량을 줄이는 트레이드오프를 제공합니다.

 따라서 사용 시 다음 사항을 고려해야 합니다.

  1. 측정 가능한 성능 향상 : 실제로 성능 향상이 있는지 React DevTools Profiler 등을 사용하여 측정하세요.
  2. 렌더링 빈도 : 자주 리렌더링되는 컴포넌트에서 더 효과적입니다.
  3. 계산 복잡도 : 복잡한 계산이나 큰 객체를 다룰 때 더 유용합니다.

 useCallback과 useMemo는 React 애플리케이션의 성능을 최적화하는 강력한 도구입니다. 그러나 이들의 효과적인 사용을 위해서는 애플리케이션의 특성과 성능 병목 지점을 정확히 파악해야 합니다. 무분별한 사용은 오히려 성능 저하와 코드 복잡성 증가를 초래할 수 있으므로, 항상 측정 가능한 성능 문제에 대한 해결책으로 접근해야 합니다.

 또한, 이러한 최적화 기법들은 React의 Concurrent Mode와 같은 미래의 기능들과 잘 작동하도록 설계되었습니다. 따라서 적절히 사용된 useCallback과 useMemo는 현재의 성능 최적화뿐만 아니라 미래의 React 기능들과의 호환성도 보장할 수 있습니다.