Communitygithub.com

YepAPI/skills

커서 기반 페이지네이션, IntersectionObserver, @tanstack/react-virtual을 사용한 가상화 리스트, 스크롤 위치 복원

skills란 무엇인가요?

이 스킬은 대량의 데이터 리스트를 효율적으로 렌더링하고 사용자 경험을 향상시키기 위해 설계된 무한 스크롤 구현 가이드입니다. 커서 기반 페이지네이션을 사용하여 데이터 삽입/삭제 시 발생하는 오프셋 문제를 방지하며, IntersectionObserver를 통해 사용자가 리스트 끝에 도달하기 전에 데이터를 미리 가져옵니다. @tanstack/react-virtual을 활용하여 1,000개 이상의 아이템을 가진 리스트에서도 DOM에 보이는 행만 렌더링하여 성능을 최적화합니다. 또한, 로딩 스켈레톤, '더 보기' 버튼, 빈 상태 및 끝 상태 표시, 스크롤 위치 복원 등 다양한 사용자 인터페이스 패턴을 포함하여 완성도 높은 무한 스크롤 환경을 제공합니다. 이 스킬은 주로 React와 React Query 환경에서 사용되며, Cursor, Claude Code, Codex 등 다양한 AI 에이전트에서 활용할 수 있습니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/YepAPI/skills/tree/HEAD/skills/infinite-scroll

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

무한 스크롤

규칙

  • 커서 기반 페이지네이션: 마지막 아이템의 ID 또는 타임스탬프를 커서로 사용 — 삽입/삭제 시 문제가 발생하는 오프셋 사용 금지
  • IntersectionObserver: 리스트 하단의 감시 요소(sentinel)에 연결 — 가시화될 때 fetch 트리거
  • 가상화: @tanstack/react-virtual을 1,000개 이상 아이템 리스트에 사용 — DOM에 보이는 행만 렌더링
  • 로딩 스켈레톤: 다음 페이지를 가져오는 동안 하단에 플레이스홀더 아이템 표시 — 스피너 사용 금지
  • "더 보기" 버튼: 수동 제어를 선호하거나 느린 연결 상태의 사용자를 위한 대체 수단 제공
  • 스크롤 위치 복원: 탐색 전 스크롤 위치 저장, 뒤로 가기 시 복원 — sessionStorage 또는 라우터 상태 사용
  • 빈 상태: 리스트에 아이템이 없을 때 메시지 표시 — 빈 페이지 금지
  • 리스트 끝: hasNextPage가 false일 때 "끝까지 도달했습니다" 표시 — observer 중단

패턴

// React Query + IntersectionObserver를 사용한 무한 스크롤
import { useInfiniteQuery } from "@tanstack/react-query";
import { useRef, useEffect } from "react";

function InfiniteList() {
  const sentinelRef = useRef<HTMLDivElement>(null);
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ["items"],
    queryFn: ({ pageParam }) => fetchItems({ cursor: pageParam, limit: 20 }),
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
    initialPageParam: undefined as string | undefined,
  });

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => { if (entries[0].isIntersecting && hasNextPage) fetchNextPage(); },
      { rootMargin: "200px" } // 사용자가 하단에 도달하기 전에 미리 가져오기
    );
    if (sentinelRef.current) observer.observe(sentinelRef.current);
    return () => observer.disconnect();
  }, [hasNextPage, fetchNextPage]);

  const allItems = data?.pages.flatMap((page) => page.items) ?? [];

  return (
    <div>
      {allItems.map((item) => <ItemCard key={item.id} item={item} />)}
      {isFetchingNextPage && <LoadingSkeleton count={3} />}
      {!hasNextPage && allItems.length > 0 && <p>끝까지 도달했습니다</p>}
      {allItems.length === 0 && !isFetchingNextPage && <EmptyState />}
      <div ref={sentinelRef} />
    </div>
  );
}

// 10,000개 이상 아이템을 위한 가상화 리스트
import { useVirtualizer } from "@tanstack/react-virtual";

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 64, // 예상 행 높이 (px)
    overscan: 5,
  });

  return (
    <div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
        {virtualizer.getVirtualItems().map((virtualRow) => (
          <div key={virtualRow.key} style={{
            position: "absolute", top: 0, left: 0, width: "100%",
            transform: `translateY(${virtualRow.start}px)`,
          }}>
            <ItemCard item={items[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

피해야 할 것

  • 오프셋 기반 페이지네이션 — 삽입/삭제로 인해 모든 오프셋이 이동하여 중복 또는 누락된 아이템 발생
  • 모든 아이템을 로드한 후 클라이언트에서 필터링 — 서버에서 현재 페이지만 가져오기
  • 10,000개 이상의 DOM 노드 렌더링 — @tanstack/react-virtual을 사용하여 보이는 행만 렌더링
  • rootMargin 없는 IntersectionObserver — 100-200px 여백을 추가하여 사용자가 하단에 도달하기 전에 미리 가져오기
  • 리스트 끝 표시 누락 — 사용자는 더 이상 로드할 항목이 없음을 알아야 함

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

YepAPI/skills

WCAG 2.1 AA — semantic HTML, keyboard navigation, screen readers.

YepAPI/skills

Experiment setup, variant splitting, statistical significance, and feature flag integration.

YepAPI/skills

CRUD generators, data tables, user management, role-based access, and bulk operations.

YepAPI/skills

Monitor what ChatGPT and Gemini say about your brand using YepAPI.

YepAPI/skills

Queue workers with BullMQ/Inngest/Trigger.dev, job retries, dead letter queues, concurrency.

YepAPI/skills

Commander.js, interactive prompts with Clack, spinners, colors, config files, exit codes, and npm publishing.

YepAPI/skills

cmdk integration, Cmd+K trigger, fuzzy search, grouped actions, keyboard navigation, and dynamic action registration.

YepAPI/skills

Content planning with trend and sentiment data from YepAPI.

YepAPI/skills

Content calendar, keyword-driven planning, and content briefs.

YepAPI/skills

Theme switching with CSS variables, system preference detection, next-themes, and persistence.

YepAPI/skills

Threaded comments, @mentions, reactions, moderation queue, optimistic UI, and cursor pagination.

YepAPI/skills

Competitive analysis between domains using YepAPI.

YepAPI/skills

Layout grids, KPI cards, data tables with sorting/filtering, sidebar navigation, and responsive breakpoints.

YepAPI/skills

Data encryption — TLS everywhere, AES-256-GCM for PII, bcrypt passwords, encrypted backups.

YepAPI/skills

Chart patterns with Recharts/Nivo/D3, responsive containers, color scales, and accessibility.

YepAPI/skills

Drizzle schema definition, push vs migrate, type-safe queries, relations, transactions, and prepared statements.

YepAPI/skills

Upload handling, resize/compress, CDN delivery, blur placeholders, and next/image patterns.

YepAPI/skills

Local business and Google Maps research using YepAPI.

YepAPI/skills

Tenant isolation (row-level vs schema), subdomain routing, per-tenant config, and data partitioning.

YepAPI/skills

Chat UI components, streaming responses with AI SDK, context window management, and RAG patterns.

관련 스킬