이번에는 좀 더 간단한 page 기반의 pagination을 알아보겠습니다.
먼저 paginatePosts()의 내부 코드를 전부 잘라내기 합니다. 그리고 아래와 같은 상태로 만들어 줍니다.
async paginatePosts(dto: PaginatePostDto) {
}
async pagePaginatePosts() {
}
async cursorPaginatePosts(dto: PaginatePostDto) {
const where: FindOptionsWhere<PostsModel> = {};
if (dto.where__id_less_than) {
where.id = LessThan(dto.where__id_less_than);
} else if(dto.where__id_more_than) {
where.id = MoreThan(dto.where__id_more_than);
}
const posts = await this.postsRepository.find({
where,
order: {
createdAt: dto.order__createdAt,
},
take: dto.take,
});
const lastItem = posts.length > 0 && posts.length === dto.take ? posts[posts.length - 1] : null;
const nextUrl = lastItem && new URL(`${PROTOCOL}://${HOST}/posts`);
if (nextUrl) {
for (const key of Object.keys(dto)) {
if (dto[key]) {
if (key !== 'where__id_more_than' && key !== 'where__id_less_than') {
nextUrl.searchParams.append(key, dto[key])
};
}
}
let key = null;
if (dto.order__createdAt === 'ASC') key = 'where__id_more_than';
else key = 'where__id_less_than';
nextUrl.searchParams.append(key, lastItem.id.toString());
}
return {
data: posts,
cursor: {
after: lastItem?.id ?? null,
},
count: posts.length,
next: nextUrl?.toString() ?? null
}
}
DTO를 받을 때 DTO의 값을 보고 page 기반의 pagination을 할지 아니면 cursor 기반의 pagination을 하는지 파악하는 것입니다. 따라서 paginate-post.dto.ts는 현재 상태는 cursor만 가능하기 때문에 page기반의 pagination도 가능하도록 바꾸겠습니다.
우리는 page만 보고 page를 할지 cursor를 할지 판단을 하도록 하겠습니다.
export class PaginatePostDto {
@IsNumber()
@IsOptional()
page?: number; // page기반의 pagination을 위한 추가
@IsNumber()
@IsOptional()
where__id_more_than?: number;
@IsNumber()
@IsOptional()
where__id_less_than?: number;
@IsOptional()
order__createdAt?: 'ASC' | 'DESC' = 'ASC';
@IsNumber()
@IsOptional()
take: number = 20;
}
그리고 service 코드를 작성해 줍니다.
async paginatePosts(dto: PaginatePostDto) {
if (dto.page) return this.pagePaginatePosts(dto); // page Pagination 코드
else return this.cursorPaginatePosts(dto); // cursor Pagination 코드
}
이제는 pagePaginatePosts 메소드를 작성하겠습니다. page Pagination은 버튼 1, 2, 3.. 형식으로 되어 있을 것입니다. 따라서 total 전체를 FE에게 주면 FE가 알아서 계산을 할 것입니다.
async pagePaginatePosts(dto: PaginatePostDto) {
/**
* data: Data[],
* total: number
*
* [1] [2] [3] [4]
*/
const posts = await this.postsRepository.find({
skip: dto.take * (dto.page - 1), // 현실에서는 1부터 시작하기 때문에
take: dto.take,
order: {
createdAt: dto.order__createdAt,
}
});
return {
data: posts,
}
}
포스트맨으로 테스트를 해보겠습니다. 아직 total은 적용되지 않았습니다.
{
"data": [
{
"id": 2,
"updatedAt": "2024-01-27T20:40:11.525Z",
"createdAt": "2024-01-27T17:40:51.930Z",
"title": "NestJS Lecture",
"content": "첫번쨰 content",
"likeCount": 0,
"commentCount": 0
},
.
.
{
"id": 21,
"updatedAt": "2024-01-28T02:12:53.903Z",
"createdAt": "2024-01-28T02:12:53.903Z",
"title": "임의로 생성된 12",
"content": "임의로 생성된 포수트 내용 12",
"likeCount": 0,
"commentCount": 0
}
]
}
{
"data": [
{
"id": 22,
"updatedAt": "2024-01-28T02:12:53.921Z",
"createdAt": "2024-01-28T02:12:53.921Z",
"title": "임의로 생성된 13",
"content": "임의로 생성된 포수트 내용 13",
"likeCount": 0,
"commentCount": 0
},
.
.
{
"id": 41,
"updatedAt": "2024-01-28T02:12:54.065Z",
"createdAt": "2024-01-28T02:12:54.065Z",
"title": "임의로 생성된 32",
"content": "임의로 생성된 포수트 내용 32",
"likeCount": 0,
"commentCount": 0
}
]
}
이번에는 total을 적용해서 작성해보도록 하겠습니다. repository에는 findAndCount가 존재합니다. 정의로 이동을 해봅시다.
/**
* Finds entities that match given find options. // 모든 엔티티를 찾고
* Also counts all entities that match given conditions, // 조건에 맞는 엔티티가 몇개인지 count
* but ignores pagination settings (from and take options).
*/
findAndCount(options?: FindManyOptions<Entity>): Promise<[Entity[], number]>;
반환 타입을 보면 Entity에는 posts를 받을 수 있고, number에는 count를 받겠습니다.
async pagePaginatePosts(dto: PaginatePostDto) {
// [Entity[], number]
const [posts, count] = await this.postsRepository.findAndCount({
skip: dto.take * (dto.page - 1),
take: dto.take,
order: {
createdAt: dto.order__createdAt,
}
});
return {
data: posts,
total: count
}
}
{
"data": [
{
"id": 2,
"updatedAt": "2024-01-27T20:40:11.525Z",
"createdAt": "2024-01-27T17:40:51.930Z",
"title": "NestJS Lecture",
"content": "첫번쨰 content",
"likeCount": 0,
"commentCount": 0
},
.
.
{
"id": 21,
"updatedAt": "2024-01-28T02:12:53.903Z",
"createdAt": "2024-01-28T02:12:53.903Z",
"title": "임의로 생성된 12",
"content": "임의로 생성된 포수트 내용 12",
"likeCount": 0,
"commentCount": 0
}
],
"total": 107
}
알 수 있듯이 page기반의 페이지네이션은 정말 쉽습니다.
하지만 cursor기반의 페이지네이션을 현대에서 많이 사용하는 이유는 데이터가 추가 또는 삭제 시, 중복을 주는 문제가 생길 수 있기 때문입니다. 이 문제를 Cursor 페이지네이션을 기반으로 데이터 스킵을 줄 수 있어서 단점을 해결할 수 있기 때문입니다.