253 lines
6.3 KiB
Markdown
253 lines
6.3 KiB
Markdown
---
|
|
id: perf-react-reconciler
|
|
title: React Reconciler / Rendering — 측정 / 최적화
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [performance, react, reconciler, vibe-coding]
|
|
tech_stack: { language: "TS / React", applicable_to: ["Frontend"] }
|
|
applied_in: []
|
|
aliases: [React reconciler, fiber, React Profiler, React Compiler, why-did-you-render, rerender]
|
|
---
|
|
|
|
# React Reconciler / Rendering
|
|
|
|
> "React 느림" 의 99% 가 over-render. **Profiler 측정 → key fix → memoization → split state**. **React Compiler (19+) 가 자동 memo**.
|
|
|
|
## 📖 핵심 개념
|
|
- Reconciler: virtual DOM diff.
|
|
- Render: 함수 컴포넌트 호출.
|
|
- Commit: DOM 변경 적용.
|
|
- Re-render trigger: state 변경, parent re-render, context 변경.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### React DevTools Profiler
|
|
```
|
|
Chrome DevTools → ⚛ Profiler → Record → 인터랙션 → Stop
|
|
- Flame: 어떤 컴포넌트가 렌더 + 시간
|
|
- Ranked: 시간 정렬
|
|
- "Why did this render?" — props / state / hook 변경
|
|
```
|
|
|
|
### why-did-you-render (dev)
|
|
```ts
|
|
// wdyr.ts (entry 첫 줄)
|
|
import React from 'react';
|
|
import wdyr from '@welldone-software/why-did-you-render';
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
wdyr(React, { trackAllPureComponents: true });
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
function MyComponent(...) { ... }
|
|
MyComponent.whyDidYouRender = true;
|
|
```
|
|
|
|
→ Console 에 "Same value but new reference" 같은 알람.
|
|
|
|
### Common 문제 1: inline object/array
|
|
```tsx
|
|
// ❌
|
|
<Child config={{ x: 1 }} items={[1, 2]} />
|
|
|
|
// ✅
|
|
const config = useMemo(() => ({ x: 1 }), []);
|
|
const items = useMemo(() => [1, 2], []);
|
|
|
|
// 또는 외부 const
|
|
const CONFIG = { x: 1 };
|
|
const ITEMS = [1, 2];
|
|
```
|
|
|
|
### Common 문제 2: inline function (children re-render)
|
|
```tsx
|
|
// ❌
|
|
<Child onClick={() => doSomething(id)} />
|
|
|
|
// ✅ — child 가 memo 인 경우만 의미 있음
|
|
const handle = useCallback(() => doSomething(id), [id]);
|
|
<Child onClick={handle} />
|
|
```
|
|
|
|
⚠️ child memo 아니면 의미 X.
|
|
|
|
### memo (compare props)
|
|
```tsx
|
|
const Row = memo(function Row({ item }: { item: Item }) {
|
|
return <div>{item.name}</div>;
|
|
});
|
|
|
|
// Custom comparator
|
|
const Row = memo(Component, (prev, next) => prev.item.id === next.item.id);
|
|
```
|
|
|
|
### Context — split
|
|
```tsx
|
|
// ❌ 한 context — 어떤 변경도 모든 consumer
|
|
<AppContext.Provider value={{ user, theme, settings }}>
|
|
|
|
// ✅ 분리
|
|
<UserContext.Provider value={user}>
|
|
<ThemeContext.Provider value={theme}>
|
|
<SettingsContext.Provider value={settings}>
|
|
```
|
|
|
|
→ Theme 변경 = User consumer 재렌더 X.
|
|
|
|
### State 위치 (lift up vs push down)
|
|
```tsx
|
|
// ❌ 큰 page state — 작은 input 변경이 page 전체 re-render
|
|
function Page() {
|
|
const [text, setText] = useState('');
|
|
return (
|
|
<>
|
|
<Input value={text} onChange={setText} />
|
|
<BigList />
|
|
<Sidebar />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ✅ State 가 사용 위치만
|
|
function Input() {
|
|
const [text, setText] = useState('');
|
|
return <input value={text} onChange={(e) => setText(e.target.value)} />;
|
|
}
|
|
```
|
|
|
|
### List + key
|
|
```tsx
|
|
// ❌ index = key — reorder 시 잘못된 reuse
|
|
{items.map((it, i) => <Row key={i} item={it} />)}
|
|
|
|
// ✅
|
|
{items.map(it => <Row key={it.id} item={it} />)}
|
|
```
|
|
|
|
### useDeferredValue / startTransition
|
|
```tsx
|
|
function Search({ query }) {
|
|
const deferred = useDeferredValue(query); // 무거운 계산은 deferred
|
|
const results = useMemo(() => search(deferred), [deferred]);
|
|
// input 은 빠름, results 는 약간 늦지만 — 안 막힘
|
|
}
|
|
|
|
const [, startTransition] = useTransition();
|
|
startTransition(() => {
|
|
setSearch(query); // 무거운 update 가 input 안 막음
|
|
});
|
|
```
|
|
|
|
### useMemo / useCallback — 언제?
|
|
```ts
|
|
// ✅ 무거운 계산
|
|
const sorted = useMemo(() => bigArray.sort(), [bigArray]);
|
|
|
|
// ✅ memo 자식의 props
|
|
const handle = useCallback(() => ..., [dep]);
|
|
<MemoChild onClick={handle} />
|
|
|
|
// ❌ 가벼운 + 일반 자식
|
|
const x = useMemo(() => a + b, [a, b]); // 비용 > 절약
|
|
```
|
|
|
|
### React Compiler (19+, 자동)
|
|
```ts
|
|
// babel.config.js
|
|
plugins: ['babel-plugin-react-compiler'];
|
|
```
|
|
|
|
→ memo / useMemo / useCallback 자동 — 수동 거의 불필요.
|
|
|
|
### Render count (custom hook)
|
|
```tsx
|
|
function useRenderCount(name: string) {
|
|
const c = useRef(0);
|
|
c.current++;
|
|
console.log(`${name} render #${c.current}`);
|
|
}
|
|
```
|
|
|
|
### Suspense + lazy = 실제 사용
|
|
```tsx
|
|
// 큰 component → 다른 chunk
|
|
const Heavy = lazy(() => import('./Heavy'));
|
|
|
|
<Suspense fallback={<Skeleton />}>
|
|
<Heavy />
|
|
</Suspense>
|
|
```
|
|
|
|
### List virtualization
|
|
```tsx
|
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
|
|
const v = useVirtualizer({
|
|
count: items.length,
|
|
getScrollElement: () => parentRef.current,
|
|
estimateSize: () => 50,
|
|
overscan: 5,
|
|
});
|
|
|
|
return (
|
|
<div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
|
|
<div style={{ height: v.getTotalSize() }}>
|
|
{v.getVirtualItems().map(vr => (
|
|
<div key={vr.key} style={{ position: 'absolute', top: vr.start, height: vr.size }}>
|
|
{items[vr.index].name}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
```
|
|
|
|
→ 1만 row 도 빠름.
|
|
|
|
### Selector pattern (Zustand / Redux)
|
|
```tsx
|
|
// ❌ 모든 state
|
|
const { user, orders, cart } = useStore();
|
|
|
|
// ✅ selective
|
|
const user = useStore(s => s.user); // shallow / deep equality
|
|
const orderCount = useStore(s => s.orders.length);
|
|
```
|
|
|
|
→ user 변경만 re-render.
|
|
|
|
## 🤔 의사결정 기준
|
|
| 증상 | 도구 |
|
|
|---|---|
|
|
| 모든 곳 느림 | Profiler 시작 |
|
|
| 특정 input 끊김 | useDeferredValue / startTransition |
|
|
| 큰 list | Virtualization |
|
|
| 자주 re-render | wdyr + memo |
|
|
| Context 변경 모두 영향 | Context split |
|
|
| Production | React Compiler (자동) |
|
|
|
|
## ❌ 안티패턴
|
|
- **모든 거 memo / useCallback**: 비용 > 이득. compiler 또는 측정 후.
|
|
- **Object literal 매 render**: useMemo 또는 외부 const.
|
|
- **Index key**: reorder 시 잘못.
|
|
- **Big context value 매번 새로 객체**: 모든 consumer re-render.
|
|
- **무거운 작업 render 안**: useEffect 또는 useDeferredValue.
|
|
- **State 너무 위 (lift up 과도)**: 모든 자식 re-render.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- React DevTools Profiler 가 정답.
|
|
- React 19 Compiler 자동 memo.
|
|
- 큰 list = virtualizer.
|
|
- Context = split.
|
|
|
|
## 🔗 관련 문서
|
|
- [[React_Rendering_Optimization]]
|
|
- [[React_Virtualization_Lists]]
|
|
- [[Web_Performance_Core_Vitals]]
|