Files
2nd/10_Wiki/Topics/Frontend/대규모 데이터 렌더링 및 가상화 최적화.md
T
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
10_Wiki/Topics 대규모 정리:
- 오류 캡처/미완성 stub 문서 227개 제거
- 교차폴더 중복 43클러스터 병합 (63파일 → redirect)
- 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건
- 카테고리 MOC 6개 신규 생성
- Graph 섹션 미해결 related-keyword 링크 10,058건 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:52:15 +09:00

5.9 KiB
Raw Blame History

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-대규모-데이터-렌더링-및-가상화-최적화 대규모 데이터 렌더링 및 가상화 최적화 10_Wiki/Topics verified self
Virtualization
Windowing
Virtual List
가상 스크롤
none A 0.9 applied
frontend
virtualization
performance
react
rendering
2026-05-10 pending
language framework
typescript react

대규모 데이터 렌더링 및 가상화 최적화

매 한 줄

"매 보이는 것만 그린다". 100k row를 한 번에 mount 하면 DOM이 죽는다. Windowing 으로 viewport 안 ~30개만 render → scroll 시 recycle. 2026 표준은 TanStack Virtual / react-virtuoso, content-visibility CSS, 그리고 React 19의 <Activity> (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 표준)

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: Row[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48,
    overscan: 5,
  });

  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map(v => (
          <div
            key={v.key}
            data-index={v.index}
            ref={virtualizer.measureElement}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${v.start}px)`,
            }}
          >
            {items[v.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

Variable height with measurement

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)

.row {
  content-visibility: auto;
  contain-intrinsic-size: 0 48px; /* 매 placeholder size */
}

브라우저가 viewport 밖 element를 자동으로 skip layout/paint. React 없이도 효과.

Infinite scroll + virtualization

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)

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 <Activity> for tab-like UI

<Activity mode={isActive ? 'visible' : 'hidden'}>
  <ExpensiveTab />  {/* 매 hidden 시 unmount X, but no render cost */}
</Activity>

Server-side pagination + cursor

// 매 절대 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 <Activity>.

기본값: TanStack Virtual + React Query infinite + content-visibility 보조.

🔗 Graph

🤖 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 통합