커서 기반 페이지네이션 vs 오프셋 페이지네이션: API는 무엇을 선택해야 할까?

커서 기반 페이지네이션과 오프셋 페이지네이션 비교: 페이지 밀림, 깊은 오프셋 비용, 키셋 SQL, Stripe 및 Slack 예시, 그리고 Apidog에서 둘 다 테스트하는 방법.

INEZA Felin-Michel

INEZA Felin-Michel

31 August 2026

커서 기반 페이지네이션 vs 오프셋 페이지네이션: API는 무엇을 선택해야 할까?

Apidog 엔터프라이즈

온프레미스 배포

SSO & RBAC

SOC 2 준수

Apidog Enterprise 살펴보기

모든 목록 엔드포인트는 결국 동일한 질문에 직면합니다: 2백만 개의 주문을 클라이언트가 탐색할 수 있는 페이지로 어떻게 나눌 것인가? 오프셋 페이지네이션을 선택하면 간단한 SQL과 사용자가 이해할 수 있는 페이지 번호를 얻을 수 있습니다. 커서 기반 페이지네이션을 선택하면 안정적인 결과와 어떤 깊이에서도 일관된 대기 시간을 얻을 수 있지만, "47페이지로 이동"하는 기능은 포기해야 합니다.

대부분의 팀은 모든 튜토리얼의 기본값인 오프셋을 선택합니다. 그러다 주문 테이블이 수백만 개의 행에 도달하면 4,000페이지부터 시간 초과가 발생하고, 사용자는 스크롤하는 동안 동일한 레코드를 두 번 본다고 보고합니다. 이 가이드는 두 가지 방식이 어떻게 작동하는지, 오프셋이 어디에서 문제가 발생하는지, Stripe와 Slack이 왜 커서 방식을 사용하는지, 그리고 Apidog에서 연결된 요청으로 두 가지 방식을 테스트하는 방법을 다룹니다. 이 가이드를 마치면 어떤 방식이 당신의 엔드포인트에 정확히 맞는지 알게 될 것입니다.

전반적인 내용을 먼저 알고 싶다면, 저희 API 페이지네이션 가이드가 모든 전략을 나란히 다루고 있습니다. 이 글은 가장 중요한 두 가지 방식에 대해 깊이 있게 다룹니다.

오프셋 페이지네이션 작동 방식

오프셋 페이지네이션은 SQL과 직접적으로 매핑됩니다. 클라이언트가 페이지 번호와 페이지 크기를 보내면, 서버는 이를 LIMITOFFSET으로 변환합니다.

SELECT id, customer_id, total_cents, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 25 OFFSET 50;

이 쿼리는 페이지당 25개 행으로 주문 목록의 3페이지를 반환합니다. 요청은 다음과 같습니다:

GET /v1/orders?page=3&per_page=25

그리고 일반적인 응답은 다음과 같습니다:

{
  "data": [
    {
      "id": "ord_8821",
      "customer_id": "cus_1932",
      "total_cents": 4599,
      "created_at": "2026-08-30T14:22:07Z"
    }
  ],
  "page": 3,
  "per_page": 25,
  "total": 1848203,
  "total_pages": 73929
}

매력은 분명합니다. 클라이언트는 어떤 페이지로든 이동할 수 있습니다. 서버는 총 개수를 반환할 수 있습니다. 어떤 개발자라도 오후에 이를 구축할 수 있습니다. 작은 관리 테이블의 경우 이것이 올바른 선택이며, REST API의 페이지네이션에 대한 저희의 단계별 가이드는 완전한 오프셋 구축 과정을 안내합니다.

하지만 오프셋에는 두 가지 구조적인 문제가 있으며, 개발 단계에서는 둘 다 나타나지 않습니다. 둘 다 프로덕션 환경에서 나타납니다.

문제 1: 페이지 드리프트 (Page Drift)

오프셋은 정렬된 결과의 맨 위에서부터 행을 계산합니다. 클라이언트가 이전에 어떤 행을 보았는지에 대해서는 아무것도 알지 못합니다. 따라서 요청 사이에 행이 삽입되거나 삭제되면, 클라이언트가 보고 있는 페이지가 아래에서 이동합니다.

예를 들어, 사용자가 최신순으로 정렬된 주문의 1페이지(1번부터 25번 행)를 로드한다고 가정해봅시다. 그들이 읽는 동안 3개의 새 주문이 도착합니다. 그들이 OFFSET 25인 2페이지를 요청하면, 첫 번째 응답의 23, 24, 25번 행이 이제 26번부터 28번 위치로 밀려납니다. 사용자는 이 행들을 다시 보게 됩니다. 중복입니다.

