React 성능 문제의 주요 원인
React 애플리케이션에서 가장 흔한 성능 문제는 불필요한 리렌더링입니다. 부모 컴포넌트가 렌더링되면 모든 자식이 함께 렌더링되는 React의 기본 동작이 큰 컴포넌트 트리에서 성능 저하를 유발합니다.
DevTools Profiler 사용법
React DevTools의 Profiler 탭에서 렌더링 기록을 시작하고, 문제 상황을 재현한 뒤 기록을 중지합니다. Flamegraph에서 각 컴포넌트의 렌더링 시간과 원인을 확인할 수 있습니다.
// 개발 모드에서 리렌더링 원인 추적
// React DevTools Settings > Profiler > Record why each component rendered
// 프로그래밍 방식으로 렌더링 추적
import { Profiler } from 'react';
function onRenderCallback(
id, // Profiler 트리의 id
phase, // "mount" 또는 "update"
actualDuration, // 실제 렌더링 시간
baseDuration, // 메모이제이션 없을 때 예상 시간
startTime,
commitTime
) {
console.table({ id, phase, actualDuration, baseDuration });
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
);
}
불필요한 리렌더링 방지
// 1. React.memo로 props 변경 시에만 리렌더링
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// 2. useMemo로 비용 큰 계산 캐싱
function Dashboard({ transactions }) {
const summary = useMemo(() => {
return transactions.reduce((acc, tx) => {
acc.total += tx.amount;
acc.count += 1;
acc.categories[tx.category] = (acc.categories[tx.category] || 0) + tx.amount;
return acc;
}, { total: 0, count: 0, categories: {} });
}, [transactions]);
return <SummaryCard data={summary} />;
}
// 3. useCallback으로 함수 참조 안정화
function Parent() {
const [selected, setSelected] = useState(null);
const handleSelect = useCallback((id) => {
setSelected(id);
}, []);
return <ExpensiveList items={items} onSelect={handleSelect} />;
}
상태 구조 최적화
// BAD: 하나의 큰 상태 — 어떤 변경이든 전체 리렌더링
const [state, setState] = useState({
user: null,
theme: 'light',
notifications: [],
sidebarOpen: false,
});
// GOOD: 상태 분리 — 변경된 부분만 리렌더링 유발
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
const [sidebarOpen, setSidebarOpen] = useState(false);
// BETTER: Context 분리로 구독 범위 최소화
const ThemeContext = createContext();
const UserContext = createContext();
const NotificationContext = createContext();
가상화로 대량 렌더링 최적화
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size,
width: '100%',
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}
프로파일링 체크리스트
| 항목 | 도구 | 확인 사항 |
|---|---|---|
| 리렌더링 횟수 | React DevTools Profiler | 불필요한 리렌더링 식별 |
| 번들 크기 | webpack-bundle-analyzer | 큰 의존성 확인 |
| 메모리 누수 | Chrome Memory 탭 | 힙 스냅샷 비교 |
| 런타임 성능 | Chrome Performance 탭 | Long Task 식별 |
- 최적화는 측정 먼저, 추측하지 말 것 — Profiler로 실제 병목을 확인한 후 최적화
- React.memo는 props가 자주 바뀌는 컴포넌트에 적용하면 오히려 오버헤드가 됩니다
- 상태를 가능한 사용하는 곳 가까이에 배치하면 리렌더링 범위가 자연스럽게 줄어듭니다
댓글 0