앱에서 무한 스크롤 기능은 흔하게 볼 수 있다. 만약에 무한 스크롤을 페이지 기반 페이지 네이션으로 구현한다면 중복되는 데이터가 연속으로 등장할 것이고 이는 서비스의 완성도를 크게 떨어 뜨릴 수 있다. 따라서 무한 스크롤 기능에는 일관된 데이터를 전달하는 커서 기반 페이지네이션을 사용하는 것이 좋다.
커서 기반 페이지네이션 구현
//cursor.dto.ts
export class CursorDto<T> {
readonly data: T[];
readonly meta: CursorMetaDto;
constructor(data: T[], meta: CursorMetaDto) {
this.data = data;
this.meta = meta;
}
//cursor-meta.dto.ts
export class CursorMetaDto {
readonly cursor?: number;
readonly take: number;
readonly itemCount: number;
readonly newCursor: number | null;
readonly hasNext: boolean;
constructor({
cursorOptionsDto,
itemCount,
newCursor,
hasNext,
}: CursorMetaDtoParametarsInterface) {
this.cursor = cursorOptionsDto.cursor;
this.take = cursorOptionsDto.take;
this.itemCount = itemCount;
this.newCursor = newCursor;
this.hasNext = hasNext;
}
}
//cursor-options.dto.ts
export class CursorOptionsDto {
readonly order: OrderConstants = OrderConstants.ASC;
readonly cursor?: number;
readonly take = 10;
constructor(
order: OrderConstants,
cursor: number,
take: number,
) {
this.order = order;
this.cursor = cursor;
this.take = take;
}
}
//cursor-meta-dto-parametars.interface.ts
export interface CursorMetaDtoParametars {
cursorOptionsDto: CursorOptionsDto;
itemCount: number;
newCursor: number;
hasNext: boolean;
}
dto는 취향 or 스타일
//controller
@Get('cursor-list')
async getCursorList(
@Query() cursorOptionsDto: CursorOptionsDto,
): Promise<CursorDto<entity>> {
const setDto = new CursorOptionsDto(
cursorOptionsDto.order === undefined
? OrderConstants.ASC
: cursorOptionsDto.order,
cursorOptionsDto.cursor === undefined ? 0 : cursorOptionsDto.cursor,
cursorOptionsDto.take === undefined ? 10 : cursorOptionsDto.take,
);
return await this.albumService.getCursorList(setDto);
}
setDto를 재정의 하는 이유는 페이지 기반 페이지네이션 내용과 동일하다
//service
async getCursorList(
cursorOptionsDto: CursorOptionsDto,
): Promise<CursorDto<entity>> {
console.log(
typeof Number(cursorOptionsDto.take) + ':',
Number(cursorOptionsDto.take),
typeof cursorOptionsDto.take + ':',
cursorOptionsDto.take,
);
const queryBuilder = await this.entityRepository
.createQueryBuilder('entity')
.orderBy('entity.Id', cursorOptionsDto.order)
.where(
cursorOptionsDto.order === OrderConstants.DESC
? 'entity.Id <= :val'
: 'entity.Id >= :val',
{
val: cursorOptionsDto.cursor,
},
)
.limit(Number(cursorOptionsDto.take) + 1);
const [entities, itemCount] = await queryBuilder.getManyAndCount();
let newCursor;
let hasNext;
if (itemCount === 0) {
newCursor = null;
hasNext = false;
} else if (entities.length < Number(cursorOptionsDto.take) + 1) {
newCursor = entities[entities.length - 1].Id;
hasNext = false;
} else {
newCursor = entities[cursorOptionsDto.take].Id;
hasNext = true;
}
// console.log(articles);
const cursorMetaDto = new CursorMetaDto({
cursorOptionsDto,
itemCount,
newCursor,
hasNext,
});
return new CursorDto(
hasNext ? entities.slice(0, entities.length - 1) : entities,
cursorMetaDto,
);
}
콘솔을 확인해 보면
number: 10 string: 10
다음과 같이 나타난다 때문에 Number로 형변환을 시켜주어야했다.
페이지 기반 페이지 네이션과 달리 커서기반 페이지네이션에서는 다음 커서값을 클라이언트에게 전달해줘야한다.