React 성능 튜닝: memo와 Suspense

컴포넌트 리렌더 원인을 최소화하고 사용자 체감 성능을 높이기 위해 메모화와 Suspense 경계 설계를 함께 적용합니다.

1. 불필요한 리렌더 줄이기

React.memo는 동일한 props에서 동일 출력을 보장할 때 유효하며 의존성이 낮은 프리미티브 props에 특히 효과적입니다.

useMemo는 연산 비용이 큰 계산 결과를 캐싱하고 useCallback은 함수 참조 동일성을 보장해 자식의 불필요한 리렌더를 방지합니다.


// Parent.jsx
const Parent = ({ data }) => {
  const expensive = useMemo(() => heavyCalc(data.items), [data.items]);
  const onSelect = useCallback((id) => doSomething(id), []);
  return <List items={expensive} onSelect={onSelect} />;
};

// List.jsx
const List = React.memo(function List({ items, onSelect }) {
  return items.map((it) => (
    <button key={it.id} onClick={() => onSelect(it.id)}>{it.label}</button>
  ));
});
      

2. Suspense로 데이터 페칭

Suspense 경계는 로딩 상태를 일관되게 관리하고 Code-Splitting과 조합해 초기 페인트를 가볍게 유지합니다.


// App.jsx
const UserPanel = React.lazy(() => import("./UserPanel"));

export default function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <ErrorBoundary>
        <UserPanel />
      </ErrorBoundary>
    </Suspense>
  );
}
      

데이터 소스는 리소스 래퍼를 통해 읽기 시점에 Promise를 던지게 하고 경계에서 로딩과 오류를 표준화합니다.


// resource.js
export function createResource(fetcher) {
  let status = "pending";
  let result;
  const suspender = fetcher().then(
    (r) => { status = "success"; result = r; },
    (e) => { status = "error"; result = e; }
  );
  return {
    read() {
      if (status === "pending") throw suspender;
      if (status === "error") throw result;
      return result;
    },
  };
}
      

3. 실전 체크리스트

4. 마무리

메모화는 변경 최소화 전략과 함께 설계될 때 효과가 극대화되며 Suspense는 로딩과 오류를 UI 패턴으로 끌어올립니다.