리스트 가상화(List Virtualization)·윈도잉(Windowing)은 수천~수만 개 항목 중 화면에 보이는 것과 약간의 여유분만 실제 DOM(Document Object Model)에 렌더하고, 스크롤할 때 그 범위를 바꿔 끼우는 기법이다. 1만 개 목록이라도 DOM에는 20~30개만 있다.
TanStack Virtual의 동작
- 위치 측정 캐시: 항목마다
start·size·end를 계산해 둔다. 실제 크기를 쟀으면 그 값을, 아니면estimateSize를 쓴다. 다음 항목의 시작은 이전 항목의 끝 + gap이다 - 가시 범위 계산: 현재 스크롤 위치에서 시작하는 첫 항목을 이진 탐색(O(log n))으로 찾고, 뷰포트 끝까지 끝 인덱스를 늘린다
- overscan: 범위를 위아래로 몇 개 더 넓혀 빠르게 스크롤할 때 빈칸이 보이지 않게 한다
- 렌더: 전체 높이(
getTotalSize())를 가진 컨테이너 안에 보이는 항목만position: absolute+translateY(start)로 놓는다
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: v.getTotalSize(), position: 'relative' }}>
{v.getVirtualItems().map(item => (
<div key={item.key} style={{ position: 'absolute', top: 0, transform: `translateY(${item.start}px)` }}>
Row {item.index}
</div>
))}
</div>
</div>- 스크롤은 패시브 리스너(Passive Event Listener)로, 컨테이너 크기는 ResizeObserver로 관찰한다
- 동적 크기: 렌더된 요소를 실제로 재서 캐시를 고친다. 현재 위치보다 위쪽 항목의 크기가 바뀌면 그 차이만큼 스크롤 위치를 보정해 화면이 튀지 않게 한다
- 계산 단계마다 의존값이 같으면 결과를 재사용해 프레임 예산 안에 끝낸다
transform을 쓰는 이유는 레이아웃을 다시 계산하지 않는 합성 단계에서 처리되기 때문이다(리플로우(레이아웃 재계산))- 훅으로 "이런 목록을 원한다"고 선언하는 구조라 React와 잘 맞는다(선언형과 명령형 프로그래밍)
더 가벼운 브라우저 기본 대안은 content-visibility, 무한 스크롤 데이터는 TanStack Query의 useInfiniteQuery와 함께 쓴다.