오프셋 기반 페이지네이션 (Offset-based Pagination)
DB의 offset 쿼리를 사용하여 '페이지' 단위로 구분하여 요청/응답하게 구현 하는 것.
기능이 직관적이며 사용자가 페이지를 이동하는 것에 있어 유연하다는 장점이 있지만
데이터가 추가 또는 삭제되는 것이 반영되지 않아 중복 또는 누락되는 데이터가 생길 수 있다.
ex) 1p : 10~6번 데이터 / 2p : 5~1 데이터 라고 할때 사용자가 1p에 머무르는 동안 새로운 데이터가 들어오면 1p : 11~7 / 2p : 6~2 가 된다. 이때 사용자가 2p로 넘어가면 6번 데이터가 중복되어 도시되는 것이다.
결정적인 단점은 offset이 작은 수라면 크게 문제가 되지 않지만 row 수가 아주 많은 경우 offset 값이 올라갈 수록 쿼리의 퍼포먼스는 이에 비례하여 떨어진다.
페이지 기반 페이지 네이션 구현
//page-options.dto.ts
export class PageOptionsDto {
readonly order?: OrderConstants = OrderConstants.ASC;
readonly page?: number = 1;
readonly take?: number = 10;
constructor(order: OrderConstants, page: number, take: number) {
this.order = order;
this.page = page;
this.take = take;
}
get skip(): number {
return (this.page - 1) * this.take;
}
}
//page-meta-dto-parametars.interface.ts
export interface PageMetaDtoParametars {
pageOptionsDto: PageOptionsDto;
itemCount: number;
}
//page-meta.dto.ts
export class PageMetaDto {
readonly page: number;
readonly take: number;
readonly itemCount: number;
readonly pageCount: number;
readonly hasPreviousPage: boolean;
readonly hasNextPage: boolean;
constructor({ pageOptionsDto, itemCount }: PageMetaDtoParametars) {
this.page = pageOptionsDto.page;
this.take = pageOptionsDto.take;
this.itemCount = itemCount;
this.pageCount = Math.ceil(this.itemCount / this.take);
this.hasPreviousPage = this.page > 1;
this.hasNextPage = this.page < this.pageCount;
}
}
//page.dto.ts
export class PageDto<T> {
readonly data: T[];
readonly meta: PageMetaDto;
constructor(data: T[], meta: PageMetaDto) {
this.data = data;
this.meta = meta;
}
}
dto는 개인의 스타일이다.
//controller
@Get('page-list')
async getPageList(
@Query() pageOptionsDto: PageOptionsDto,
): Promise<PageDto<entity>> {
console.log(pageOptionsDto);
const setDto = new PageOptionsDto(
pageOptionsDto.order === undefined
? OrderConstants.ASC
: pageOptionsDto.order,
pageOptionsDto.page === undefined ? 1 : pageOptionsDto.page,
pageOptionsDto.take === undefined ? 10 : pageOptionsDto.take,
);
console.log(setDto);
return await this.entityService.getPageList(setDto);
}
@Query() pageOptionsDto: PageOptionsDto 로 타입을 설정했지만
setDto를 다시 해주는 이유는... 반영이 안되기 때문이다.
바디나 쿼리로 가져온 dto는 타입이 반영되지 않는 듯하다. 위 코드에서 콘솔을 확인하면 다음과 같다.
{ page: '1', take: '10' }
PageOptionsDto { order: 'ASC', page: '1', take: '10' }
때문에 setDto를 통해 타입을 재정의 해주었다.
//service
constructor(
@InjectRepository(entity)
private entityRepository: Repository<entity>,
){}
.
.
.
async getPageList(
pageOptionsDto: PageOptionsDto,
): Promise<PageDto<entity>> {
const queryBuilder = await this.entityRepository
.createQueryBuilder('entity')
.orderBy('entity.createdAt', pageOptionsDto.order)
.skip(pageOptionsDto.skip)
.take(pageOptionsDto.take);
//const { entities } = await queryBuilder.getRawAndEntities();
const [entities, itemCount] = await queryBuilder.getManyAndCount();
const pageMetaDto = new PageMetaDto({ pageOptionsDto, itemCount });
return new PageDto(entities, pageMetaDto);
}