삭제는 반대 상황을 만듭니다. 사용자가 1페이지를 읽는 동안 3개 행이 삭제되면, OFFSET 25는 사용자가 보지 못한 3개 행을 건너뛰게 됩니다. 이는 조용한 데이터 손실이며 아무도 오류를 받지 않습니다.

실시간으로 스크롤하지 않는 월간 보고서의 경우 드리프트는 무해합니다. 활동 피드, 동기화 엔드포인트 또는 쓰기가 계속되는 동안 스크립트가 페이지별로 이동하는 모든 것에서는 드리프트가 중복되거나 누락된 레코드를 의미합니다. 사용자는 이를 알아차립니다.

문제 2: 깊은 오프셋은 건너뛰는 모든 것을 스캔합니다

OFFSET 500000은 500,001번째 행으로 순간 이동하지 않습니다. 데이터베이스는 인덱스를 통해 50만 개의 항목을 탐색하고, 이들을 버린 다음 25개의 행을 반환합니다. 비용은 깊이에 따라 선형적으로 증가합니다: n이 오프셋일 때 O(n)입니다.

구체적인 숫자로 이를 실제화해 봅시다. 2백만 개의 행이 있고 created_at에 인덱스가 있는 Postgres 주문 테이블에서:

Markus Winand의 no-offset writeup on Use The Index, Luke는 쿼리 계획으로 이 비용을 시연하며 전체를 읽어볼 가치가 있습니다. 프로덕션 환경의 패턴은 높은 오프셋 요청으로 가득 찬 느린 쿼리 로그인데, 이는 종종 퍼블릭 API의 모든 페이지를 충실히 탐색하는 한 크롤러에서 비롯됩니다. 클라이언트 하나만으로도 p99 지연 시간이 두 배로 늘어납니다.

커서 기반 페이지네이션 작동 방식

커서 기반 페이지네이션은 키셋 페이지네이션이라고도 불리며, 행 카운터를 사용하지 않습니다. 클라이언트는 "50개 행을 건너뛰라"는 대신 "이 특정 레코드 다음의 행을 달라"고 요청합니다. 커서는 클라이언트가 본 마지막 행을 식별하므로, 서버는 다음 배치로 직접 탐색할 수 있습니다.

SQL은 OFFSET 대신 정렬 키에 대한 행 비교를 사용합니다:

SELECT id, customer_id, total_cents, created_at
FROM orders
WHERE (created_at, id) < ('2026-08-30T14:22:07Z', 'ord_8821')
ORDER BY created_at DESC, id DESC
LIMIT 25;

두 열 비교에 주목하십시오. created_at만으로는 고유하지 않습니다. 두 주문이 같은 밀리초에 발생할 수 있으며, 고유하지 않은 정렬 키는 페이지 경계에서 행이 건너뛰거나 반복될 수 있음을 의미합니다. id를 동점 처리기로 추가하면 순서가 총괄적이고 페이지네이션이 정확해집니다. (created_at, id)에 복합 인덱스가 있으면 데이터베이스는 경계로 바로 찾아가 25개 항목을 읽습니다. 1페이지와 60,000페이지의 비용은 동일합니다.

하지만 API는 이러한 원시 값을 노출해서는 안 됩니다. 실제 구현에서는 정렬 키를 일반적으로 base64로 인코딩된 불투명한 토큰으로 변환합니다:

GET /v1/orders?limit=25&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNDoyMjowN1oiLCJpZCI6Im9yZF84ODIxIn0

불투명성은 그 자체를 위한 난독화가 아니라 설계 결정입니다. 커서를 파싱할 수 없는 클라이언트는 수동으로 URL을 생성할 수 없으므로, 정렬 키를 변경하거나 샤드 힌트를 추가하거나 저장 엔진을 전환하더라도 누구에게도 문제를 일으키지 않을 자유를 얻게 됩니다. 계약은 "우리가 준 것을 돌려줘라"는 것 이상이 아닙니다.

절충점: 47페이지가 없습니다. 커서는 "이 행 다음"만 알기 때문에 클라이언트는 한 번에 한 페이지씩 앞으로(이전 커서를 발행하면 뒤로도) 이동합니다. 총 개수도 무료로 제공되지 않습니다. 개수 세기는 별도의 쿼리입니다. 데이터셋 자체가 방대한 설계를 위해서는 수백만 개의 레코드를 위한 API 페이지네이션 설계에 대한 저희 가이드가 스케일링 측면을 더 깊이 다룹니다.

한눈에 보는 장단점

