본문으로 건너뛰기

가상 스크롤

수천 행의 목록 · 표에서 보이는 행과 위아래 10행만 그립니다. @tanstack/react-virtual 이 필요하고, 그 API 는 밖으로 내보내지 않습니다.

화면 밖 행이 DOM 에 없어도 위치를 읽게 표에는 aria-rowcount · aria-rowindex, 목록에는 aria-setsize · aria-posinset 을 답니다. 포커스된 행은 스크롤로 화면 밖에 나가도 그대로 남아 포커스가 떨어지지 않습니다. 스크롤하는 동안 위치 알약(1,201–1,250 / 12,480)이 1초 보이고, 읽기 프로그램에는 읽히지 않습니다. 줄인 모션에서는 scrollToIndex 가 부드럽게 넘기지 않고 바로 갑니다.

import
import { VirtualList, VirtualTable } from "@uyoolabs/ui/virtual";
쓰는 경우
결제 내역 · 감사 로그 · 검색 결과처럼 1,000행이 넘는 목록. 행 높이가 고정이어야 합니다(셀 글은 한 줄 말줄임). 표는 열 너비가 흔들리지 않게 Th 에 너비를 줍니다.
쓰지 않는 경우
1,000행 아래는 페이지 나누기(Pagination)가 낫습니다 — 브라우저 찾기 · 인쇄 · 위치 기억이 됩니다. 높이가 바뀌는 행(메시지)은 받지 않습니다. 브라우저 찾기(⌘F)는 그린 행만 찾으니 표 위 검색칸을 함께 둡니다.

표 — 12,480행

머리는 위에 붙고(층 10), 아직 오지 않은 페이지는 300ms 뒤 뼈대 줄로 자리를 지킵니다.

#결제금액
1구독 갱신 · 워크스페이스 2₩84,000
2구독 갱신 · 워크스페이스 3₩84,137
3구독 갱신 · 워크스페이스 4₩84,274
4구독 갱신 · 워크스페이스 5₩84,411
5구독 갱신 · 워크스페이스 6₩84,548
6구독 갱신 · 워크스페이스 7₩84,685
7구독 갱신 · 워크스페이스 8₩84,822
8구독 갱신 · 워크스페이스 9₩84,959
9구독 갱신 · 워크스페이스 10₩85,096
10구독 갱신 · 워크스페이스 11₩85,233
11구독 갱신 · 워크스페이스 12₩85,370
12구독 갱신 · 워크스페이스 13₩85,507
13구독 갱신 · 워크스페이스 14₩85,644
14구독 갱신 · 워크스페이스 15₩85,781
15구독 갱신 · 워크스페이스 16₩85,918
16구독 갱신 · 워크스페이스 17₩86,055
17구독 갱신 · 워크스페이스 18₩86,192
18구독 갱신 · 워크스페이스 19₩86,329
19구독 갱신 · 워크스페이스 20₩86,466
20구독 갱신 · 워크스페이스 21₩86,603

목록 — 커서 ‘더 보기’

끝 근처에서 onEndReached 를 한 번 부릅니다.

  • 감사 로그 1 · 결제 설정을 바꿨습니다
  • 감사 로그 2 · 결제 설정을 바꿨습니다
  • 감사 로그 3 · 결제 설정을 바꿨습니다
  • 감사 로그 4 · 결제 설정을 바꿨습니다
  • 감사 로그 5 · 결제 설정을 바꿨습니다
  • 감사 로그 6 · 결제 설정을 바꿨습니다
  • 감사 로그 7 · 결제 설정을 바꿨습니다
  • 감사 로그 8 · 결제 설정을 바꿨습니다
  • 감사 로그 9 · 결제 설정을 바꿨습니다
  • 감사 로그 10 · 결제 설정을 바꿨습니다
  • 감사 로그 11 · 결제 설정을 바꿨습니다
  • 감사 로그 12 · 결제 설정을 바꿨습니다
  • 감사 로그 13 · 결제 설정을 바꿨습니다
  • 감사 로그 14 · 결제 설정을 바꿨습니다
  • 감사 로그 15 · 결제 설정을 바꿨습니다
  • 감사 로그 16 · 결제 설정을 바꿨습니다

키보드

행 포커스가 있을 때입니다.

키동작
Tab목록에 들어온다. 로빙 포커스라 Tab 정지점은 행 하나다
↑·↓이전 · 다음 행. 화면 밖이면 먼저 스크롤해 그린 뒤 포커스
PageUp·PageDown한 화면만큼 이동
Home·End·⌘/Ctrl+Home·⌘/Ctrl+End처음 · 마지막 행(그리지 않은 끝도 스크롤해서 간다)

props

소스의 타입과 JSDoc 에서 생성한 표입니다. 컴포넌트를 고치면 pnpm gen 으로 다시 만듭니다.

VirtualTable

