MongoDB Pagination
MongoDB에서 페이징을 처리하는 방법은 여러 가지가 있습니다.
효율적으로 처리하기 위해 했던 고민들을 공유합니다.
개발 환경
- Node.js(Express)
- mongoDB(Mongoose)
❌ Skip, Limit
처음 적용했던 방식은 mongoDB의 skip과 limit 기능을 이용해서 데이터를 건너뛰고(skip), 한 페이지에 보여줘야 하는 수만큼 제한(skip)하는 것입니다.
현재 3 페이지고 한 페이지에 10개의 데이터를 보여준다면 21~30 번째 글을 보여주면 되므로 skip(20).limit(10) 가 됩니다.
db.students.find().skip(20) // 10개를 건너뛰고 21개부터 조회
db.students.find().limit(10) // 10개만 조회
db.students.find().skip(20).limit(10) // 20개를 건너뛰고 10개만 조회
Node.js에서 페이징을 적용하려면 아래와 같이 작성하면 됩니다.
const itemsPerPage = 10;
const page = 3;
const resuilt = await this.students.find({})
.skip((page - 1) * itemsPerPage)
.limit(itemsPerPage)
같은 방법으로 페이징이 아닌 무한 스크롤(Infinite Scroll)에도 적용할 수 있습니다.
시작할 순서(offset)와 조회 개수(limit)를 요청받아 skip과 limit에 그대로 사용하면 됩니다.
한 번에 20개의 데이터를 조회한다면
- 최초 조회: offset 0, limit 20
- 첫 번째 스크롤: offset 20, limit 20
- 두 번째 스크롤: offset 40. limit 20
const resuilt = await this.students.find({})
.skip(offset)
.limit(limit)
Skip과 Limit의 단점
1. 성능 저하
사용하기는 정말 편하지만 find에 해당되는 데이터를 전부 조회한 후 skip과 limit을 적용하기 때문에 성능에 문제가 생길 수 있습니다. 필요한 데이터는 10건인데 매번 전체 데이터를 읽는 것은 비효율적입니다.
2. 데이터 불일치 가능성
사용자가 3번째 페이지를 읽다가 새로운 글이 등록됐을 경우 4번 페이지로 이동했을 때 같은 글이 중복되어 노출될 수 있습니다. skip & limit은 이 문제를 해결할 수 있는 방법이 없습니다.
skip과 limit 대신 다른 방법이 필요해 보입니다
✅ Object ID 비교(참고글)
MongoDB에서 document별로 부여되는 Object ID의 앞자리는 timestamp를 가지고 있습니다.
Object ID를 비교하면 어떤 글이 먼저 등록되었는지 알 수 있습니다.(timestamp가 클수록 최신 글입니다)
// NodeJS로 확인
if(첫번째로 등록된 글("632862fb37ad6700140c2f11") < 두번째로 등록된 글("63482c55d4143d48ee4d5796")) // true
// MongoDB에서 확인
{_id: {$gt: ObjectId('632862fb37ad6700140c2f11') }}
대부분 페이징 처리를 하는 경우 등록일을 오름차순이나 내림차순으로 정렬되어 있습니다.
즉 데이터가 정렬되어 있는 상태라면 ObjectID를 통해 페이징을 처리할 수 있습니다.
현재 페이지의 마지막 글보다 ObjectID가 큰 데이터를 조회하면 다음 페이지를 가져올 수 있습니다.
const result = await this.students.find({ $gt: "1페이지의 마지막 Object ID" })
.limit(limit)
이 방법을 사용하면 데이터 전체를 조회하지 않아도 되기 때문에 속도도 개선되고 데이터 중복도 피할 수 있습니다! 👍👍
주의 사항
이때 find 조건에 $or이 있을 경우 데이터가 섞일 수 있습니다. $and 연산자를 이용하여 데이터가 섞이지 않도록 주의해야 합니다.
여러 페이지 이동
한 페이지를 이동이 아닌 여러 페이지를 이동할 때는 어떻게 처리해야 할까요?
현재 페이지는 3 페이지고 이전 페이지가 1일 경우 Object ID 비교와 skip을 같이 사용하면 됩니다.
const itemsPerPage = 10; // 한 페이지에 표현할 글 수
let currentPage = 3; // 현재 페이지
let previousPage = 1; // 이전 페이지
let pagesToSkip = currentPage - previousPage; // 이동한 페이지
db.students
.find({’_id’: {’$gt’: last_id}})
.skip(itemsPerPage * pagesToSkip)
.limit(itemsPerPage)
저는 페이지가 앞에서 뒤로 이동할 수도 있으므로 pagesToSkip에 따라 $gt, $lt로 조회하도록 처리했습니다.
더 좋은 방법이 있다면 댓글로 알려주시면 감사하겠습니다. 🙏
참고 글
MongoDB Pagination, Fast & Consistent
Implementing pagination in mongodb
'DataBase' 카테고리의 다른 글
[MongoDB]mongoose에서 가상 필드 조회안하기, Document를 Object로 변환하기 (0) | 2021.11.16 |
---|---|
SQL, NoSQL 비교(특징, 스키마, 속도, 확장) (2) | 2021.09.01 |