관리자 화면에서 주문 목록 첫 페이지를 본 뒤 ‘다음’을 눌렀는데 방금 본 주문이 또 나타난다. 새 주문이 들어와 맨 앞에 끼어들면서 뒤의 항목이 한 칸씩 밀렸기 때문이다. 반대로 삭제가 일어나면 아직 보지 못한 항목 하나를 건너뛸 수도 있다. 데이터가 계속 바뀌는 목록에서 페이지 번호는 위치를 기억하지만, 마지막으로 본 항목은 기억하지 못한다.
작은 정적 목록에는 페이지 번호가 충분할 수 있다. 문제는 주문, 문의, 로그처럼 삽입이 잦고 사용자가 오래 탐색하는 목록이다. 이때는 무엇을 기준으로 ‘다음’을 정의할지 먼저 정해야 한다. Microsoft의 페이지 결과 가이드도 고유 키 없이 정렬하면 동일 값들의 순서가 불안정해 항목이 빠질 수 있음을 설명한다.
오프셋은 변하는 목록의 위치를 가리킨다
오프셋 방식은 ‘처음 20개를 건너뛰고 다음 20개’를 요청한다. 구현과 페이지 번호 UI가 쉽다. 하지만 첫 페이지를 본 뒤 새 항목이 맨 앞에 추가되면 두 번째 요청의 21번째 항목은 이전 첫 페이지의 마지막 항목일 수 있다. 큰 오프셋을 계속 건너뛰는 조회는 데이터베이스 부담도 커질 수 있다. 물론 정렬과 저장소의 실행 방식에 따라 성능은 다르므로, 모든 오프셋을 나쁘다고 단정할 필요는 없다.

그래서 API에서 ‘최신순 목록을 계속 읽기’가 핵심이면 커서 또는 키셋 방식이 유용하다. 첫 응답은 항목과 함께 다음 커서를 준다. 다음 요청은 페이지 번호 대신 그 커서를 보내고, 서버는 마지막으로 본 정렬 키 뒤의 항목을 조회한다. Microsoft의 keyset 페이지네이션 설명도 커서에 이전 페이지의 위치를 담아 이어 읽는 구조를 보여 준다.
정렬 키는 동률을 깨는 고유값까지 포함한다
‘등록 시각 내림차순’만으로는 같은 시각에 등록된 두 항목의 순서가 불분명하다. 등록 시각 뒤에 고유 ID 같은 보조 키를 두어 전체 순서를 고정한다. 다음 페이지 조건도 두 키를 함께 사용한다. 예를 들어 마지막 항목의 시각보다 과거이거나, 시각이 같으면 ID가 더 작은 항목을 가져오는 식이다. 목록 필터와 정렬 방향에 따라 비교 방향을 맞춰야 한다. 키에 사용한 컬럼의 인덱스도 실제 조회 계획으로 확인한다.
정렬 키가 자주 바뀌는 필드라면 문제가 남는다. ‘최근 수정 시각’ 정렬 목록에서 한 항목이 수정되면 읽는 도중 앞쪽으로 이동할 수 있다. 커서는 그 순간의 모든 변경을 마법처럼 고정하지 않는다. 변경이 잦은 목록은 어떤 일관성이 필요한지 먼저 정한다. 일반 탐색에서 약간의 변동을 허용할지, 회계 보고처럼 반드시 동일한 집합을 끝까지 훑어야 할지 요구가 다르다.
커서 토큰에 필터와 권한의 문맥을 묶는다
커서를 단순히 마지막 ID만 넣은 문자열로 만들면 다른 고객사의 목록이나 다른 필터에 다시 쓰기 쉽다. 토큰은 정렬 키와 방향뿐 아니라 요청 당시의 필터, 고객사 범위, 필요하다면 만료 시각이나 데이터 기준 시점에 묶어야 한다. 사용자가 상태 필터를 ‘진행 중’에서 ‘완료’로 바꿨다면 이전 커서를 그대로 사용하지 말고 첫 페이지부터 다시 시작하게 한다. 토큰은 클라이언트가 임의 수정할 수 없는 불투명한 값으로 다루고, 매 요청마다 서버 권한을 다시 확인한다.

커서가 너무 오래 유지되면 스키마 변경, 삭제, 권한 변경을 다루기 어려워진다. 만료된 커서는 명확한 오류와 재시작 방법을 제공한다. 클라이언트가 같은 커서를 반복 요청할 때 같은 페이지가 나오는지, 페이지 크기를 변경할 때 어떤 동작을 할지도 계약으로 정한다. 무한 스크롤 UI라면 사용자가 새로고침했을 때 읽던 위치를 복원할 수 있는지 별도로 결정한다.
전수 처리에는 스냅샷이나 상한선이 필요할 수 있다
커서 방식은 일반적인 중복·누락을 줄이지만, ‘오늘 9시에 존재했던 주문을 모두 정확히 한 번 처리하라’는 요구를 보장하지는 않는다. 항목이 읽는 동안 수정·삭제되면 결과 집합 자체가 바뀌기 때문이다. 데이터 내보내기나 청구 집계처럼 전수성이 중요하다면 고정된 기준 시각, 버전, 스냅샷 또는 별도 작업 ID를 둔다. 사용자 탐색 API와 확정 집계 작업을 같은 페이지네이션 계약으로 처리하려고 하면 혼란이 생긴다.
테스트도 실제 변동을 흉내 내야 한다. 첫 페이지 뒤에 새 항목을 넣고, 같은 시각의 항목을 여러 개 만들고, 목록 중간의 항목을 삭제하고, 필터를 변경해 본다. 다른 고객사 커서를 넣었을 때 거절되는지 확인한다. 속도만 재는 테스트보다 ‘어떤 항목을 두 번 보거나 건너뛰는가’를 추적하는 테스트가 먼저다.
UI 요구도 API 계약과 연결한다. 사용자가 임의의 137페이지로 바로 이동해야 하는 보고서라면 커서 방식만으로는 불편할 수 있다. 반대로 모바일 무한 스크롤은 앞뒤 이동보다 이어 읽기가 중요하다. ‘전체 몇 건’이라는 정확한 숫자를 매번 구하는 일이 비싸거나 데이터가 계속 변한다면, 추정치인지 기준 시각이 있는 집계인지 표시한다. 페이지 번호가 필요하면 별도 검색 결과 스냅샷을 만드는 방법도 검토한다.
API 설계 체크리스트
- 목록이 정적인지 계속 바뀌는지 파악했는가?
- 정렬에 고유한 보조 키가 있는가?
- 커서에 필터·정렬·테넌트 범위를 묶었는가?
- 권한을 페이지마다 다시 확인하는가?
- 커서 만료와 필터 변경 시 재시작 경로가 있는가?
- 전수 처리 요구에는 스냅샷 또는 상한선을 별도로 정했는가?
- 삽입·삭제·동률 데이터로 중복·누락을 시험했는가?
페이지네이션은 단순히 결과를 잘라 보내는 기술이 아니다. 변하는 데이터에서 ‘다음’의 의미를 정하는 계약이다. 그 계약이 분명해야 사용자 화면도 배치 작업도 같은 목록을 믿고 읽을 수 있다.