Intro
첫 회사에서 진행하게 된 프로젝트에서 대용량 데이터를 처리하게 되었다.
페이지네이션, 무한스크롤 등으로 데이터를 분할해서 가져와야 하는 기능이 필수적이었는데, 취준하며 활용했던 페이징과는 다른 방식을 취하고 있었다. page, limit을 주면 알아서 해당 페이지 데이터를 가져왔는데 회사에서 사용하는 API에는 마지막으로 받아온 데이터의 유니크한 값을 같이 넘겨줘야 하는 것이다. (왜??)
알고 보니 페이징 기법이 offset, cursor 두 가지가 있는 거였잖아..?
1. 대용량 데이터에서 페이징이 필요한 이유
나는 5만 건의 데이터를 관리하는 프로그램을 만든다. 그런데 심지어 자재를 관리하는 거라 row 한 개에 수많은 데이터가 들어간다. 고로 5만 건의 데이터를 한 번에 받아온다면? 이용자는 데이터 로딩만 10분 이상을 기다려야 한다. 그런데 이것은 조회만 되는 데이터가 아니라 수정도 필요한 데이터다. 수정을 하고 저장을 하면 연관된 데이터가 재로딩되어야 하기 때문에 refresh하는 데.. 그럼 또 5분 대기해야 한다. 그리고 버전업을 한다? 또 10분 대기해야 한다.. (무한 반복)
인텔리제이에서 파일 하나 여는데 10분 걸린다고 생각해보자. 진짜 끔찍하다. 이러면 업무의 효율성이 얼마나 떨어지냐고..
그래서! 이 큰 데이터를 다루기 위해 다양한 기법들이 존재하는데.
대용량 데이터를 다루기 위한 방법 5가지
- 페이징 기법 (Pagination Techniques)
- 인덱스 최적화 (Index Optimization)
- 캐싱 전략 (Caching Strategies)
- 비동기 데이터 로딩 (Asynchronous Data Loading)
- 데이터 압축 및 최소화 (Data Compression and Minimization)
등•••
이 중에서 데이터를 분할해서 가져오는 페이징 기법에 대해 다뤄보겠다.
2. 페이징 기법
대용량 데이터를 효과적으로 관리하고, 사용자의 경험을 향상시키기 위해서는 데이터를 적절히 분할하여 로드하는 것이 중요하다.
페이징 기법은 데이터를 여러 페이지로 나누어 점진적으로 로드함으로써 초기 로딩 시간을 단축하고, 서버와 클라이언트의 부하를 줄이는 데 도움을 준다.
대표적인 페이징 기법으로는 offset-based 페이징과 cursor-based 페이징이 있다.offset-based 페이징이 기존 프로젝트에서 적용했던 기법으로 책을 넘기듯이 탐색할 수 있는 방법이고,cursor-based 페이징이 회사 프로젝트에서 적용한 기법으로 Unique한 값을 활용해 페이징을 처리하는 방법!
아래에서 두 가지 페이징 기법에 대해 상세히 살펴보자.
3. Offset 기반 페이징 기법
Offset-based 페이징은 가장 일반적으로 사용되는 페이징 기법으로, 클라이언트가 요청할 데이터의 시작 위치(offset)와 가져올 데이터의 개수(limit)를 지정하여 데이터를 분할하는 방식이다.
이 기법은 SQL의 LIMIT과 OFFSET 절을 사용하여 쉽게 구현할 수 있다.
장점
간단한 구현: 대부분의 데이터베이스에서 기본적으로 지원되며, 클라이언트와 서버 간의 통신이 간단하다.
유연성: 임의의 페이지로 접근할 수 있어, 사용자가 특정 페이지로 바로 이동할 수 있다.
단점
성능 저하: 큰 offset 값을 사용할수록 데이터베이스가 더 많은 데이터를 스캔해야 하므로 성능이 저하될 수 있다.
데이터 일관성 문제: 데이터가 삽입되거나 삭제될 경우, 페이지가 변경되어 중복되거나 누락된 데이터가 발생할 수 있다.
불안정한 페이지 번호: 데이터가 변경될 때마다 페이지 번호가 변할 수 있어, 클라이언트가 페이지 번호를 신뢰하기 어려워진다.
구현 예시
Next.js + Axios
// src/pages/products.tsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
interface Product {
id: number;
name: string;
description: string;
}
const ProductsPage = () => {
const [products, setProducts] = useState<Product[]>([]);
const [page, setPage] = useState(0);
const [size, setSize] = useState(10);
const [totalPages, setTotalPages] = useState(0);
useEffect(() => {
const fetchProducts = async () => {
const response = await axios.get('/api/products', {
params: { page, size }
});
setProducts(response.data.content);
setTotalPages(response.data.totalPages);
};
fetchProducts();
}, [page, size]);
return (
<div>
<h1>Products</h1>
<ul>
{products.map(product => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
</li>
))}
</ul>
<div>
<button
onClick={() => setPage(prev => Math.max(prev - 1, 0))}
disabled={page === 0}
>
Previous
</button>
<span> Page {page + 1} of {totalPages} </span>
<button
onClick={() => setPage(prev => (prev + 1 < totalPages ? prev + 1 : prev))}
disabled={page + 1 >= totalPages}
>
Next
</button>
</div>
</div>
);
};
export default ProductsPage;
4. Cursor 기반 페이징 기법
Cursor-based 페이징은 마지막으로 가져온 데이터의 고유한 식별자(cursor)를 사용하여 다음 데이터를 가져오는 방식이다.
이 기법은 주로 데이터의 순서가 중요한 경우에 사용되며, 특히 대용량 데이터셋에서 성능과 일관성을 유지하는 데 효과적이다.
장점
성능 향상: offset-based 페이징과 달리, 특정 위치로 이동하지 않고, 마지막으로 받은 데이터의 식별자만 사용하므로 성능이 뛰어나다.
데이터 일관성 유지: 데이터가 삽입되거나 삭제되어도 일관된 결과를 유지할 수 있어, 중복되거나 누락된 데이터가 발생하지 않는다.
효율적인 인덱스 활용: 인덱스를 효과적으로 활용하여 빠른 검색이 가능하다.
단점
구현 복잡성: offset-based 페이징에 비해 구현이 복잡하다.
임의의 페이지 접근 불가: 특정 페이지로 직접 접근하기 어려워, 처음부터 순차적으로 페이지를 탐색해야 한다.
불완전한 지원: 모든 데이터베이스나 ORM이 cursor-based 페이징을 기본적으로 지원하지 않는다.
구현 예시
Next.js + Axios
// src/pages/products-cursor.tsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
interface Product {
id: number;
name: string;
description: string;
}
const ProductsCursorPage = () => {
const [products, setProducts] = useState<Product[]>([]);
const [lastId, setLastId] = useState<number | null>(null);
const [hasMore, setHasMore] = useState(true);
const size = 10;
useEffect(() => {
const fetchProducts = async () => {
const response = await axios.get('/api/products-cursor', {
params: { lastId, size }
});
const newProducts: Product[] = response.data;
setProducts(prev => [...prev, ...newProducts]);
if (newProducts.length < size) {
setHasMore(false);
} else {
setLastId(newProducts[newProducts.length - 1].id);
}
};
fetchProducts();
}, [lastId]);
const loadMore = () => {
if (hasMore) {
setLastId(products[products.length - 1].id);
}
};
return (
<div>
<h1>Products (Cursor Pagination)</h1>
<ul>
{products.map(product => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
</li>
))}
</ul>
{hasMore && (
<button onClick={loadMore}>Load More</button>
)}
</div>
);
};
export default ProductsCursorPage;
5. 두 기법의 비교
Offset-based 페이징과 Cursor-based 페이징은 각각의 장단점이 있으며, 사용하는 상황에 따라 적절한 기법을 선택하는 것이 중요하다. 아래 표는 두 페이징 기법의 주요 차이점을 비교한 것이다.
| 특징 | Offset 기반 페이징 | Cursor 기반 페이징 |
|---|---|---|
| 구현 난이도 | 간단 | 복잡 |
| 성능 | 대량의 데이터일수록 성능 저하 발생 | 높은 성능 유지 |
| 데이터 일관성 | 데이터 삽입/삭제 시 중복 또는 누락 가능 | 데이터 삽입/삭제에도 일관성 유지 |
| 임의 페이지 접근 | 가능 | 불가능 (순차적으로 접근 필요) |
| 인덱스 활용 | LIMIT과 OFFSET 사용, 인덱스 효율적으로 활용하지 못함 |
인덱스를 효율적으로 활용하여 빠른 검색 가능 |
| 적용 사례 | 페이지 네비게이션이 중요한 경우, 임의의 페이지로 이동할 필요가 있는 경우 | 대용량 데이터셋, 실시간 데이터 스트리밍, 성능이 중요한 경우 |
요약
Offset 기반 페이징은 구현이 간단하고, 사용자 경험 측면에서 임의 페이지 접근이 가능하다는 장점이 있지만, 대량의 데이터 처리 시 성능 저하와 데이터 일관성 문제가 발생할 수 있다.Cursor 기반 페이징은 높은 성능과 데이터 일관성을 유지할 수 있지만, 구현이 복잡하고, 임의의 페이지 접근이 어려워 순차적으로 데이터에 접근해야 하는 단점이 있다.
래퍼런스
Is offset pagination dead? Why cursor pagination is taking over
커서 기반 페이지네이션 (Cursor-based Pagination) 구현하기
Faster Pagination in Mysql – Why Order By With Limit and Offset is Slow?
마치며
대용량 데이터를 다루다 보니 최적화에 관해 사용한 기술이 많아 공부할 내용이 산더미다.
모르는 기술이 있으면 바로 바로 알아보고 정리해야지.
일주일에 한번씩 돌아오는 것을 목표로 오늘 기록은 여기서 끝!
'Frontend' 카테고리의 다른 글
| 웹에서 엑셀과 워드 다루기: SpreadJS & OnlyOffice 산업데이터 시각화 (0) | 2026.01.12 |
|---|---|
| 분산 문서 시스템 통합 과정에서의 대용량 파일 처리 및 전송 최적화 (0) | 2025.11.24 |
| React Portal과 Window.open으로 PIP 플레이어 구현하기 (1) | 2025.11.19 |
| EventSource vs EventSourcePolyfill: SSE로 프로그래스바 구현하기 (1) | 2025.05.14 |
| 서버와 클라이언트 간 지속적 통신 방법(Polling, Long Polling, WebSocket, SSE) 비교 (1) | 2025.01.08 |