[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
---
|
||||
id: react-virtualization-lists
|
||||
title: React List 가상화 (Virtualization)
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [react, virtualization, lists, performance, vibe-coding]
|
||||
tech_stack: { language: "TypeScript / @tanstack/react-virtual", applicable_to: ["Web", "React Native"] }
|
||||
applied_in: []
|
||||
aliases: [windowing, react-virtual, react-window, infinite scroll]
|
||||
---
|
||||
|
||||
# React List 가상화
|
||||
|
||||
> 1만 행 모두 렌더하면 DOM 폭발 → 60fps 깨짐. **화면에 보이는 30개만 렌더 + 위/아래 padding 으로 스크롤바 유지** = windowing. 1만 행도 60fps.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- 가상 list: 보이는 영역(viewport) + overscan 만 실제 DOM.
|
||||
- Total height = `itemCount * itemHeight` 로 가짜 padding.
|
||||
- 스크롤 시 보이는 영역 계산 → 그 인덱스 범위만 render.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### @tanstack/react-virtual
|
||||
```tsx
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
|
||||
function BigList({ items }: { items: Row[] }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const v = useVirtualizer({
|
||||
count: items.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 60, // 평균 행 높이
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={parentRef} style={{ height: '100vh', overflow: 'auto' }}>
|
||||
<div style={{ height: v.getTotalSize(), position: 'relative' }}>
|
||||
{v.getVirtualItems().map(vi => (
|
||||
<div
|
||||
key={items[vi.index].id}
|
||||
data-index={vi.index}
|
||||
ref={v.measureElement} // 가변 높이 측정
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0, left: 0, width: '100%',
|
||||
transform: `translateY(${vi.start}px)`,
|
||||
}}
|
||||
>
|
||||
<Row data={items[vi.index]} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 무한 스크롤 + virtualization
|
||||
```tsx
|
||||
const { items, fetchNextPage, hasNextPage } = useInfiniteQuery(...);
|
||||
const flat = items?.pages.flatMap(p => p.items) ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
const last = v.getVirtualItems().at(-1);
|
||||
if (!last) return;
|
||||
if (last.index >= flat.length - 5 && hasNextPage) fetchNextPage();
|
||||
}, [v.getVirtualItems(), hasNextPage]);
|
||||
```
|
||||
|
||||
### Grid (2D)
|
||||
```tsx
|
||||
const colVirt = useVirtualizer({ count: cols, ..., horizontal: true });
|
||||
const rowVirt = useVirtualizer({ count: rows, ... });
|
||||
// 두 차원 모두 windowing
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 항목 수 | 가상화 |
|
||||
|---|---|
|
||||
| < 50 | 불필요 |
|
||||
| 50-500 | 선택. 모바일이면 권장 |
|
||||
| 500+ | 필수 |
|
||||
| 가변 높이 | measureElement 또는 ResizeObserver |
|
||||
| 그리드 (행+열) | 2D virtualizer |
|
||||
| Drag-and-drop | 가상화 + DnD 통합 — react-arborist, dnd-kit |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 list 가상화**: 작은 list 도 가상화 비용. 50+ 부터.
|
||||
- **estimateSize 부정확**: 스크롤바 점프. 평균 + measureElement 권장.
|
||||
- **overscan 0**: 빠른 스크롤 시 빈 화면. 5~10 권장.
|
||||
- **각 item 안의 무거운 컴포넌트 + memo 없음**: 빠른 스크롤 시 끊김. memo + stable props.
|
||||
- **inner div 에 transform 외 layout 속성**: 위치 계산 깨짐. translate3d 권장.
|
||||
- **Tab/검색 정확 위치 못 잡음**: scrollToIndex 제공.
|
||||
- **SEO 필요한 list 가상화**: SSR 시 화면 외 행 없음. SSR 은 처음 N개 + hydration 후 가상화.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- "@tanstack/react-virtual 선호 (RR/Next 호환). FlashList 는 RN".
|
||||
- 무한 스크롤은 useInfiniteQuery + virtualizer 결합.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[React_Rendering_Optimization]]
|
||||
- [[Backpressure_Patterns]]
|
||||
Reference in New Issue
Block a user