NestJS / / 2024. 11. 19. 12:48

NestJS 페이징처리

페이징(Pagination)이란?

Pagination은 많은 데이터를 한 번에 불러오는 대신 부분적으로 나눠서 가져오는 기술입니다. 이는 서버의 메모리와 네트워크 자원을 아끼고 성능을 높이는 데 큰 도움이 됩니다.

Pagination의 특징

  • 모든 데이터를 한 번에 불러오는 대신 필요한 만큼만 가져오는 방식입니다.
  • 예를 들어, 쿠팡 같은 앱에는 수억 개의 상품이 있습니다. 사용자가 상품 검색을 할 때마다 모든 상품 정보를 한 번에 전송하는 것은 비효율적이며, 메모리 문제네트워크 비용이 발생합니다.
  • 메모리 문제: 많은 데이터를 한 번에 로드하면 메모리가 부족해질 수 있습니다.
  • 네트워크 비용: 클라우드에서는 데이터 전송에도 비용이 발생하기 때문에, 페이징을 통해 네트워크 비용도 줄일 수 있습니다.
  • 데이터를 나눠 가져오면 데이터 전송 시간도 줄어듭니다.

Pagination의 두 가지 방식

  1. Page Based Pagination
    • 페이지 단위로 데이터를 나누어 가져오는 방식입니다.
    • 요청 시 페이지 번호페이지당 데이터 수를 명시합니다.
    • 예를 들어, "두 번째 페이지, 한 번에 20개의 데이터"와 같은 식으로 요청합니다.
    • 단점: 데이터가 갱신되면 페이지가 바뀌면서 중복되거나 누락되는 데이터가 발생할 수 있습니다.
    • 장점: 간단한 구현이 가능하며, 페이지 번호 UI에서 많이 사용됩니다.
  2. Cursor Based Pagination
    • 마지막으로 가져온 데이터를 기준으로 다음 데이터를 가져오는 방식입니다.
    • 요청 시 마지막 데이터의 고유 값(예: ID)과 가져올 데이터 수를 명시합니다.
    • 데이터가 추가되거나 삭제될 때 중복이나 누락 가능성이 적습니다.
    • 장점: 데이터가 변경되어도 잘 대응할 수 있어 리스트 스크롤 형태에 적합합니다.

Cursor 기반 페이징 처리

DTO(Data Transfer Object) 생성

다음은 페이징 처리를 위한 DTO(PaginatePostDto)입니다.

import { IsIn, IsNumber, IsOptional } from "class-validator";
import { Type } from "class-transformer";

export class PaginatePostDto {
    @Type(() => Number)
    @IsNumber()
    @IsOptional()
    where__id_more_than?: number;

    @IsIn(['ASC', 'DESC'])
    @IsOptional()
    order_createdAt?: 'ASC' | 'DESC' = 'ASC';

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

1. DTO 필드 설명

  • where__id_more_than: 이전 마지막 데이터의 ID보다 큰 데이터를 가져오도록 설정합니다.
    • @Type(() => Number): 쿼리 문자열로 들어온 데이터를 숫자로 변환합니다.
  • order_createdAt: 데이터 정렬 방식입니다. 오름차순('ASC') 또는 내림차순('DESC')으로 정렬할 수 있습니다. 기본값은 오름차순입니다.
  • take: 가져올 데이터의 개수입니다. 기본값으로 20개를 설정했습니다.

2. 페이징 처리 로직 구현

페이징 처리를 위한 paginatePosts 메서드를 구현합니다.

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

    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') {
                    nextUrl.searchParams.append(key, dto[key]);
                }
            }
        }
        nextUrl.searchParams.append('where__id_more_than', lastItem.id.toString());
    }

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

3. 페이징 로직 설명

  • 게시글 가져오기 (postsRepository.find)
    • where 조건에서 *id: MoreThan(dto.where__id_more_than ?? 0)을 사용하여 특정 ID보다 큰 게시글만 가져옵니다.
    • order 필드를 통해 생성일(createdAt) 기준으로 오름차순 또는 내림차순 정렬을 합니다.
    • take는 가져올 데이터 개수입니다.
  • 마지막 게시글 찾기 (lastItem)
    • 가져온 게시글 개수(posts.length)가 0보다 크고, *dto.take와 같으면 마지막 게시글을 *lastItem으로 설정합니다.
    • 그렇지 않으면 *lastItemnull입니다.
  • 다음 페이지 URL 생성 (nextUrl)
    • lastItem이 있을 때 *nextUrl을 생성합니다.
    • DTO에 있는 모든 필드를 순회하면서 쿼리 파라미터로 URL에 추가합니다.
    • where__id_more_than의 값은 마지막 데이터의 ID로 갱신하여 다음 데이터를 가져올 때 기준이 되도록 합니다.

