[Nest.js] Pagination 구현하기: 특징과 종류, 문제점 분석

궁금하면 500원·2024년 8월 17일

1. Pagination의 특징

Pagination(페이징)은 데이터베이스에서 대량의 데이터를 효율적으로 처리하고 사용자에게 보여주기 위한 방법입니다.

주요 특징은 다음과 같습니다.

데이터 분할

데이터셋을 여러 페이지로 나누어, 한 번에 너무 많은 데이터를 로드하지 않고도 필요한 정보에 접근할 수 있게 합니다.

이는 사용자 경험을 개선하고, 시스템의 메모리 사용을 줄이는 데 도움이 됩니다.

네비게이션 컨트롤

사용자가 페이지 간 쉽게 이동할 수 있도록 하는 네비게이션 요소
(예: 이전, 다음, 첫 페이지, 마지막 페이지 링크)를 제공합니다.

균일한 데이터 규격

페이지에 표시되는 데이터의 형식과 구조를 일관되게 유지하여
사용자에게 친숙한 경험을 제공합니다.

상태 유지

사용자가 현재 위치한 페이지를 기억하고, 이전 상태로 돌아갈 수 있도록 합니다.
이는 특히 검색 및 필터링을 수행하는 경우 유용합니다.

퍼포먼스 최적화

데이터베이스에서 필요한 데이터만 로드하여 성능을 향상시킵니다.
전체 데이터를 한 번에 가져오는 것보다 페이지 단위로 요청하여 서버 및 클라이언트 측의 부하를 줄일 수 있습니다.

2. 간단한 예제 코드

Nest.js에서의 Pagination을 구현하기 위한 간단한 예제 코드입니다.

다음은 GET /users?page=1&limit=10 형식으로 사용자 목록을 페이지네이션하여 가져오는 API의 예시입니다.

2.1. DTO와 Service 구현

user.dto.ts

import { IsInt, IsNotEmpty } from 'class-validator';

export class PaginationDto {
  @IsInt()
  @IsNotEmpty()
  page: number;

  @IsInt()
  @IsNotEmpty()
  limit: number;
}

user.service.ts

import { Injectable } from '@nestjs/common';
import { PaginationDto } from './user.dto';

@Injectable()
export class UserService {
  private readonly users = [ /* ... User Data ... */ ];

  findAll(paginationDto: PaginationDto) {
    const { page, limit } = paginationDto;

    const startIndex = (page - 1) * limit;
    const endIndex = page * limit;

    const paginatedUsers = this.users.slice(startIndex, endIndex);
    return {
      data: paginatedUsers,
      total: this.users.length,
      page,
      limit,
    };
  }
}

2.2. Controller 구현

user.controller.ts

import { Controller, Get, Query } from '@nestjs/common';
import { UserService } from './user.service';
import { PaginationDto } from './user.dto';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  findAll(@Query() paginationDto: PaginationDto) {
    return this.userService.findAll(paginationDto);
  }
}

3. Pagination의 종류

Pagination의 종류에는 여러 가지가 있습니다

Page-Based Pagination (페이지 기반)

사용자가 특정 페이지 번호를 요청하여 데이터를 가져오는 방식입니다.
예를 들어, GET /users?page=2&limit=10.

Cursor-Based Pagination (커서 기반)

각 페이지에 대한 데이터의 "커서"를 사용하여 다음 데이터를 가져오는 방식입니다.
이는 데이터의 위치를 기반으로 하여, 특정 데이터부터 다음 데이터를 가져오는 것입니다.

예: GET /users?after=cursorValue&limit=10.

Offset-Based Pagination (오프셋 기반)

데이터의 오프셋(시작 위치)을 지정하여 가져오는 방식입니다.

예: GET /users?offset=20&limit=10.

Keyset Pagination (키셋 기반)

특정 데이터의 키를 사용하여 다음 데이터를 가져오는 방식으로,
성능과 일관성을 유지하는 데 유리합니다.

4. Page-Based Pagination의 문제점과 Cursor-Based Pagination

Page-Based Pagination의 문제점

데이터 변경에 대한 불안정성:

사용자가 페이지를 이동하는 동안 데이터가 추가되거나 삭제되면, 결과 세트가 바뀌어 이전에 보던 데이터와 다른 데이터가 나타날 수 있습니다.

성능 문제: 대규모 데이터셋에서는 페이지가 깊어질수록 성능 저하가 발생할 수 있습니다.
데이터베이스에서 페이지 번호에 따라 많은 데이터를 건너뛰어야 하므로 비효율적일 수 있습니다.

Cursor-Based Pagination

장점: Cursor-Based Pagination은 이전 페이지의 마지막 항목을 기준으로 다음 페이지를 로드하기 때문에, 데이터 변경 시에도 안정적인 결과를 제공합니다.

사용자가 위치한 항목을 기준으로 다음 데이터를 쉽게 가져올 수 있습니다.

단점: 커서를 사용해야 하므로, 구현이 다소 복잡할 수 있습니다.

또한 커서가 무효화될 경우(예: 데이터가 삭제되거나 변경됨) 문제가 발생할 수 있습니다.

5. EnableImplicitTransformation

EnableImplicitTransformation은 Nest.js에서 데이터 전송 객체(DTO)와 요청 본문 간의 변환을 자동으로 처리할 수 있도록 설정하는 옵션입니다.

이 설정을 활성화하면, 요청에서 들어온 데이터가 DTO의 타입과 일치하는지 자동으로 검사하고,
필요에 따라 변환을 수행합니다.

main.ts


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, whitelist: true }));
  await app.listen(3000);
}
bootstrap();

이렇게 설정하면 DTO에서 정의한 데이터 유형에 맞게 들어온 요청을 변환하여 유효성을 검증할 수 있습니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글