---
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]]
