Viewport Hook
전체 데이터를 배열로 만들지 않고 현재 스크롤 주변 블록을 조회합니다. useCominsViewport가 소비자 React 상태를 관리하며 Table은 필요한 범위를 요청합니다.
실행 예제
섹션 제목: “실행 예제”import { useMemo, useRef, useState } from 'react';import { CominsTable, reduceCominsViewportData, useCominsViewport, type CominsTableColumn, type CominsViewportRequest } from 'comins-table';import 'comins-table/styles.css';
type Row = { id: number; name: string; amount: number };const rowCount = 50_000;
export default function TableViewportDemo({ lang = 'ko' }: { lang?: string }) { const ko = lang === 'ko'; const [revision, setRevision] = useState(0); const [descending, setDescending] = useState(false); const [events, setEvents] = useState<string[]>([]); const edits = useRef(new Map<number, Row>()); const failNext = useRef(false); const record = (text: string) => setEvents(previous => [...previous.slice(-7), text]); const columns = useMemo<CominsTableColumn<Row>[]>(() => [ { field: 'id', label: 'ID', width: 110 }, { field: 'name', label: ko ? '이름' : 'Name', width: 220 }, { field: 'amount', label: ko ? '수량' : 'Amount', width: 130 }, ], [ko]); const viewport = useCominsViewport<Row>({ rowCount, queryKey: `${descending ? 'desc' : 'asc'}:${revision}`, blockSize: 50, cacheSize: 3, maxConcurrentRequests: 2, getRows: async ({ startIndex, endIndex, signal }: CominsViewportRequest) => { const range = `${startIndex}–${endIndex}`; record(`request ${range}`); // Simulated remote latency; replace this block with fetch(url, { signal }). await new Promise<void>((resolve, reject) => { if (signal.aborted) { reject(new DOMException('Aborted', 'AbortError')); return; } const abort = () => { clearTimeout(timer); record(`cancel ${range}`); reject(new DOMException('Aborted', 'AbortError')); }; const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve(); }, 400); signal.addEventListener('abort', abort, { once: true }); }); if (failNext.current) { failNext.current = false; record(`error ${range}`); throw new Error('Demo request failure'); } const rows = Array.from({ length: endIndex - startIndex }, (_, offset) => { const index = startIndex + offset; const id = descending ? rowCount - index : index + 1; return edits.current.get(id) ?? { id, name: `${ko ? '항목' : 'Item'} ${id}`, amount: id * 10 }; }); record(`success ${range}`); return rows; }, onEdit: changes => { // The app retains edits outside the block cache. No server write is made here. changes.forEach(({ row }) => edits.current.set(row.id, row)); record(`edit ${changes.map(change => change.index).join(',')}`); }, }); const first = viewport.data.blocks[0]; const editFirst = () => { const row = first?.rows[0]; if (!first || !row) return; viewport.tableProps.onChangeData(reduceCominsViewportData(viewport.data, { type: 'patch', changes: [{ index: first.startIndex, row: { ...row, amount: row.amount + 1 } }], })); }; return <div className="demo" data-demo="table-viewport"> <div className="demo-toolbar"> <button onClick={() => setDescending(value => !value)}>{ko ? '순서 전환' : 'Reverse order'}</button> <button onClick={() => setRevision(value => value + 1)}>{ko ? '조회 갱신' : 'Reload query'}</button> <button onClick={() => { failNext.current = true; setRevision(value => value + 1); }}>{ko ? '오류 재현' : 'Simulate error'}</button> <button disabled={!first?.rows.length} onClick={editFirst}>{ko ? '첫 캐시 행 +1' : 'First cached row +1'}</button> </div> <p className="demo-status">{ko ? '50,000행 · 범위 조회 시뮬레이션 · 블록 밖에서도 편집 보존' : '50,000 rows · simulated range loading · edits retained outside the cache'}</p> <div className="table-viewport" style={{ height: 320 }}> <CominsTable {...viewport.tableProps} columns={columns} getRowId={row => row.id} rowHeight={36} /> </div> <p className="demo-status" data-viewport-stats> {ko ? '조회' : 'Query'} {viewport.data.revision} · {ko ? '캐시' : 'Cached'} {viewport.data.blocks.reduce((sum, block) => sum + block.rows.length, 0)} / {rowCount} · {ko ? '요청' : 'Requests'} {viewport.data.requests.length} </p> <details open><summary>{ko ? '요청·편집 기록' : 'Request & edit log'}</summary><pre data-viewport-events>{events.join('\n')}</pre></details> <details><summary>{ko ? '캐시 상태' : 'Cache state'}</summary><pre data-viewport-state>{JSON.stringify({ revision: viewport.data.revision, blocks: viewport.data.blocks.map(block => ({ startIndex: block.startIndex, count: block.rows.length, first: block.rows[0] })), requests: viewport.data.requests, editCount: edits.current.size }, null, 2)}</pre></details> </div>;}useCominsViewport
섹션 제목: “useCominsViewport”export declare function useCominsViewport<T>(options: CominsViewportOptions<T>): { data: CominsViewportData<T>; tableProps: { data: CominsViewportData<T>; onChangeData: (next: CominsViewportData<T>) => void; onViewportRequest: (request: CominsViewportRequest) => Promise<void>; viewportDatasource: CominsViewportDatasource; virtualized: true; };};반환값 data는 현재 snapshot입니다. tableProps를 CominsTable에 펼치고 columns와 안정적인 getRowId를 지정합니다. tableProps에는 data, onChangeData, onViewportRequest, viewportDatasource, virtualized: true가 포함됩니다. 해당 콜백을 덮어쓰면 직접 상태 연동이 필요합니다.
queryKey·rowCount·blockSize·cacheSize 변경은 hook snapshot을 새로 만듭니다. 조회 순서나 필터가 바뀌면 queryKey도 바꾸십시오. getRows 함수 참조만 바꾸는 것은 데이터 초기화가 아닙니다. abort된 요청·이전 초기 상태의 응답은 반영하지 않고, 조회 예외는 오류 요청 상태로 변환합니다.
CominsViewportOptions
섹션 제목: “CominsViewportOptions”export type CominsViewportOptions<T> = { rowCount: number; queryKey: CominsViewportRevision; getRows: (request: CominsViewportRequest) => readonly T[] | Promise<readonly T[]>; blockSize?: number; cacheSize?: number; maxConcurrentRequests?: number; heightCacheSize?: number; onEdit?: (changes: readonly CominsViewportPatch<T>[]) => void;};| 속성 | 동작·기본값 |
|---|---|
rowCount |
필수. 전체 건수인 0 이상의 안전한 정수. 알 수 없으면 먼저 count를 조회합니다. |
queryKey |
필수. 문자열/숫자 dataset revision. 검색·정렬·인덱스 순서가 달라지면 변경합니다. |
getRows |
필수. [startIndex, endIndex) 범위와 signal을 받습니다. 정확히 endIndex − startIndex개의 null/undefined 없는 행 배열을 반환합니다. 마지막 블록도 동일합니다. |
blockSize |
기본 100행. 양의 안전한 정수가 아니면 기본값을 사용합니다. |
cacheSize |
기본 12블록. 현재 범위와 활성 요청 블록 보존 때문에 실제 보관량이 한도를 넘을 수 있습니다. |
maxConcurrentRequests |
기본 2. Table의 동시 범위 요청 수. 양의 안전한 정수가 아니면 2입니다. |
heightCacheSize |
기본 64블록. 데이터 캐시와 별도로 행 높이 측정값을 보관합니다. |
onEdit |
선택. 절대 인덱스와 새 행의 patch 배열 알림. 동기 호출 후 로컬 snapshot에 반영합니다. 서버 저장은 앱 책임이며, 캐시 퇴출 후 재조회 시 미저장값을 다시 합쳐야 합니다. |
지원 범위
섹션 제목: “지원 범위”고정/자동 행 높이, 컬럼 고정·크기 조절, 로딩된 행의 편집을 지원합니다. Tree·Row Grouping·Row Detail·행 이동·append 로딩·pagination과 결합하지 않습니다. 정렬·필터는 외부 조회 조건으로 처리합니다. 서버 push나 미수신 행 자동 다운로드는 제공하지 않습니다.