페이징(Pagination)이란?
Pagination은 많은 데이터를 한 번에 불러오는 대신 부분적으로 나눠서 가져오는 기술입니다. 이는 서버의 메모리와 네트워크 자원을 아끼고 성능을 높이는 데 큰 도움이 됩니다.
Pagination의 특징
- 모든 데이터를 한 번에 불러오는 대신 필요한 만큼만 가져오는 방식입니다.
- 예를 들어, 쿠팡 같은 앱에는 수억 개의 상품이 있습니다. 사용자가 상품 검색을 할 때마다 모든 상품 정보를 한 번에 전송하는 것은 비효율적이며, 메모리 문제와 네트워크 비용이 발생합니다.
- 메모리 문제: 많은 데이터를 한 번에 로드하면 메모리가 부족해질 수 있습니다.
- 네트워크 비용: 클라우드에서는 데이터 전송에도 비용이 발생하기 때문에, 페이징을 통해 네트워크 비용도 줄일 수 있습니다.
- 데이터를 나눠 가져오면 데이터 전송 시간도 줄어듭니다.
Pagination의 두 가지 방식
- Page Based Pagination
- 페이지 단위로 데이터를 나누어 가져오는 방식입니다.
- 요청 시 페이지 번호와 페이지당 데이터 수를 명시합니다.
- 예를 들어, "두 번째 페이지, 한 번에 20개의 데이터"와 같은 식으로 요청합니다.
- 단점: 데이터가 갱신되면 페이지가 바뀌면서 중복되거나 누락되는 데이터가 발생할 수 있습니다.
- 장점: 간단한 구현이 가능하며, 페이지 번호 UI에서 많이 사용됩니다.
- 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
으로 설정합니다. - 그렇지 않으면 *
lastItem
은null
입니다.
- 가져온 게시글 개수(
- 다음 페이지 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
를 추가하여 모든 변환을 한 번에 처리할 수 있습니다.
- DTO에
마지막 페이지 처리 로직
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 |