개요

Prisma에서 cursor는 특정 레코드 지점부터 결과를 읽기 시작하는 기준점으로 동작함 skip: 1은 해당 cursor 레코드를 결과에서 제외하기 위한 옵션으로, 페이지 간 중복을 제거하는 데 사용함

핵심 동작

  • cursor는 그 지점부터 시작
await prisma.user.findMany({
  cursor: { id: 100 },
  take: 5,
  orderBy: { id: "asc" },
});
// 결과: 100부터 시작해 5개 반환
  • skip: 1은 cursor에 해당하는 레코드를 건너뜀
await prisma.user.findMany({
  cursor: { id: 100 },
  skip: 1,
  take: 5,
  orderBy: { id: "asc" },
});
// 결과: 101부터 5개 반환

예시로 보는 차이

데이터가 아래와 같다고 가정

98, 99, 100, 101, 102, 103, 104, 105, 106, 107

  • skip 없이
cursor: { id: 100 }, take: 3, orderBy: { id: 'asc' }
// 결과: [100, 101, 102]
  • skip: 1과 함께
cursor: { id: 100 }, skip: 1, take: 3, orderBy: { id: 'asc' }
// 결과: [101, 102, 103]

왜 skip: 1을 쓰는가

페이지 간 중복 방지 목적

// 첫 페이지
const page1 = await prisma.user.findMany({
  take: 3,
  orderBy: { id: "asc" },
});
// 결과: [98, 99, 100]
// nextCursor = 100

// 다음 페이지
const page2 = await prisma.user.findMany({
  cursor: { id: 100 },
  skip: 1,
  take: 3,
  orderBy: { id: "asc" },
});
// 결과: [101, 102, 103]  중복 없음

코드 패턴 요약

  • 더보기 여부 판단을 위해 take를 limit + 1로 지정하는 패턴 권장
  • nextCursor는 반환 목록의 마지막 id 사용
const data = await prisma.user.findMany({
  take: limit + 1,
  ...(cursor && { cursor: { id: cursor }, skip: 1 }),
  orderBy: { id: "asc" },
});

const hasMore = data.length > limit;
const items = hasMore ? data.slice(0, -1) : data;
const nextCursor = items.length ? items[items.length - 1].id : null;

return { data: items, hasMore, nextCursor };

내부 처리 관점

사용자 질문 요지: take: 501, skip: 1, cursor: { id: 1 }이라면 실제로 1부터 502까지 502개의 로우를 확인하는가

  • 인덱스 관점에서 맞음
  • 일반적인 실행 흐름
    • 인덱스 탐색으로 id = 1 위치 도달
    • 그 항목은 skip으로 제외
    • 이후 연속된 501개 항목을 읽어 반환
  • 결과적으로 인덱스 레벨에서 502개 항목을 방문하는 효과가 발생
  • 클라이언트로 반환되는 레코드 수는 501개

SQL 관점 설명

Prisma의 cursor 기반 페이지네이션은 보통 OFFSET을 쓰지 않고 정렬 컬럼 기준 범위 조건으로 변환됨 단일 정렬 키 id 기준 오름차순의 경우 개념적으로 아래와 유사한 형태

SELECT ...
FROM user_nickname_log
WHERE id > 1           -- cursor: { id: 1 }, skip: 1일 때의 개념적 조건
ORDER BY id ASC
LIMIT 501               -- take만큼 제한
  • skip: 1 없이 cursor만 쓰면 WHERE id >= 1에 가까운 조건으로 해석됨
  • skip: 1을 붙이면 WHERE id > 1로 바뀌는 효과가 나서 중복 제거
  • 실제 생성 SQL과 실행 계획은 데이터베이스와 드라이버, 정렬 키 구성에 따라 다를 수 있음

핵심은 OFFSET으로 앞 페이지를 건너뛰지 않고 인덱스 범위 스캔으로 이어가기 때문에 커서 기반 페이지네이션의 성능 특성이 보장된다는 점임

정리

  • cursor는 기준 레코드 지점부터 시작
  • skip: 1은 기준 레코드를 제외해 중복 제거
  • limit + 1 패턴으로 hasMore를 안정적으로 판정
  • 인덱스 관점에서 cursor 레코드 포함 총 502개 항목을 방문하고 501개만 반환하는 해석 가능
  • Prisma는 일반적으로 WHERE 범위 조건과 LIMIT을 활용해 cursor 기반 페이지네이션을 수행하며 OFFSET 기반 페이지네이션과 다름

현재와 같은 구현은 동작 의도와 성능 측면 모두에서 타당함

참고자료