--- id: wiki-2026-0508-대규모-데이터-렌더링-및-가상화-최적화 title: 대규모 데이터 렌더링 및 가상화 최적화 category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Virtualization, Windowing, Virtual List, 가상 스크롤] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [frontend, virtualization, performance, react, rendering] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: typescript framework: react --- # 대규모 데이터 렌더링 및 가상화 최적화 ## 매 한 줄 > **"매 보이는 것만 그린다"**. 100k row를 한 번에 mount 하면 DOM이 죽는다. Windowing 으로 viewport 안 ~30개만 render → scroll 시 recycle. 2026 표준은 TanStack Virtual / react-virtuoso, content-visibility CSS, 그리고 React 19의 `` (offscreen). ## 매 핵심 ### 매 왜 가상화가 필수인가 - DOM node 1개 ≈ 1-5KB memory + layout/paint cost. - 10k+ row mount → 수백 MB heap, scroll jank, INP > 500ms. - React reconciliation도 O(n) — fiber tree가 거대해지면 commit이 느려진다. ### 매 windowing 전략 - **Fixed size**: row height 동일 → offset = index × height. 가장 빠름. - **Variable size**: row 마다 다름 → measured cache + estimated. ResizeObserver 로 동적 측정. - **Dynamic load**: scroll 끝 도달 시 fetch (infinite scroll) — virtualization 과 결합. ### 매 응용 1. Admin table 100k+ row. 2. Chat history (역방향 virtualization). 3. Image gallery / Pinterest grid. 4. Code editor (Monaco-style line virtualization). ## 💻 패턴 ### TanStack Virtual (2026 표준) ```tsx import { useVirtualizer } from '@tanstack/react-virtual'; function VirtualList({ items }: { items: Row[] }) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 48, overscan: 5, }); return (
{virtualizer.getVirtualItems().map(v => (
{items[v.index].name}
))}
); } ``` ### Variable height with measurement ```tsx const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 80, // 매 best guess measureElement: el => el.getBoundingClientRect().height, // 매 actual measure overscan: 8, }); ``` ### CSS content-visibility (zero-JS virtualization) ```css .row { content-visibility: auto; contain-intrinsic-size: 0 48px; /* 매 placeholder size */ } ``` 브라우저가 viewport 밖 element를 자동으로 skip layout/paint. React 없이도 효과. ### Infinite scroll + virtualization ```tsx const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ['rows'], queryFn: ({ pageParam = 0 }) => fetchPage(pageParam), getNextPageParam: last => last.nextCursor, }); const allRows = data?.pages.flatMap(p => p.items) ?? []; const virtualizer = useVirtualizer({ count: allRows.length, /* ... */ }); useEffect(() => { const last = virtualizer.getVirtualItems().at(-1); if (last && last.index >= allRows.length - 5 && hasNextPage) { fetchNextPage(); } }, [virtualizer.getVirtualItems()]); ``` ### Grid virtualization (2D) ```tsx const rowVirtualizer = useVirtualizer({ count: rows, /* ... */ }); const colVirtualizer = useVirtualizer({ horizontal: true, count: cols, getScrollElement: () => parentRef.current, estimateSize: () => 120, }); // 매 nested loop: rows × cols 의 visible cells only. ``` ### React 19 `` for tab-like UI ```tsx {/* 매 hidden 시 unmount X, but no render cost */} ``` ### Server-side pagination + cursor ```ts // 매 절대 client 에 100k row 보내지 마라. GET /api/rows?cursor=abc123&limit=50 → { items: [...], nextCursor: 'def456' } ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | < 100 rows | 매 plain map. Virtualization X. | | 100-1000 rows, fixed height | content-visibility CSS. | | 1000+ rows | TanStack Virtual / react-virtuoso. | | Variable height | measure + estimateSize. | | Infinite stream | virtualization + cursor pagination. | | Tab/accordion | React 19 ``. | **기본값**: TanStack Virtual + React Query infinite + content-visibility 보조. ## 🔗 Graph - 부모: [[Frontend Performance]] · [[Rendering]] - 변형: [[Infinite Scroll]] · [[Pagination]] - 응용: [[Admin Dashboard]] · [[Chat UI]] · [[Code Editor]] - Adjacent: [[React Query]] · [[ResizeObserver]] · [[content-visibility]] ## 🤖 LLM 활용 **언제**: row count > 500, scroll 성능 issue, INP regression. **언제 X**: static page, 매 작은 list (< 100), SEO-critical content (crawler 가 못 봄). ## ❌ 안티패턴 - **Mount all then hide**: `display: none` 으로 숨겨도 DOM 비용은 낸다. - **Key as index**: row reorder 시 reconciliation 깨짐 — stable id 사용. - **Inline measure on every render**: ResizeObserver + cache 필수. - **No overscan**: scroll 시 빈 영역 보임. overscan 5-10. - **Animation on virtualized rows**: 매 recycle 시 animation 초기화 — layout id 따로 관리. ## 🧪 검증 / 중복 - Verified (TanStack Virtual docs, Chrome DevTools, web.dev/content-visibility). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — virtualization + content-visibility + Activity 통합 |