컴포넌트 리렌더 원인을 최소화하고 사용자 체감 성능을 높이기 위해 메모화와 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>
));
});
- 객체/배열 props는 useMemo로 포장하거나 상위에서 안정 참조를 유지합니다.
- 컨텍스트 값은 좁은 범위로 분리해 변경 전파를 최소화합니다.
- 리스트는 key 안정성과 가상 스크롤을 함께 고려해 비용을 낮춥니다.
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. 실전 체크리스트
- props 구조 안정화와 핫패스 컴포넌트에 한정된 메모화를 적용합니다.
- Suspense 경계를 페이지 상단과 중요 영역에 배치해 스켈레톤을 일관되게 노출합니다.
- 상태 전파 범위를 좁히고 불변성 유지로 변경 탐지를 명확히 합니다.
4. 마무리
메모화는 변경 최소화 전략과 함께 설계될 때 효과가 극대화되며 Suspense는 로딩과 오류를 UI 패턴으로 끌어올립니다.