백엔드 면접 용어 사전
API·RESTcursor pagination · 커서 · cursor

커서 페이지네이션

마지막 항목의 기준값 이후를 조회하는 방식. OFFSET의 깊은 페이지 성능 저하와 누락·중복을 피한다.

"몇 번째부터"가 아니라 "이 지점 이후" 로 다음 페이지를 요청하는 방식.

오프셋 방식의 문제

SELECT * FROM posts ORDER BY id DESC LIMIT 20 OFFSET 100000;

문제 ① — 뒤로 갈수록 느려진다

DB는 OFFSET만큼을 건너뛰기 위해 실제로 읽는다. 100,020건을 읽어 100,000건을 버리고 20건만 반환한다.

OFFSET 0      →  5ms
OFFSET 10000  →  50ms
OFFSET 100000 →  500ms
OFFSET 1000000 → 5s      ← 선형으로 나빠진다

문제 ② — 데이터가 밀린다

1페이지 조회: [글100, 글99, …, 글81] 그 사이 새 글이 3개 등록됨 2페이지 조회(OFFSET 20): [글83, 글82, …]

  • 글81, 82, 83 이 중복으로 보인다
  • 반대로 글이 삭제되면 건너뛴다

무한 스크롤에서 같은 글이 반복되는 현상의 원인이다.

커서 방식

SELECT * FROM posts
WHERE id < 100          -- 마지막으로 본 지점
ORDER BY id DESC
LIMIT 20;
{ "items": [...], "nextCursor": "80" }

다음 요청은 ?cursor=80.

왜 빠른가

  • 인덱스에서 id=100 위치를 바로 찾아(O(log n)) 거기서 20건만 읽는다
  • 몇 페이지째든 항상 같은 속도

건너뛸 것을 읽지 않는다는 것이 핵심이다.

데이터가 밀려도 안전한 이유

기준이 "몇 번째"가 아니라 "어떤 값 이후"이기 때문이다. 중간에 글이 추가되거나 삭제돼도 "id가 80보다 작은 것"은 변하지 않는다.

정렬 기준이 유일하지 않으면 — 복합 커서

-- created_at 만으로는 같은 시각의 행에서 누락·중복이 생긴다
WHERE (created_at, id) < ('2024-01-01 10:00:00', 500)
ORDER BY created_at DESC, id DESC;

튜플 비교를 쓰면 정확하다. MySQL은 이 문법을 지원하지만 인덱스 활용을 확인해야 하고, 지원이 애매하면 풀어 쓴다.

WHERE created_at < ? OR (created_at = ? AND id < ?)

(created_at, id) 복합 인덱스가 반드시 있어야 한다.

커서 인코딩

  • 커서를 그대로 노출하면 내부 구조가 드러난다

  • Base64 로 감싸 불투명하게 만든다

  • {"createdAt":"2024-01-01T10:00:00","id":500}

  • eyJjcmVhdGVkQXQiOiIyMDI0LTAx…

암호화가 아니므로 보안 수단은 아니다. 클라이언트가 내부 값에 의존해 파싱하는 것을 막고, 나중에 구조를 바꿀 여지를 남기는 것이 목적이다.

한계 — 커서가 항상 답은 아니다

❌ "7페이지로 바로 가기" 불가능 — 순차적으로만 이동 ❌ 전체 페이지 수를 알기 어렵다 ❌ 임의 정렬(가격순, 인기순)을 바꾸면 커서가 무효

언제 무엇을 쓰나

상황방식
무한 스크롤·피드커서
실시간으로 데이터가 추가되는 목록커서
데이터가 많은 API커서
관리자 화면(페이지 번호로 이동해야)오프셋
데이터가 적고 정적오프셋 (단순함이 이득)

오프셋을 유지해야 한다면 — 지연 조인

SELECT p.* FROM posts p
JOIN (SELECT id FROM posts ORDER BY id DESC LIMIT 20 OFFSET 100000) t
  ON p.id = t.id;

안쪽 서브쿼리가 커버링 인덱스로만 처리되어 버릴 행의 본문을 안 읽는다. 근본 해결은 아니지만 상당히 개선된다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — API·REST 설계 — 멱등성·상태코드·버저닝·GraphQL