벡엔드 개발에 있어서 필수적인 요소라고 볼 수 있는 페이지네이션을 구현해보자
페이지네이션이란?
웹사이트를 이용할 때 게시글을 한번에 보여주지않고 전체게시글을 나눠서 페이지 별로 볼 수 있게 하는 구조를 많이 사용하는데, 여기서 페이지를 나누고 요청한 페이지의 데이터를 보내주는 것을 의미한다.
Page 클래스 객체는 실제 프론트에 데이터를 보내줄 때, 보내줄 데이터를 넣어 페이지네이션을 구현하게 해주는 객체이다.
//page.ts
//프론트에 보내줄때 사용하는 실제 Page 데이터 class
export class Page<T> {
pageSize: number;
totalCount: number;
totalPage: number;
items: T[];
constructor(totalCount: number, pageSize: number, items: T[]) {
this.pageSize = pageSize;
this.totalCount = totalCount;
this.totalPage = Math.ceil(totalCount / pageSize);
this.items = items;
}
}
PageRequest 클래스 객체는 페이징요청이 들어오면 사용하는 객체이다.
Page 객체에 데이터를 넣어주기 전에 데이터베이스에서 원하는 데이터를 찾는데 그냥 전부 가져오는 것이 아니며
현재 페이지의 위치(pageNo 맴버변수, index 역할을 한다) 와 페이지 안의 데이터 갯수(pageSize 맴버변수) 으로 이루어져 있다.
그리고 getOffset() 메서드는 데이터가 시작하는 위치인 pageNo를 적절하게 초기화 하고
getLimit() 메서드는 pageSize를 초기화 하고 리턴한다.
//pageRequest.ts
import { IsOptional, IsString } from 'class-validator';
//페이지네이션 요청 받을때 사용하는 클래스 양식
export class PageRequest {
//@IsOptional() 데코레이터는 undefined도 받을 수 있다.
@IsString()
@IsOptional()
pageNo?: number | 1;
@IsString()
@IsOptional()
pageSize?: number | 10;
getOffset(): number {
if (this.pageNo < 1 || this.pageNo === null || this.pageNo === undefined) {
this.pageNo = 1;
}
if (
this.pageSize < 1 ||
this.pageSize === null ||
this.pageSize === undefined
) {
this.pageSize = 10;
}
return (Number(this.pageNo) - 1) * Number(this.pageSize);
}
getLimit(): number {
if (
this.pageSize < 1 ||
this.pageSize === null ||
this.pageSize === undefined
) {
this.pageSize = 10;
}
return Number(this.pageSize);
}
}
상품 정보를 전부 가져오는 api를 요청하는 컨트롤러를 만들고 들어오는 데이터를 PageRequest 클래스를 확장한 DTO로 받아온다.
//모든 상품 정보 불러오기
@Get('')
getAllGoods(@Query() page: SearchGoodsDto) {
return this.goodsService.getAllGoods(page);
}
export class SearchGoodsDto extends PageRequest {
@IsString()
@IsOptional()
@ApiProperty({ type: String, description: 'ID' })
id?: number;
...Dto 코드
}
미리 정의한 page.getLimit(), page.getOffset() 메서드를 이용한다.
이때, take는 한번에 가져올 데이터 양을 의미하고 skip은 한 페이지에 들어갈 데이터 개수를 의미한다.
(take, skip은 find()메서드의 옵션이다.)
typeorm 공식문서 참고
//모든 goods 가져오기
async getAllGoods(page: SearchGoodsDto) {
const total = await this.goodsRepository.count();
const goods = await this.goodsRepository.find({
take: page.getLimit(),
skip: page.getOffset(),
});
return new Page(total, page.pageSize, goods);
}
한 페이지당 10개의 데이터를 요구했을 때 (현재 데이터는 11개가 들어가있다.)
1페이지는 id가 1부터 10까지 10개의 데이터가 들어있고
2페이지는 id가 11인 데이터 하나만 들어가 있는것을 볼 확인해 볼 수 있다!
{
"pageSize": 10,
"totalCount": 11,
"totalPage": 2,
"items": [
{
"createdAt": "2022-03-29T11:58:08.557Z",
"updatedAt": "2022-03-29T11:58:08.557Z",
"deletedAt": null,
"id": 1,
"category": "PANTS",
"name": "수정된 데님",
"price": 200000,
"discount": 10,
"stock": 1000,
},
{
"createdAt": "2022-03-29T11:58:48.687Z",
"updatedAt": "2022-03-29T11:58:48.687Z",
"deletedAt": null,
"id": 2,
"category": "",
"name": "멋쟁이 셔츠",
"price": 200000,
"discount": 10,
"stock": 1000,
},
{
"createdAt": "2022-03-30T11:50:36.674Z",
"updatedAt": "2022-03-30T11:50:36.674Z",
"deletedAt": null,
"id": 3,
"category": "PANTS",
"name": "데님",
"price": 200000,
"discount": 10,
"stock": 1000,
},
{
"createdAt": "2022-03-30T12:13:15.345Z",
"updatedAt": "2022-03-30T12:13:15.345Z",
"deletedAt": null,
"id": 4,
"category": "PANTS",
"name": "데님1",
"price": 200000,
"discount": 10,
"stock": 1000,
},
{
"createdAt": "2022-03-30T12:13:26.342Z",
"updatedAt": "2022-03-30T12:13:26.342Z",
"deletedAt": null,
"id": 5,
"category": "PANTS",
"name": "데님2",
"price": 200000,
"discount": 10,
"stock": 1000,
},
{
"createdAt": "2022-03-30T12:13:31.087Z",
"updatedAt": "2022-03-30T12:13:31.087Z",
"deletedAt": null,
"id": 6,
"category": "PANTS",
"name": "데님3",
"price": 200000,
"discount": 10,
"stock": 1000,
},
{
"createdAt": "2022-03-30T12:17:42.514Z",
"updatedAt": "2022-03-30T12:17:42.514Z",
"deletedAt": null,
"id": 7,
"category": "PANTS",
"name": "데님",
"price": 200000,
"discount": 10,
"stock": 1000,
},
{
"createdAt": "2022-03-30T12:17:43.795Z",
"updatedAt": "2022-03-30T12:17:43.795Z",
"deletedAt": null,
"id": 8,
"category": "PANTS",
"name": "데님",
"price": 200000,
"discount": 10,
"stock": 1000,
},
{
"createdAt": "2022-03-30T12:17:46.718Z",
"updatedAt": "2022-03-30T12:17:46.718Z",
"deletedAt": null,
"id": 9,
"category": "PANTS",
"name": "데님",
"price": 200000,
"discount": 10,
"stock": 1000,
},
{
"createdAt": "2022-03-30T12:17:48.419Z",
"updatedAt": "2022-03-30T12:17:48.419Z",
"deletedAt": null,
"id": 10,
"category": "PANTS",
"name": "데님",
"price": 200000,
"discount": 10,
"stock": 1000,
}
]
}
//pageNo=2로 요청했을떄
{
"pageSize": 10,
"totalCount": 11,
"totalPage": 2,
"items": [
{
"createdAt": "2022-03-30T12:17:50.051Z",
"updatedAt": "2022-03-30T12:17:50.051Z",
"deletedAt": null,
"id": 11,
"category": "PANTS",
"name": "데님",
"price": 200000,
"discount": 10,
"stock": 1000,
}
]
}
사실상 데이터베이스에서 데이터를 가져올 때,
find() 메서드의 옵션을 주어서 가져오는 것인데 이것을 좀 더 편하게 하기 위해 Page 객체를 따로 생성해서 관리해줬다고 생각하면 될 것 같다.