Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9장. 페이지네이션과 대용량 조회

9.1 모든 데이터를 한 번에 가져올 수는 없다

지금까지의 예제는 대부분 다음과 같았다.

const users = await prisma.user.findMany();

데이터가 적을 때는 문제가 없다.
하지만 사용자가 10만 명, 게시글이 1억 건이 되면 이야기가 달라진다.

  • 메모리 사용량이 폭증한다
  • 응답이 느려진다
  • DB에 큰 부하가 걸린다

그래서 데이터는 “잘라서” 가져와야 한다.

👉 이때 등장하는 것이 **페이지네이션(Pagination)**이다.


9.2 Offset 기반 페이지네이션

가장 흔하게 보는 방식이다.

const page = 3;
const pageSize = 20;

const users = await prisma.user.findMany({
  skip: (page - 1) * pageSize,
  take: pageSize
});

내부적으로는 다음과 같은 SQL이 실행된다.

SELECT * FROM users
ORDER BY id
LIMIT 20 OFFSET 40;

👉 “몇 페이지로 이동” 같은 UI에 잘 맞는다

  • 1페이지, 2페이지, 3페이지 …
  • 마지막 페이지로 이동
  • 전체 개수 표시

대부분의 게시판이 이 방식을 사용한다.


9.3 Offset의 한계

Offset 방식은 직관적이지만,
데이터가 커지면 두 가지 문제가 드러난다.

1) 뒤로 갈수록 느려진다

SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 1000000;

DB는 OFFSET 1,000,000을 처리하기 위해
👉 100만 건을 모두 읽고 버린 뒤 마지막 20건을 반환한다.

페이지가 뒤로 갈수록
선형적으로 느려진다.

2) 데이터가 추가되면 결과가 흔들린다

페이지를 보는 동안 새 데이터가 들어오면

[1페이지 조회] → 새 글 등록 → [2페이지 조회]

2페이지에 1페이지에 있던 글이 다시 보이게 된다.

👉 누락 또는 중복이 발생한다


9.4 Cursor 기반 페이지네이션

이 한계를 해결하기 위해 등장한 방식이
👉 Cursor 기반 페이지네이션이다.

“몇 번째”가 아니라 “어디부터”로 자른다.

const users = await prisma.user.findMany({
  take: 20,
  cursor: { id: lastSeenId },
  skip: 1,
  orderBy: { id: 'asc' }
});

내부 SQL은 다음과 같다.

SELECT * FROM users
WHERE id > 12345
ORDER BY id
LIMIT 20;

👉 마지막으로 본 ID를 기준으로 다음 데이터를 가져온다

Cursor 방식의 장점

  • 항상 인덱스를 타기 때문에 빠르다 (성능이 페이지 위치에 영향받지 않음)
  • 새 데이터가 들어와도 결과가 흔들리지 않는다
  • 무한 스크롤에 최적

Cursor 방식의 한계

  • “3페이지로 이동” 같은 임의 페이지 이동이 불가능하다
  • 전체 페이지 수를 알기 어렵다
  • 정렬 기준이 고유하지 않으면 누락이 발생할 수 있다
    (정렬 키는 유니크해야 한다)

9.5 언제 어떤 방식을 쓸 것인가

상황추천 방식
게시판 (페이지 번호 UI)Offset
관리자 화면 (소량 데이터)Offset
무한 스크롤 (피드, 타임라인)Cursor
대용량 데이터 순회Cursor
실시간으로 데이터가 늘어나는 목록Cursor

핵심 기준은 두 가지다.

  • 임의 페이지 이동이 필요한가? → Offset
  • 데이터가 많거나 자주 추가되는가? → Cursor

9.6 페이지네이션은 “조회 전략”이다

페이지네이션은 단순히 LIMIT/OFFSET을 붙이는 일이 아니다.

  • 데이터의 크기
  • 정렬 기준
  • 사용자 UX
  • 실시간성

이 모든 것이 영향을 준다.

👉 결국 어떻게 데이터를 잘라서 보여줄 것인가에 대한 설계다.


9.7 핵심 정리

  • findMany() 한 줄로 끝나는 시대는 끝났다
  • 데이터가 커지면 “자르는 전략”이 필요하다
  • Offset은 직관적, Cursor는 확장성
  • 그리고 가장 중요한 것은

👉 데이터 규모와 UX에 맞춰 선택하는 것이다