캡스톤 설계 [건물별 소통 플랫폼 BBC]

게시물 페이지 네이션

softmoca__ 2024. 3. 24. 17:56
목차

https://softmoca.tistory.com/315

 

페이지네이션[Pagination]

페이지네이션[Pagination] 이란 ? 많은 데이터를 부분적으로 나눠서 불러오는 기술. 특징 - 쿼리에 해당되는 모든 데이터를 한번에 다 불러오지 않고 부분적으로 나눠서 불러온다 예) 한번에 20개씩

softmoca.tistory.com

 

페이지 네이션 전반적인 내용은 위 게시물을 참조 !

 

 

1) 커서 기반 페이지 네이션 

 

게시물 20개의 오름차순 정렬된 게시물 가져오기

 

오름 차순 페이지네이션을 위한 기본 dto생성

export class PaginatePostDto {
  // 이전 마지막 데이터의 ID
  // 이 프로퍼티에 입력된 ID 보다 높은 ID 부터 값을 가져오기
  @IsNumber()
  @IsOptional()
  where__id__more_than?: number;

  // 정렬
  // createdAt -> 생성된 시간의 오름차 순으로 정렬
  @IsIn(["ASC"])
  @IsOptional()
  order__createdAt: "ASC" = "ASC";

  // 몇개의 데이터를 응답으로 받을지
  @IsNumber()
  @IsOptional()
  take: number = 20;
}

 

 

post.controller와 post.service

  @Get()
  // @UseInterceptors(LogInterceptor)
  getPosts(@Query() query: PaginatePostDto) {
    return this.postsService.paginatePosts(query);
  }
  // 1) 오름차 순으로 정렬하는 pagination만 구현한다
  async paginatePosts(dto: PaginatePostDto) {
    const posts = await this.postsRepository.find({
      where: {
        id: MoreThan(dto.where__id__more_than ?? 0),
      },
      order: {
        createdAt: dto.order__createdAt,
      },
      take: dto.take,
    });

    return { data: posts };
  }

 

 

main.ts

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
    })
  );

dto의 정렬의 기본 디폴트 값으로 ASC 즉, 오름차순으로 된 값을 dto단에서 처리하기 위해 transform 옵션을 true로 한다.

 

그럼 현재 22개의 게시물 중 오름차순으로 20개의 게시물만 가져올 수 있다 !

 

 

다음 20개의 게시물 데이터 가져 오기

id가 1-20까지의 데이터를 조회 했으니 이제 21-40까지의 다음 페이지 게시물 데이터를 조회 해야한다.

 

id가 20 이상인 게시물을 쿼리로 요청을 하면 아래와 같이 에러가 나온다.

그 이유로 쿼리는 url에 있는 것이기 때문에 숫자 형태로 입력하더라고 string으로 간주가 된다.

그래서 string을 숫자 형태로 변환을 해줘야 한다. 그걸 바로 classtransformer가 해준다.

 

paginat-post.dto.ts

 @Type(()=>Number)
  @IsNumber()
  @IsOptional()
  where__id__more_than?: number;

위와 같이 타입 변환을 Type어노테이션을 사용하면 string이 number타입으로 변환되어 에러 없이 쿼리 실행 잘 된다.

 

 

main.ts

app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      transformOptions: {
        enableImplicitConversion: true,
      },
    })
  );

하지만 매 속성마다 Type 어노테이션을 붙이지 않고 위와 같이 enableImplicitConversion을 true로 해주면 

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

위와 같이 IsNumber 어노테이션을 확인하고 '아 ! 숫자가 와야 하는구나 !' 라고 인식해 자동으로 string을 Number타입으로 변환시켜준다. 즉, 어노테이션에 작성된 타입을 기반으로 자동으로 class-transformer가 작동하여 타입을 변환 한다.

 

 

 

다음 페이지를 가져오기 위한 cursor와 nextURL [+내림차순]

export class PaginatePostDto {
  // 이전 마지막 데이터의 ID
  // 이 프로퍼티에 입력된 ID 보다 높은 ID 부터 값을 가져오기
  @IsNumber()
  @IsOptional()
  where__id__more_than?: number;

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

내림 차순을 위한 where__id__less_than 옵션 추가.

 

  async paginatePosts(dto: PaginatePostDto) {
    const where: FindOptionsWhere<PostModel> = {};

    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,
    });

쿼리가 less이냐 more이냐에 따라 다른 필터링 where 객체 생성하여 분기처리한다.

 

 

post.service.ts

const lastItem = posts.length > 0 && posts.length === dto.take ? posts[posts.length - 1] : null;

해당되는 포스트가 0개 이상이면서 take와 길이가 같다(다음페이지 데이터가 더 있다)면 마지막 포스트를 가져오고 아니면 null을 반환한다.  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의 키값들을 루핑하면서 키값에 해당되는 벨류가 존재하면 param에 그대로 붙여넣는다.
단, where__id_more_than과 less_than 값만 lastItem의 마지막 값으로 넣어준다.
    

그럼 이제 내림차순 또한 다음 페이지를 조회 하기 위한 nextURL와  cursor가 잘 나온다.

 

 

 

 

 

2) 페이지 기반 페이지 네이션

export class PaginatePostDto {
  @IsNumber()
  @IsOptional()
  page?: number;

페이지 네이션 dto에 page 속성 추가.

 

 

 

  async pagePaginatePosts(dto: PaginatePostDto) {
      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,
    }
  }

페이지의 갯수를 구하기 위해 total과 dto.take을 알려준다.

 

 

위와 같이 페이지 별로 게시물 데이터를 정렬 하려 잘 가져올 수 있다.