페이징 처리 테스트를 위한 임의의 게시물 생성

다음은 테스트를 위해 임의의 게시물 100개를 생성하는 코드입니다.

// POST /posts/random
@Post('random')
@UseGuards(AccessTokenGuard)
async postPostsRandom(@User() user: UsersModel) {
    await this.postsService.generatePosts(user.id);
    return true;
}

async generatePosts(userId: number) {
    for (let i = 0; i < 100; i++) {
        await this.createPost(userId, {
            title: `임의로 생성된 포스트 제목 ${i}`,
            content: `임의로 생성된 포스트 내용 ${i}`,
        });
    }
}
  • 임의의 포스트 생성:
    • generatePosts() 메서드를 통해 100개의 임의 게시물을 생성합니다.
    • 이 작업은 페이징 처리의 동작을 확인하기 위한 것입니다.

쿼리 문자열을 숫자로 변환하기

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

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

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

설명

  • 쿼리 문자열을 숫자로 변환해야 하는 이유:
    • 쿼리 파라미터는 기본적으로 문자열로 전달됩니다.
    • where__id_more_than과 같은 필드는 숫자 타입이어야 하므로 문자열을 숫자로 변환해야 합니다.
  • 방법:
    • DTO에 @Type(() => Number) 어노테이션을 추가하여 필드별로 처리할 수 있습니다.
    • 또는 *ValidationPipe*transformOptions를 추가하여 모든 변환을 한 번에 처리할 수 있습니다.

마지막 페이지 처리 로직

const lastItem = posts.length > 0 && posts.length === dto.take ? posts[posts.length - 1] : null;
  • lastItem가져온 게시물 개수가 dto.take와 같을 때만 마지막 게시물을 가져옵니다.
  • 이를 통해 추가 데이터가 있는 경우에만 다음 페이지 요청을 위한 정보를 생성합니다.

정렬 방식 추가하기 (DESC 방식)

기존의 ASC 방식만을 지원했던 코드를 개선하여 DESC 방식도 지원하도록 변경합니다.

  • order__createdAt 필드ASC 또는 DESC 값을 입력받아 정렬 조건을 설정합니다.
    @IsIn(['ASC', 'DESC'])
    @IsOptional()
    order__createdAt?: 'ASC' | 'DESC' = 'ASC'

내림차순 처리 로직 추가

내림차순 정렬이 가능하도록 조건을 추가합니다.

if (dto.order__createdAt === 'ASC') {
    key = 'where__id_more_than';
} else {
    key = 'where__id_less_than';
}
nextUrl.searchParams.append(key, lastItem.id.toString());
  • ASC와 DESC 조건에 따라 다음 페이지 요청 시 사용될 파라미터(where__id_more_than 또는 where__id_less_than)를 설정합니다.
  • 이를 통해 오름차순 및 내림차순 모두 페이징 처리가 가능해집니다.

Page 기반 페이징 처리

페이지 기반의 페이징 처리를 위해 page 필드를 추가한 DTO입니다.

import { IsNumber, IsOptional, IsIn } from "class-validator";

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

    @IsIn(['ASC', 'DESC'])
    @IsOptional()
    order_createdAt?: 'ASC' | 'DESC' = 'ASC';

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

Page 기반 페이징 로직

다음은 페이지 번호 기반으로 페이징 처리하는 로직입니다.

async paginatePosts(dto: PaginatePostDto) {
    if (dto.page) {
        return this.pagePaginatePosts(dto);
    } else {
        return this.cursorPaginatePosts(dto);
    }
}
  • 페이지 번호가 있는 경우 (dto.page):
    • 페이지 번호를 이용해 Page 기반 페이징 처리를 합니다.
    • 그렇지 않다면 Cursor 기반 페이징 처리를 수행합니다.

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,
    };
}
  • skip: 페이지 번호에 따라 건너뛸 데이터 수를 계산합니다.
  • take: 한 페이지당 가져올 데이터의 개수입니다.
  • 응답 데이터에는 게시물 목록(posts)과 총 데이터 개수(count)가 포함됩니다.

이렇게 두 가지 방식(Page 기반과 Cursor 기반)을 나란히 구현함으로써, 다양한 요구 사항에 맞는 페이징을 구현할 수 있습니다. Page 기반 페이징은 간단하고 직관적이지만, Cursor 기반 페이징은 데이터 갱신에 더 잘 대응할 수 있는 장점이 있습니다.

'NestJS' 카테고리의 다른 글

NestJS Interceptor  (0) 2024.11.25
NestJS Pagination Common  (0) 2024.11.20
NestJS Class-transfomer  (0) 2024.11.18
NestJS DTO,Validation  (0) 2024.11.18
NestJS 데코레이터  (0) 2024.11.16
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유