Offset 페이지네이션 vs Cursor 페이지네이션: 성능·일관성·사용성 중심으로 비교해보는 최적의 데이터 페이징
대규모 데이터 처리 시스템에서 pagination offset vs cursor 방식 중 어느 것이 더 효율적인지 알아보고, 각각의 성능과 일관성, 사용성을 비교 분석하여 프로젝트에 최적화된 페이지네이션 전략을 제시합니다.
페이지네이션이란 무엇인가
데이터베이스 페이지네이션 방법은 대용량 데이터를 작은 단위로 나누어 전송하는 핵심 기술입니다.
현대 웹 애플리케이션에서 수백만 건의 데이터를 한 번에 로드하는 것은 비현실적입니다.
사용자 경험과 시스템 성능을 동시에 고려할 때, 적절한 API 페이지네이션 전략 선택이 서비스 품질을 좌우합니다.
페이지네이션은 크게 두 가지 방식으로 구분됩니다.
전통적인 LIMIT OFFSET 방식과 최신 트렌드인 커서 페이지네이션 방식입니다.
각각의 특징과 적용 시나리오를 이해하는 것이 올바른 선택의 첫걸음입니다.
일반적인 페이지네이션 흐름
데이터 요청 → 페이지네이션 로직 적용 → 제한된 데이터 반환 → 다음 페이지 정보 제공
Offset 페이지네이션 완전 분석
Offset 페이지네이션 동작 원리
Offset pagination은 데이터베이스에서 특정 개수만큼 건너뛰고 데이터를 가져오는 방식입니다.
SQL의 LIMIT
과 OFFSET
구문을 활용하여 구현됩니다.
-- 3페이지 데이터 조회 (페이지당 10개)
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;
이 방식은 직관적이고 구현이 간단해서 많은 개발자들이 선택하는 방법입니다.
페이지 번호를 클릭하여 특정 페이지로 바로 이동하는 전통적인 UI에 최적화되어 있습니다.
Offset 방식의 장점
limit offset vs keyset 비교에서 offset 방식이 가지는 주요 장점들입니다.
구현 복잡도가 낮음
기존 SQL 지식만으로도 쉽게 구현할 수 있어 학습 비용이 적습니다.
대부분의 개발자가 익숙한 방식이므로 코드 유지보수가 용이합니다.
임의 페이지 접근 가능
사용자가 원하는 페이지 번호로 바로 이동할 수 있습니다.
검색 결과 페이지처럼 특정 페이지를 북마크하거나 공유하기 편리합니다.
전체 데이터 개수 파악 용이
COUNT(*)
쿼리를 통해 전체 페이지 수를 계산할 수 있습니다.
페이지네이션 UI에서 "1 of 100 페이지" 같은 정보 표시가 가능합니다.
Offset 방식의 심각한 단점
성능 저하 문제
OFFSET 값이 클수록 데이터베이스가 건너뛸 레코드를 일일이 세어야 합니다.
100만 번째 페이지를 조회할 때는 100만 개 레코드를 스캔해야 하므로 극심한 성능 저하가 발생합니다.
데이터 일관성 문제도 심각합니다.
페이지를 조회하는 동안 새로운 데이터가 추가되거나 삭제되면 중복 조회나 누락이 발생할 수 있습니다.
특히 실시간성이 중요한 소셜 미디어나 뉴스 피드에서는 치명적인 문제가 됩니다.
Cursor 페이지네이션 심화 가이드
Cursor 페이지네이션 핵심 개념
Cursor-based pagination 성능이 주목받는 이유는 고유한 동작 방식에 있습니다.
특정 기준점(cursor)을 설정하고, 그 지점부터 다음 데이터를 조회하는 방식입니다.
-- 커서 기반 페이지네이션 예시
SELECT * FROM posts
WHERE id > 12345
ORDER BY id ASC
LIMIT 10;
커서는 보통 고유 식별자나 타임스탬프를 사용합니다.
정렬 기반 커서 방식으로 데이터의 순서를 보장하면서도 효율적인 조회가 가능합니다.
Cursor 방식이 성능에서 우월한 이유
인덱스 스캔 최적화가 핵심입니다.
WHERE 절에서 특정 값보다 큰/작은 조건을 사용하므로
데이터베이스가 B-tree 인덱스를 효율적으로 활용할 수 있습니다.
OFFSET 방식과 달리 건너뛸 레코드를 세는 과정이 없어 일정한 성능을 보장합니다.
데이터 양이 증가해도 쿼리 성능이 일정하게 유지됩니다.
무한스크롤 페이지네이션과의 완벽한 조화
무한스크롤 페이지네이션은 사용자가 스크롤을 내릴 때마다 자동으로 다음 데이터를 로드하는 방식입니다.
소셜 미디어, 이커머스 상품 목록, 뉴스 피드 등에서 널리 사용됩니다.
커서 페이지네이션은 무한스크롤과 완벽하게 호환됩니다.
마지막으로 로드된 항목의 ID나 타임스탬프를 커서로 사용하여 다음 배치를 요청하면 됩니다.
Cursor 방식의 제약사항
임의 페이지 점프가 불가능합니다.
순차적으로만 데이터를 탐색할 수 있어서 "5페이지로 바로 이동" 같은 기능을 구현하기 어렵습니다.
전체 데이터 개수를 정확히 파악하기 어렵습니다.
페이지 번호 기반 UI보다는 "더 보기" 버튼이나 무한스크롤에 적합합니다.
성능 비교 분석
쿼리 실행 시간 비교
데이터 위치 | Offset 방식 | Cursor 방식 | 성능 차이 |
---|---|---|---|
처음 1,000개 | 5ms | 3ms | 1.7배 |
10,000번째 | 45ms | 3ms | 15배 |
100,000번째 | 350ms | 3ms | 117배 |
1,000,000번째 | 3,200ms | 3ms | 1,067배 |
데이터가 뒤쪽에 위치할수록 성능 차이는 기하급수적으로 벌어집니다.
쿼리 최적화 관점에서 보면 커서 방식이 압도적으로 우수합니다.
메모리 사용량 분석
Offset 방식은 건너뛸 레코드들을 메모리에서 처리해야 합니다.
특히 복잡한 정렬 조건이 있을 때 임시 테이블을 생성하여 메모리 사용량이 급증합니다.
Cursor 방식은 인덱스를 활용한 직접 접근이므로 메모리 효율성이 뛰어납니다.
데이터 일관성 문제 해결 방안
Offset 방식의 일관성 이슈
실시간으로 데이터가 변경되는 환경에서 offset 방식은 심각한 문제를 야기합니다.
사용자가 2페이지를 보고 있는 동안 새 게시물이 추가되면, 3페이지를 요청했을 때 2페이지에서 봤던 내용이 다시 나타날 수 있습니다.
이는 사용자 경험을 크게 해치는 요소입니다.
Cursor 방식의 일관성 보장
커서 페이지네이션은 특정 지점을 기준으로 다음 데이터를 조회하므로 중복이나 누락 문제가 발생하지 않습니다.
새로운 데이터가 추가되어도 기존 커서 위치는 변하지 않습니다.
데이터 일관성이 중요한 금융 시스템이나 실시간 채팅에서는 커서 방식이 필수적입니다.
하이브리드 접근법
두 방식의 장점을 결합한 하이브리드 접근법도 고려할 수 있습니다.
초기 몇 페이지는 offset 방식으로 임의 접근을 허용하고,
특정 지점 이후부터는 커서 방식으로 전환하는 방법입니다.
// 하이브리드 페이지네이션 예시
if (page <= 5) {
// Offset 방식 사용
const offset = (page - 1) * limit;
query = `SELECT * FROM posts ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`;
} else {
// Cursor 방식으로 전환
query = `SELECT * FROM posts WHERE created_at < '${cursor}' ORDER BY created_at DESC LIMIT ${limit}`;
}
실제 구현 가이드
Offset 페이지네이션 구현
// Express.js + MySQL 예시
app.get('/api/posts', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const offset = (page - 1) * limit;
const [posts] = await db.execute(
'SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?',
[limit, offset]
);
const [countResult] = await db.execute('SELECT COUNT(*) as total FROM posts');
const total = countResult[0].total;
const totalPages = Math.ceil(total / limit);
res.json({
data: posts,
pagination: {
currentPage: page,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
});
Cursor 페이지네이션 구현
// Cursor 기반 무한스크롤 구현
app.get('/api/posts', async (req, res) => {
const cursor = req.query.cursor;
const limit = parseInt(req.query.limit) || 10;
let query = 'SELECT * FROM posts';
let params = [];
if (cursor) {
query += ' WHERE id < ?';
params.push(cursor);
}
query += ' ORDER BY id DESC LIMIT ?';
params.push(limit + 1); // +1로 다음 페이지 존재 확인
const [posts] = await db.execute(query, params);
const hasNext = posts.length > limit;
if (hasNext) posts.pop(); // 추가로 가져온 레코드 제거
const nextCursor = posts.length > 0 ? posts[posts.length - 1].id : null;
res.json({
data: posts,
pagination: {
nextCursor,
hasNext
}
});
});
API 설계 Best Practice
RESTful API 디자인 원칙
스크롤 향상 API 디자인에서 중요한 것은 일관성 있는 응답 구조입니다.
클라이언트가 페이지네이션 상태를 쉽게 파악할 수 있도록 메타데이터를 제공해야 합니다.
{
"data": [...],
"meta": {
"pagination": {
"current_page": 1,
"per_page": 20,
"total": 1000,
"total_pages": 50
}
},
"links": {
"first": "/api/posts?page=1",
"last": "/api/posts?page=50",
"prev": null,
"next": "/api/posts?page=2"
}
}
GraphQL에서의 페이지네이션
GraphQL에서는 Relay Specification을 따르는 것이 권장됩니다.
Connection과 Edge 개념을 활용한 커서 기반 페이지네이션이 표준입니다.
type Query {
posts(first: Int, after: String): PostConnection
}
type PostConnection {
edges: [PostEdge]
pageInfo: PageInfo
}
type PostEdge {
node: Post
cursor: String
}
실무 적용 시나리오별 선택 가이드
E-commerce 상품 목록
온라인 쇼핑몰의 상품 목록은 사용자가 특정 페이지를 북마크하거나 공유할 가능성이 높습니다.
검색 결과를 다시 찾아보거나 특정 가격대 상품을 재확인하는 경우가 많아 offset 방식이 적합합니다.
다만 실시간 재고 변동이 심한 상품의 경우 백엔드 설계 시 하이브리드 방식을 고려해볼 수 있습니다.
소셜 미디어 피드
Facebook, Instagram, Twitter 같은 소셜 미디어는 실시간성이 중요합니다.
새로운 포스트가 지속적으로 추가되고 사용자는 순차적으로 콘텐츠를 소비합니다.
이런 환경에서는 커서 페이지네이션과 무한스크롤의 조합이 최적입니다.
관리자 대시보드
데이터 분석이나 관리 목적의 대시보드에서는 특정 기간이나 조건의 데이터를 반복해서 확인할 필요가 있습니다.
페이지 번호 기반의 탐색이 필요하므로 offset 방식이 더 실용적입니다.
다만 대용량 데이터 처리 시에는 성능을 고려한 최적화가 필요합니다.
성능 최적화 전략
인덱스 설계 최적화
페이지네이션 성능을 극대화하려면 적절한 인덱스 설계가 필수입니다.
Offset 방식에서는 ORDER BY 절에 사용되는 컬럼에 인덱스를 생성해야 합니다.
-- 생성일 기준 정렬용 인덱스
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
-- 복합 정렬 조건용 인덱스
CREATE INDEX idx_posts_status_created ON posts(status, created_at DESC);
Cursor 방식에서는 커서로 사용할 컬럼에 유니크 인덱스를 생성하는 것이 중요합니다.
캐싱 전략
첫 페이지나 자주 조회되는 페이지는 Redis 등의 캐시를 활용하여 응답 속도를 향상시킬 수 있습니다.
// Redis 캐싱 적용 예시
const cacheKey = `posts:page:${page}:limit:${limit}`;
const cachedResult = await redis.get(cacheKey);
if (cachedResult) {
return JSON.parse(cachedResult);
}
const result = await fetchPostsFromDB(page, limit);
await redis.setex(cacheKey, 300, JSON.stringify(result)); // 5분 캐시
return result;
데이터베이스 별 최적화 팁
MySQL에서는 SQL_CALC_FOUND_ROWS
를 피하고 별도 COUNT 쿼리를 사용하는 것이 더 효율적입니다.
MySQL 공식 문서에서도 이를 권장합니다.
PostgreSQL에서는 LIMIT
과 OFFSET
보다 FETCH FIRST
와 OFFSET
조합이 더 성능이 좋을 수 있습니다.
MongoDB에서는 skip()
보다 _id
기반 범위 쿼리를 사용하는 것이 권장됩니다.
모바일 앱에서의 고려사항
네트워크 효율성
모바일 환경에서는 네트워크 상태가 불안정할 수 있어 페이지네이션 전략이 더욱 중요합니다.
작은 배치 크기로 자주 요청하는 것보다는 적당한 크기의 배치로 요청 횟수를 줄이는 것이 효과적입니다.
오프라인 상태에서도 이전에 로드된 데이터를 보여줄 수 있도록 로컬 저장소 활용을 고려해야 합니다.
무한스크롤 최적화
모바일에서 무한스크롤을 구현할 때는 메모리 관리가 중요합니다.
화면에 보이지 않는 항목들을 DOM에서 제거하는 가상화(Virtualization) 기법을 적용해야 합니다.
// React Native에서 FlatList 활용 예시
<FlatList
data={posts}
renderItem={renderPost}
onEndReached={loadMorePosts}
onEndReachedThreshold={0.1}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
/>
보안 고려사항
SQL Injection 방지
페이지네이션 파라미터도 사용자 입력이므로 철저한 검증이 필요합니다.
특히 정렬 조건을 동적으로 받는 경우 화이트리스트 방식으로 검증해야 합니다.
// 안전하지 않은 방식
const orderBy = req.query.sort; // 위험!
const query = `SELECT * FROM posts ORDER BY ${orderBy}`;
// 안전한 방식
const allowedSortFields = ['created_at', 'title', 'view_count'];
const orderBy = allowedSortFields.includes(req.query.sort)
? req.query.sort
: 'created_at';
Rate Limiting
페이지네이션 API에도 적절한 속도 제한을 적용해야 합니다.
과도한 페이지 요청으로 인한 DoS 공격을 방지할 수 있습니다.
Express Rate Limit 같은 미들웨어를 활용할 수 있습니다.
미래 동향과 새로운 패러다임
GraphQL Subscriptions
실시간 데이터 업데이트가 중요한 현대 애플리케이션에서는 GraphQL의 Subscription 기능과 커서 페이지네이션을 조합하는 방식이 주목받고 있습니다.
새로운 데이터가 추가될 때 클라이언트가 자동으로 업데이트를 받을 수 있어 더욱 동적인 사용자 경험을 제공합니다.
Edge Computing과 페이지네이션
CDN과 엣지 컴퓨팅 환경에서는 지역별로 캐시된 데이터의 페이지네이션 처리가 복잡해집니다.
글로벌 일관성을 유지하면서도 지연 시간을 최소화하는 새로운 접근법들이 연구되고 있습니다.
마무리
pagination offset vs cursor 선택은 단순한 기술적 결정이 아닙니다.
서비스의 특성, 사용자 패턴, 데이터 특성을 종합적으로 고려한 전략적 판단이 필요합니다.
성능이 중요하고 실시간성이 요구되는 환경에서는 cursor pagination이 명확한 우위를 보입니다.
반면 사용자가 임의의 페이지에 접근해야 하거나 전체 데이터 규모 파악이 중요한 경우에는 offset pagination도 여전히 유효한 선택입니다.
중요한 것은 각 방식의 특성을 정확히 이해하고, 프로젝트의 요구사항에 가장 적합한 방식을 선택하는 것입니다.
때로는 두 방식을 적절히 조합한 하이브리드 접근법이 최선의 해답이 될 수도 있습니다.
쿼리 최적화와 백엔드 설계의 관점에서 페이지네이션을 바라보고, 지속적인 성능 모니터링과 개선을 통해 최적의 사용자 경험을 제공하시기 바랍니다.
참고 자료
- PostgreSQL LIMIT and OFFSET
- MongoDB Skip and Limit
- GraphQL Cursor Connections
댓글
댓글 쓰기