차원 오프셋 페이지네이션 커서 기반 페이지네이션
임의 페이지로 이동 예, 모든 페이지 번호 아니오, 순차적 탐색만 가능
총 개수 / 페이지 개수 포함하기 저렴 별도의 개수 쿼리 필요
깊은 페이지 성능 O(n), 깊이에 따라 저하 어떤 깊이에서도 페이지당 O(1)
쓰기 시 안정성 드리프트: 중복 및 누락 안정적, 행에 고정됨
구축 비용 사소함 중간: 인코딩, 동점 처리, 인덱스 설계
정렬 요구 사항 모든 ORDER BY 작동 고유하고 인덱싱된 정렬 키 필요
페이지 URL 캐싱 쉬움, URL 예측 가능 어려움, 커서는 탐색마다 다름
클라이언트 복잡성 낮음 낮음, 응답 형식이 깔끔하다면

이 표에서 한 가지 미묘한 점은 강조할 가치가 있습니다: 커서 페이지네이션은 확정적인 정렬을 요구합니다. 만약 엔드포인트가 클라이언트가 status와 같이 변경 가능하고 고유하지 않은 열로 정렬하도록 허용한다면, 키셋 로직은 빠르게 고통스러워집니다. 오프셋은 느슨한 정렬을 용인하지만, 커서는 이를 용납하지 않습니다.

어떤 것을 선택해야 할까요?

데이터가 소비되는 방식에 맞게 스타일을 선택하십시오.

관리 테이블 및 대시보드: 오프셋. 수천 개의 행, 페이지 번호를 클릭하는 사람, 그리고 눈에 보이는 "1,848개 결과"가 있는 내부 도구. 드리프트는 중요하지 않고, 깊이는 얕게 유지되며, 페이지 이동은 실제 기능입니다. 오프셋은 구축 비용 면에서 우세합니다.

무한 스크롤 피드: 커서. 누구도 피드의 47페이지로 이동하지 않습니다. 사용자는 "더 보기"만 로드하고, 쓰기는 계속 발생하며, 중복은 눈에 띄고 당황스럽습니다. 이것이 교과서적인 커서 사례입니다.

퍼블릭 API: 커서. 소비자를 통제할 수 없습니다. 누군가는 모든 페이지를 탐색하는 루프를 작성할 것이고, 오프셋을 사용하면 깊은 페이지가 새벽 3시에 문제가 될 수 있습니다. 커서는 모든 페이지를 저렴하게 유지하며 불투명한 토큰 뒤에서 내부를 발전시킬 수 있도록 합니다. 저희 REST API 페이지네이션 가이드는 URL 및 헤더 규칙을 자세히 다룹니다.

내보내기 및 동기화 작업: 커서. 2백만 개의 모든 주문을 가져오는 배치 작업은 두 가지 보장을 필요로 합니다: 동시 쓰기에도 불구하고 누락되는 행이 없어야 하며, 페이지당 비용이 일정해야 합니다. 오프셋은 둘 다 제공하지 않습니다. 또한 커서는 작업이 140만 번째 행에서 중단될 때 무료 재개 지점을 제공합니다.

솔직한 경험 법칙: 작고 사람이 탐색하며 개수가 중요한 인터페이스에는 오프셋을, 크고 실시간이거나 공개적인 모든 것에는 커서를 사용하십시오.

실제 API는 어떻게 처리할까요?

Stripe는 완전히 커서 기반입니다. 모든 목록 엔드포인트는 starting_after(객체 ID)와 limit를 허용하며, 응답에는 has_more가 포함됩니다. 다음 결제 페이지를 가져오려면, 이전에 받은 마지막 결제의 ID를 전달합니다. Stripe 페이지네이션 문서는 이 패턴을 보여줍니다. 그들의 쓰기 볼륨을 고려할 때, 총 개수가 어디에도 없다는 점은 의도적인 생략입니다.

GitHub의 REST API는 대부분의 엔드포인트에서 여전히 pageper_page를 노출하며, 다음 페이지와 마지막 페이지를 가리키는 Link 헤더를 제공합니다. 하지만 GitHub 페이지네이션 문서를 자세히 읽어보면, 클라이언트에게 페이지 URL을 직접 구성하는 대신 Link 헤더를 그대로 따르도록 지시하며, 최근 엔드포인트들은 커서 방식으로 전환되었습니다. 이는 대규모 리포지토리에서 깊은 오프셋 탐색이 성능에 해로웠기 때문입니다.

Slack은 웹 API를 커서 페이지네이션으로 마이그레이션했으며, 이제 모든 새로운 메서드가 사용하는 접근 방식으로 표시하고 있습니다. conversations.history와 같은 메서드는 response_metadata.next_cursor를 반환하며, 빈 커서 문자열은 끝에 도달했음을 의미합니다. 이는 Slack 페이지네이션 문서에 설명되어 있습니다.

세 개의 트래픽이 많은 API, 그리고 방향은 한 가지입니다: 커서 방식.

응답 엔벨로프 설계

커서 API는 응답 엔벨로프에 따라 성패가 갈립니다. 지루하고 예측 가능하게 유지하십시오:

