개요
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 기반 페이지네이션과 다름
현재와 같은 구현은 동작 의도와 성능 측면 모두에서 타당함