[nestjs] 커서 기반 페이지네이션 (Cursor-Based Pagination)

Eden·2023년 11월 16일
0

앱에서 무한 스크롤 기능은 흔하게 볼 수 있다. 만약에 무한 스크롤을 페이지 기반 페이지 네이션으로 구현한다면 중복되는 데이터가 연속으로 등장할 것이고 이는 서비스의 완성도를 크게 떨어 뜨릴 수 있다. 따라서 무한 스크롤 기능에는 일관된 데이터를 전달하는 커서 기반 페이지네이션을 사용하는 것이 좋다.

커서 기반 페이지네이션 구현

//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로 형변환을 시켜주어야했다.

페이지 기반 페이지 네이션과 달리 커서기반 페이지네이션에서는 다음 커서값을 클라이언트에게 전달해줘야한다.

profile
주섬주섬..

0개의 댓글