prop타입기본값설명
itemsreadonly T[]—받은 행. count · getItem 을 쓰면 비워 둔다
countnumber—서버가 아는 전체 수(aria-setsize · aria-rowcount). 없으면 items 길이
getItem(index: number) => T | undefined—아직 오지 않은 행은 undefined 를 돌려준다 — 뼈대가 그 자리를 지킨다
getKey필수(item: T, index: number) => string—행의 안정된 키(React key)
rowHeight필수number—고정 높이(44 · 촘촘 34). 늘었다 줄었다 하는 높이는 받지 않는다
overscannumber—보이는 행 위아래로 더 그려 둘 행 수
renderSkeleton(index: number) => ReactNode—아직 오지 않은 행(getItem 이 undefined)의 뼈대. skeletonDelay 안에 오면 그리지 않는다
skeletonDelaynumber—뼈대를 그리기 전 기다림(ms). 그 안에 온 행은 뼈대 없이 바로 그린다
onRangeChange(range: { start: number; end: number }) => void—보이는 범위(0부터, 끝 포함)가 바뀔 때 — 서버 페이지를 불러온다
onEndReached() => void—끝에서 rowHeight × 5 안쪽에 오면 한 번. count 가 늘면 다시 부를 수 있다(커서 ‘더 보기’)
loadingMoreboolean—끝에서 더 불러오는 중. 끝에 뼈대 3줄을 두고 ‘더 불러오는 중’ 을 한 번 알린다
scrollToIndexnumber—제어 — 이 행을 화면 안으로 옮긴다(고른 항목). 줄인 모션이면 부드럽게 넘기지 않는다
initialScrollIndexnumber—처음 열 때 이 행에서 시작한다
showPositionboolean | "always"—스크롤하는 동안 위치 알약을 1초 띄운다. "always" 는 늘 띄운다(문서 · 스냅샷)
heightnumber | string—스크롤 영역 높이. 숫자는 px
emptyReactNode—행이 하나도 없을 때 그릴 것(Empty)
footerReactNode—마지막 행 뒤의 줄(오류 줄 + 다시 시도 · 끝 표시)
aria-label필수string—스크롤 영역(region)의 이름 — 필수
classNamestring—
lang"ko" | "en" | "ja"—
labelsPartial<VirtualLabels>—
head필수ReactNode—D-Table 의 Th 들. 머리는 스크롤해도 위에 붙어 있다(sticky, 층 10)
columnsnumber—열 수. 기본 뼈대 줄 · 더 불러오는 줄이 열 전체를 덮는 데 쓴다
renderRow필수(item: T, index: number) => ReactElement—한 행. ./ui 의 <Tr> 를 돌려준다 — 부품이 aria-rowindex · 로빙 포커스를 그 행에 단다

VirtualList

prop타입기본값설명
itemsreadonly T[]—받은 행. count · getItem 을 쓰면 비워 둔다
countnumber—서버가 아는 전체 수(aria-setsize · aria-rowcount). 없으면 items 길이
getItem(index: number) => T | undefined—아직 오지 않은 행은 undefined 를 돌려준다 — 뼈대가 그 자리를 지킨다
getKey필수(item: T, index: number) => string—행의 안정된 키(React key)
rowHeight필수number—고정 높이(44 · 촘촘 34). 늘었다 줄었다 하는 높이는 받지 않는다
overscannumber—보이는 행 위아래로 더 그려 둘 행 수
renderSkeleton(index: number) => ReactNode—아직 오지 않은 행(getItem 이 undefined)의 뼈대. skeletonDelay 안에 오면 그리지 않는다
skeletonDelaynumber—뼈대를 그리기 전 기다림(ms). 그 안에 온 행은 뼈대 없이 바로 그린다
onRangeChange(range: { start: number; end: number }) => void—보이는 범위(0부터, 끝 포함)가 바뀔 때 — 서버 페이지를 불러온다
onEndReached() => void—끝에서 rowHeight × 5 안쪽에 오면 한 번. count 가 늘면 다시 부를 수 있다(커서 ‘더 보기’)
loadingMoreboolean—끝에서 더 불러오는 중. 끝에 뼈대 3줄을 두고 ‘더 불러오는 중’ 을 한 번 알린다
scrollToIndexnumber—제어 — 이 행을 화면 안으로 옮긴다(고른 항목). 줄인 모션이면 부드럽게 넘기지 않는다
initialScrollIndexnumber—처음 열 때 이 행에서 시작한다
showPositionboolean | "always"—스크롤하는 동안 위치 알약을 1초 띄운다. "always" 는 늘 띄운다(문서 · 스냅샷)
heightnumber | string—스크롤 영역 높이. 숫자는 px
emptyReactNode—행이 하나도 없을 때 그릴 것(Empty)
footerReactNode—마지막 행 뒤의 줄(오류 줄 + 다시 시도 · 끝 표시)
aria-label필수string—스크롤 영역(region)의 이름 — 필수
classNamestring—
lang"ko" | "en" | "ja"—
labelsPartial<VirtualLabels>—
renderRow필수(item: T, index: number) => ReactNode—한 행의 내용. 행 틀(role=listitem · 높이 · 포커스)은 부품이 두른다