NestJS-Page Pagination

jaegeunsong97·2024년 1월 29일
0

NestJS

목록 보기
21/37
post-custom-banner

이번에는 좀 더 간단한 page 기반의 pagination을 알아보겠습니다.

🖊️DTO에 프로퍼티 추가

먼저 paginatePosts()의 내부 코드를 전부 잘라내기 합니다. 그리고 아래와 같은 상태로 만들어 줍니다.

  • posts.service.ts
async paginatePosts(dto: PaginatePostDto) {

}

async pagePaginatePosts() {

}

async cursorPaginatePosts(dto: PaginatePostDto) {
  	const where: FindOptionsWhere<PostsModel> = {};

    if (dto.where__id_less_than) {
      	where.id = LessThan(dto.where__id_less_than);
    } else if(dto.where__id_more_than) {
      	where.id = MoreThan(dto.where__id_more_than);
    }

    const posts = await this.postsRepository.find({
        where,
        order: {
          	createdAt: dto.order__createdAt,
        },
        take: dto.take,
    });

    const lastItem = posts.length > 0 && posts.length === dto.take ? posts[posts.length - 1] : null;
    const nextUrl = lastItem && new URL(`${PROTOCOL}://${HOST}/posts`);
  
    if (nextUrl) {
        for (const key of Object.keys(dto)) {
            if (dto[key]) {
                if (key !== 'where__id_more_than' && key !== 'where__id_less_than') {
                  	nextUrl.searchParams.append(key, dto[key])
                };
            }
        }

        let key = null;
        if (dto.order__createdAt === 'ASC') key = 'where__id_more_than';
        else key = 'where__id_less_than';

      	nextUrl.searchParams.append(key, lastItem.id.toString());
    }
    return {
        data: posts,
        cursor: {
          	after: lastItem?.id ?? null,
        },
        count: posts.length,
        next: nextUrl?.toString() ?? null
    }
}

DTO를 받을 때 DTO의 값을 보고 page 기반의 pagination을 할지 아니면 cursor 기반의 pagination을 하는지 파악하는 것입니다. 따라서 paginate-post.dto.ts는 현재 상태는 cursor만 가능하기 때문에 page기반의 pagination도 가능하도록 바꾸겠습니다.

  • posts/dto/paginate-post.dto.ts

우리는 page만 보고 page를 할지 cursor를 할지 판단을 하도록 하겠습니다.

export class PaginatePostDto {

    @IsNumber()
    @IsOptional()
    page?: number; // page기반의 pagination을 위한 추가

    @IsNumber()
    @IsOptional()
    where__id_more_than?: number;

    @IsNumber()
    @IsOptional()
    where__id_less_than?: number;

    @IsOptional()
    order__createdAt?: 'ASC' | 'DESC' = 'ASC';

    @IsNumber()
    @IsOptional()
    take: number = 20;
}

그리고 service 코드를 작성해 줍니다.

  • posts.service.ts
async paginatePosts(dto: PaginatePostDto) { 
    if (dto.page) return this.pagePaginatePosts(dto); // page Pagination 코드
    else return this.cursorPaginatePosts(dto); // cursor Pagination 코드
}

🖊️Post 응답 생성

이제는 pagePaginatePosts 메소드를 작성하겠습니다. page Pagination은 버튼 1, 2, 3.. 형식으로 되어 있을 것입니다. 따라서 total 전체를 FE에게 주면 FE가 알아서 계산을 할 것입니다.

  • posts.service.ts
async pagePaginatePosts(dto: PaginatePostDto) {
    /**
    * data: Data[],
    * total: number
    * 
    * [1] [2] [3] [4]
    */
    const posts = await this.postsRepository.find({
        skip: dto.take * (dto.page - 1), // 현실에서는 1부터 시작하기 때문에
        take: dto.take,
        order: {
          	createdAt: dto.order__createdAt,
        }
    });
    return {
      	data: posts,
    }
}

포스트맨으로 테스트를 해보겠습니다. 아직 total은 적용되지 않았습니다.

{
    "data": [
        {
            "id": 2,
            "updatedAt": "2024-01-27T20:40:11.525Z",
            "createdAt": "2024-01-27T17:40:51.930Z",
            "title": "NestJS Lecture",
            "content": "첫번쨰 content",
            "likeCount": 0,
            "commentCount": 0
        },
        .
        .
        {
            "id": 21,
            "updatedAt": "2024-01-28T02:12:53.903Z",
            "createdAt": "2024-01-28T02:12:53.903Z",
            "title": "임의로 생성된 12",
            "content": "임의로 생성된 포수트 내용 12",
            "likeCount": 0,
            "commentCount": 0
        }
    ]
}

{
    "data": [
        {
            "id": 22,
            "updatedAt": "2024-01-28T02:12:53.921Z",
            "createdAt": "2024-01-28T02:12:53.921Z",
            "title": "임의로 생성된 13",
            "content": "임의로 생성된 포수트 내용 13",
            "likeCount": 0,
            "commentCount": 0
        },
        .
        .
        {
            "id": 41,
            "updatedAt": "2024-01-28T02:12:54.065Z",
            "createdAt": "2024-01-28T02:12:54.065Z",
            "title": "임의로 생성된 32",
            "content": "임의로 생성된 포수트 내용 32",
            "likeCount": 0,
            "commentCount": 0
        }
    ]
}

🖊️응답에 total 프로퍼티 추가

이번에는 total을 적용해서 작성해보도록 하겠습니다. repository에는 findAndCount가 존재합니다. 정의로 이동을 해봅시다.

/**
* Finds entities that match given find options. // 모든 엔티티를 찾고
* Also counts all entities that match given conditions, // 조건에 맞는 엔티티가 몇개인지 count
* but ignores pagination settings (from and take options).
*/
findAndCount(options?: FindManyOptions<Entity>): Promise<[Entity[], number]>;

반환 타입을 보면 Entity에는 posts를 받을 수 있고, number에는 count를 받겠습니다.

  • posts.service.ts
async pagePaginatePosts(dto: PaginatePostDto) {
  	// [Entity[], number]
    const [posts, count] = await this.postsRepository.findAndCount({
        skip: dto.take * (dto.page - 1),
        take: dto.take,
        order: {
          createdAt: dto.order__createdAt,
        }
    });

    return {
        data: posts,
        total: count
    }
}

{
    "data": [
        {
            "id": 2,
            "updatedAt": "2024-01-27T20:40:11.525Z",
            "createdAt": "2024-01-27T17:40:51.930Z",
            "title": "NestJS Lecture",
            "content": "첫번쨰 content",
            "likeCount": 0,
            "commentCount": 0
        },
        .
        .
        {
            "id": 21,
            "updatedAt": "2024-01-28T02:12:53.903Z",
            "createdAt": "2024-01-28T02:12:53.903Z",
            "title": "임의로 생성된 12",
            "content": "임의로 생성된 포수트 내용 12",
            "likeCount": 0,
            "commentCount": 0
        }
    ],
    "total": 107
}

알 수 있듯이 page기반의 페이지네이션은 정말 쉽습니다.

하지만 cursor기반의 페이지네이션을 현대에서 많이 사용하는 이유는 데이터가 추가 또는 삭제 시, 중복을 주는 문제가 생길 수 있기 때문입니다. 이 문제를 Cursor 페이지네이션을 기반으로 데이터 스킵을 줄 수 있어서 단점을 해결할 수 있기 때문입니다.

profile
블로그 이전 : https://medium.com/@jaegeunsong97
post-custom-banner

0개의 댓글