Interval Virtualizer
숫자 축 위의 구간 데이터에서 화면과 겹치는 항목을 찾는 TypeScript 라이브러리입니다. 오디오·비디오 타임라인, 일정, 문자 범위처럼 시작·종료 좌표가 있는 데이터에 사용할 수 있습니다.
구간 조회와 화면 범위 계산은 React나 DOM에 의존하지 않습니다. React 18·19용 구독 훅은 별도 진입점으로 제공합니다.
설치
Node.js 20 이상을 지원하며 ESM과 CommonJS, TypeScript 타입을 제공합니다.
npm install @hurraey/interval-virtualizer
React 훅을 사용할 때는 애플리케이션에 React 18.2 이상 또는 React 19가 필요합니다. 순수 계산 API만 사용하면 React를 설치할 필요가 없습니다.
구간 조회
import { createIntervalIndex, queryIntervals } from '@hurraey/interval-virtualizer';
const clips = [
{ id: 'background', start: 0, end: 100 },
{ id: 'effect', start: 75, end: 78 },
{ id: 'later', start: 110, end: 120 },
];
const index = createIntervalIndex(clips);
const result = queryIntervals(index, { start: 70, end: 80 });
console.log(result.intervals.map((clip) => clip.id));
// ['background', 'effect']
- 시작 좌표와 종료 좌표는 같은 단위를 사용합니다. 초, 프레임, 픽셀, 문자 위치 모두 가능합니다.
- 조회 범위의 경계에 닿는 구간과 길이가 0인 구간도 포함합니다. 겹침 조건은
start <= range.end && end >= range.start입니다. - 결과는 입력 순서를 유지하며 원래 객체를 반환합니다. 입력 배열을 정렬하거나 수정하지 않습니다.
- 음수 좌표를 지원합니다. 유한한 숫자가 아닌 좌표와
end < start인 항목은 인덱스에서 제외합니다. - 잘못된 조회 범위에는 빈 배열과
inspectedIntervalCount: 0을 반환합니다.
인덱스는 생성 시점의 좌표를 보관합니다. 항목을 추가·삭제하거나 시작·종료 좌표를 바꾸면 다시 생성해야 합니다. 스크롤과 줌만 바뀌면 같은 인덱스를 재사용합니다.
조회 방식과 비용
시작 좌표순으로 정렬한 배열에서 이진 탐색으로 후보의 끝을 찾습니다. 각 하위 구간의 최대 종료 좌표를 저장한 세그먼트 트리를 따라가며, 화면 전에 모두 끝난 구간은 건너뜁니다.
인덱스 생성에는 정렬 비용과 항목 수에 비례하는 메모리가 필요합니다. 조회 후 입력 순서를 복원하는 정렬도 수행하므로 전체 조회가 항상 O(log N)에 끝나는 것은 아닙니다.
inspectedIntervalCount는 실제로 내려가 검사한 개별 항목 수입니다. 이진 탐색 비교, 트리 노드 방문, 정렬 비용이나 실행 시간을 나타내지 않습니다.
화면 범위 계산
import { getVirtualViewportRange } from '@hurraey/interval-virtualizer';
const range = getVirtualViewportRange({
offset: 4_100,
viewportSize: 800,
contentSize: 10_000,
chunkSize: 1_000,
overscanChunks: 2,
padding: 8,
});
console.log(range);
// { start: 1992, end: 7008 }
offset은 현재 스크롤 위치이고 viewportSize와 contentSize는 각각 화면과 전체 콘텐츠의 크기입니다. chunkSize는 조회 범위를 묶는 단위입니다. 같은 청크 안에서는 작은 스크롤 변화에도 같은 범위를 반환할 수 있습니다.
overscanChunks는 양옆에 추가할 청크 수이며 기본값은 0입니다. 소수는 내림합니다. padding은 청크 바깥에 더할 여유 크기이며 기본값은 0입니다. 청크 수를 제외한 옵션은 모두 같은 좌표 단위를 사용합니다.
결과는 콘텐츠 범위를 넘지 않습니다. 화면·콘텐츠·청크 크기가 0 이하이거나 입력이 유효하지 않으면 null을 반환합니다.
픽셀 범위를 초 단위 인덱스에 조회하려면 두 경계를 초당 픽셀 수로 나눠 사용합니다.
React 연결
@hurraey/interval-virtualizer/react의 useVirtualViewportRange는 브라우저에서 사용합니다. 서버 렌더링용 스냅샷은 제공하지 않습니다. Next.js 등에서는 클라이언트에서 마운트된 영역에 연결하세요.
import { useCallback, useRef } from 'react';
import { useVirtualViewportRange } from '@hurraey/interval-virtualizer/react';
export function ScrollRange() {
const offsetRef = useRef(0);
const listenersRef = useRef(new Set<() => void>());
const subscribe = useCallback((listener: () => void) => {
listenersRef.current.add(listener);
return () => {
listenersRef.current.delete(listener);
};
}, []);
const range = useVirtualViewportRange({
offsetRef,
subscribe,
viewportSize: 800,
contentSize: 10_000,
chunkSize: 1_000,
overscanChunks: 1,
});
return (
<div
style={{ width: 800, overflowX: 'auto' }}
onScroll={(event) => {
offsetRef.current = event.currentTarget.scrollLeft;
listenersRef.current.forEach((listener) => listener());
}}
>
<div style={{ width: 10_000 }}>{range ? `${range.start} ~ ${range.end}` : '화면 크기 측정 전'}</div>
</div>
);
}
스크롤을 처리하는 쪽에서 ref를 먼저 갱신한 뒤 구독자에게 알립니다. subscribe는 구독 해제 함수를 반환해야 합니다. 훅은 계산된 범위가 같으면 같은 스냅샷 객체를 반환하며, 언마운트할 때 구독을 해제합니다.
DOM 크기 측정, 스크롤 이벤트 등록, requestAnimationFrame 예약, 실제 UI 렌더링은 사용하는 애플리케이션에서 담당합니다. 선택 중인 항목을 화면 밖에서도 유지하는 정책 역시 애플리케이션에서 처리합니다.
공개 API
createIntervalIndex<T extends Interval>(intervals)- 구간 인덱스를 생성합니다.queryIntervals(index, range)-{ intervals, inspectedIntervalCount }를 반환합니다.getVirtualViewportRange(options)-{ start, end }또는null을 반환합니다.useVirtualViewportRange(options)- React에서 외부 스크롤 알림을 구독합니다./react에서 가져옵니다.
루트 진입점은 Interval, IntervalIndex, IntervalQueryRange, IntervalQueryResult, VirtualViewportOptions 타입을 제공합니다. /react는 VirtualViewportSubscriptionOptions 타입을 제공합니다.
개발과 검증
저장소 루트에서 실행합니다.
npm ci
npm run check
구간 조회·화면 범위·React 구독 테스트, 타입 검사, ESM·CommonJS 빌드와 실제 패키지 설치 검증을 실행합니다. 패키지 설치 검증은 임시 디렉터리에서 실행하고 종료 시 삭제합니다.