Nestjs 페이지네이션 구현하기

페이지네이션 구현하기

벡엔드 개발에 있어서 필수적인 요소라고 볼 수 있는 페이지네이션을 구현해보자

페이지네이션이란?
웹사이트를 이용할 때 게시글을 한번에 보여주지않고 전체게시글을 나눠서 페이지 별로 볼 수 있게 하는 구조를 많이 사용하는데, 여기서 페이지를 나누고 요청한 페이지의 데이터를 보내주는 것을 의미한다.

Page 클래스와 PageRequest 클래스 만들기

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, skipfind()메서드의 옵션이다.)
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 객체를 따로 생성해서 관리해줬다고 생각하면 될 것 같다.

좋은 웹페이지 즐겨찾기