{
  "data": [
    {
      "id": "ord_8846",
      "customer_id": "cus_2201",
      "total_cents": 12900,
      "created_at": "2026-08-30T16:01:44Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNjowMTo0NFoiLCJpZCI6Im9yZF84ODQ2In0"
}

네 가지 규칙이 이를 견고하게 만듭니다:

Apidog에서 두 가지 스타일 테스트하기

페이지네이션 버그는 경계에 숨어 있습니다: 마지막 페이지, 빈 페이지, 앵커 행이 삭제된 커서. 수동 클릭으로는 이러한 버그를 잡을 수 없지만, 연결된 테스트 시나리오는 가능하며, 이곳이 Apidog가 워크플로우에서 그 자리를 차지하는 지점입니다.

커서 엔드포인트의 경우, 두 단계로 테스트 시나리오를 구축하십시오:

  1. 엔드포인트를 호출하고 커서를 추출합니다. 첫 번째 요청에 $.next_cursor JSONPath를 사용하여 후처리기를 추가하고, 이를 nextCursor와 같은 변수에 저장하십시오. Apidog는 응답 패널에서 JSONPath를 직접 복사할 수 있도록 합니다. 전체 절차는 JSONPath로 어설션 설정 및 변수 추출 방법에 있습니다.
  2. 다음 페이지 요청을 반복합니다. 두 번째 요청을 ForEach 또는 루프 단계로 묶고, {{nextCursor}}를 커서 매개변수로 전달하고, 각 반복마다 $.next_cursor를 다시 추출하며, has_more가 false일 때 종료하십시오. 매번 이전 페이지에서 id가 반복되지 않고 페이지 크기가 limit를 초과하지 않는지 어설션하십시오.

오프셋 엔드포인트의 경우, 동일한 구조가 카운터 변수와 함께 적용됩니다: page를 증가시키고, 마지막 페이지까지 data 길이가 per_page와 같은지 어설션하며, 탐색 전반에 걸쳐 total이 일관되게 유지되는지 어설션하십시오.

그런 다음 각 단계에 명시적인 어설션과 함께 예외 상황을 추가하십시오:

시나리오가 로컬에서 통과하면, 모든 병합 시 CI에서 실행하십시오. Apidog를 무료로 다운로드하면 루프 및 어설션을 포함한 전체 커서 탐색 시나리오를 30분 이내에 실행할 수 있습니다.

자주 묻는 질문 (FAQ)

커서 페이지네이션이 항상 더 좋은가요?

아니오. 오프셋은 대부분의 내부 관리 도구를 설명하는 중간 규모의 데이터셋에 대해 사용자가 페이지 번호, 총계, 무작위 접근을 필요로 할 때 더 적합합니다. 커서는 데이터셋이 크거나, 쓰기가 빈번하거나, API가 공개될 때 더 좋습니다. 실패 모드는 공용 목록 엔드포인트에 오프셋을 기본으로 사용하다가 출시 후 O(n) 비용을 발견하는 것입니다.

커서 페이지네이션으로 총 개수를 어떻게 얻나요?

동일한 필터로 별도의 SELECT COUNT(*)를 실행하십시오. 이는 별도의 엔드포인트가 될 수도 있고 include_count=true와 같은 선택적 쿼리 매개변수일 수도 있습니다. 적극적으로 캐싱하십시오. 1분마다 새로 고쳐지는 대략적인 개수는 거의 모든 UI를 만족시킵니다. Stripe는 총계를 완전히 건너뛰는데, 이는 클라이언트가 총계를 얼마나 자주 필요로 하는지 알려줍니다.

하나의 엔드포인트에서 두 가지 페이지네이션 스타일을 모두 제공할 수 있나요?

그럴 수 있으며, GitHub는 전환 기간 동안 효과적으로 그렇게 했습니다. 하지만 새로운 API에서는 피하십시오. 두 가지 스타일은 두 가지 예외 상황 세트, 두 가지 테스트 매트릭스, 그리고 어떤 것을 사용해야 할지에 대한 클라이언트의 혼란을 의미합니다. 엔드포인트당 하나를 선택하십시오. 처음부터 계약을 설계하는 경우, 저희 REST API 페이지네이션 가이드의 패턴은 모든 표면에서 매개변수 명칭을 일관되게 유지하는 데 도움이 될 것입니다.

커서의 앵커 행이 삭제되면 어떻게 되나요?

키셋 페이지네이션을 사용하면 아무것도 고장 나지 않습니다. WHERE (created_at, id) < (?, ?) 비교는 앵커 행이 존재할 것을 요구하지 않습니다. 이는 경계 위치를 찾아 계속 진행합니다. 이것은 "행 조회로서의 커서" 설계에 비해 실제적인 이점이며, 소비자가 발견하기 전에 Apidog 테스트 시나리오에서 어설션할 가치가 있는 예외 상황입니다.

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요