본문으로 건너뛰기

React Virtual

@tanstack/react-virtual 어댑터는 핵심 가상화 로직을 감싸는 래퍼입니다.

useVirtualizer

function useVirtualizer<TScrollElement, TItemElement = unknown>(
options: PartialKeys<
ReactVirtualizerOptions<TScrollElement, TItemElement>,
'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
>,
): Virtualizer<TScrollElement, TItemElement>

이 함수는 HTML 요소를 scrollElement로 사용하도록 구성된 표준 Virtualizer 인스턴스를 반환합니다.

useWindowVirtualizer

function useWindowVirtualizer<TItemElement = unknown>(
options: PartialKeys<
ReactVirtualizerOptions<Window, TItemElement>,
| 'getScrollElement'
| 'observeElementRect'
| 'observeElementOffset'
| 'scrollToFn'
>,
): Virtualizer<Window, TItemElement>

이 함수는 window를 scrollElement로 사용하도록 구성된 window 기반 Virtualizer 인스턴스를 반환합니다.

React 전용 옵션

useFlushSync

type ReactVirtualizerOptions<TScrollElement, TItemElement> = 
VirtualizerOptions<TScrollElement, TItemElement> & {
useFlushSync?: boolean
}

useVirtualizeruseWindowVirtualizer는 모두 useFlushSync 옵션을 받으며, 이 옵션은 동기 업데이트에 React의 flushSync를 사용할지 제어합니다.

  • 타입: boolean
  • 기본값: true
  • 설명: true이면 버추얼라이저가 flushSyncreact-dom에서 가져와 사용하여 스크롤 이벤트 중 동기 렌더링을 보장합니다. 가장 정확한 스크롤 동작을 제공하지만 일부 상황에서는 성능에 영향을 줄 수 있습니다.

useFlushSync를 비활성화해야 하는 경우

다음과 같은 경우에는 useFlushSync: false로 설정하는 것을 고려할 수 있습니다.

  • React 19 호환성: React 19에서는 스크롤할 때 다음과 같은 콘솔 경고가 표시될 수 있습니다.
    flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.
    useFlushSync: false로 설정하면 React가 업데이트를 자연스럽게 배칭할 수 있어 이 경고가 사라집니다.
  • 성능 최적화: 저사양 기기에서 빠르게 스크롤할 때 성능 문제가 발생하는 경우
  • 테스트 환경: 동기 DOM 업데이트가 필요하지 않은 테스트를 실행하는 경우
  • 중요하지 않은 목록: 전반적인 성능을 개선하기 위해 스크롤 중 약간의 시각적 지연을 허용할 수 있는 경우

예시

const virtualizer = useVirtualizer({
count: 10000,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
useFlushSync: false, // Disable synchronous updates
})

directDomUpdates

  • 타입: boolean
  • 기본값: false
  • 설명: 스크롤 전용 업데이트에서는 React의 재렌더링을 건너뜁니다. 활성화하면 버추얼라이저가 항목 위치(top/left 또는 transform)와 컨테이너 크기(height/width)를 DOM에 직접 쓰고, 표시되는 인덱스 범위나 isScrolling이 변경될 때만 재렌더링합니다.

활성화 시 요구 사항

  • 항목 요소에는 position: absolute를 지정해야 하며, 'transform' 모드에서는 top: 0 / left: 0으로 기준점도 고정해야 합니다.
  • 항목 요소는 스타일에서 주축 위치를 설정하면 안 됩니다. 버추얼라이저가 top / left'position' 모드에서, transform'transform' 모드에서 관리합니다.
  • 내부 크기 컨테이너는 virtualizer.containerRef를 전달받아야 하며 스타일에서 height / width를 설정하면 안 됩니다.
  • 다중 레인 레이아웃(그리드 / 메이슨리)에서는 교차축 위치(예: left: ${(item.lane * 100) / lanes}%)가 항목별로 고정되므로 JSX에서 계속 설정해야 합니다. 자동화되는 것은 주축뿐입니다.

⚠️ 이 플래그는 마운트할 때 한 번만 설정하도록 설계되었습니다. 런타임에 이 플래그(또는 directDomUpdatesMode)를 전환하면 항목과 컨테이너에 더 이상 유효하지 않은 인라인 스타일이 남을 수 있습니다.

참고: containerRef를 생략하면 버추얼라이저가 DOM에 직접 아무것도 쓰지 않습니다. 즉, 항목 위치와 컨테이너 크기를 모두 쓰지 않습니다. 이 경우 재렌더링을 건너뛰는 이점은 그대로 얻으면서 항목 배치와 컨테이너 크기 설정을 직접(예: onChange에서) 처리해야 합니다.

예시

const virtualizer = useVirtualizer({
count: 10000,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
directDomUpdates: true,
})

return (
<div ref={parentRef} style={{ overflow: 'auto', height: 400 }}>
{/* The inner container must use virtualizer.containerRef and not set height */}
<div ref={virtualizer.containerRef} style={{ position: 'relative' }}>
{virtualizer.getVirtualItems().map((item) => (
<div
key={item.key}
ref={virtualizer.measureElement}
data-index={item.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
// Do NOT set top/left/transform — the virtualizer handles it
}}
>
Row {item.index}
</div>
))}
</div>
</div>
)

directDomUpdatesMode

  • 타입: 'position' | 'transform'
  • 기본값: 'transform'
  • 설명: directDomUpdates가 항목 요소의 위치를 지정하는 방식을 제어합니다.
    • 'transform'(기본값): transform: translate3d(...)를 씁니다. 항목을 각각의 컴포지터 레이어로 승격하므로 일반적으로 긴 목록에서 더 부드럽지만, 쌓임 맥락을 생성하고 position: fixed인 하위 요소의 작동을 방해할 수 있습니다. 항목 요소는 position: absolute, top: 0, left: 0으로 기준점을 고정해야 합니다.
    • 'position': top / left를 씁니다. 항목 요소는 position: absolute여야 합니다.

예시

const virtualizer = useVirtualizer({
count: 10000,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
directDomUpdates: true,
directDomUpdatesMode: 'position', // Use top/left instead of transform
})