Skip to Content
문서가이드페이지네이션

페이지네이션

ASAPJS는 페이지네이션을 프레임워크 레벨에서 지원합니다. 모든 라우트 핸들러에 paging 객체가 자동으로 제공되며, PaginationQueryDtoTypeIs.PAGING을 조합하면 Swagger 문서까지 한 번에 생성됩니다.

동작 원리

클라이언트가 ?page=0&limit=10 쿼리 파라미터를 보내면, @asapjs/routerWrapper 함수가 이를 자동으로 파싱하여 ExecuteArgs.paging 객체로 전달합니다.

// Wrapper 내부 (packages/router/src/utils/wrapper.ts) const { page: pageProp = 0, limit: limitProp = 20 } = req.query; const page = parseInt(String(pageProp), 10); const limit = parseInt(String(limitProp), 10); const paging: PaginationQueryType = { page, limit };
파라미터기본값설명
page0페이지 번호 (0부터 시작)
limit20한 페이지당 항목 수

참고: page0-based 인덱스입니다. 첫 번째 페이지는 page=0이며, Repository는 내부적으로 offset = limit * page로 계산합니다.

참고: pagingquery 옵션에 PaginationQueryDto를 지정하지 않아도 모든 핸들러에서 사용할 수 있습니다. Wrapper가 모든 요청에 대해 자동으로 생성합니다.

PaginationQueryDto와 PaginationQueryType

PaginationQueryDto

@asapjs/sequelize에서 제공하는 내장 DTO로, pagelimit 두 필드를 가집니다. 라우트 데코레이터의 query 옵션에 전달하면, Swagger UI에 pagelimit 쿼리 파라미터가 자동으로 문서화됩니다.

export default class PaginationQueryDto extends ExtendableDto { @TypeIs.INT({ comment: '페이지' }) page: number; @TypeIs.INT({ comment: '한 페이지당 표시 개수' }) limit: number; }

PaginationQueryType

PaginationQueryTypePaginationQueryDto에서 pagelimit만 추출한 타입 별칭입니다:

export type PaginationQueryType = Pick<PaginationQueryDto, 'page' | 'limit'>;

Wrapper가 생성하는 ExecuteArgs.paging의 타입이 PaginationQueryType이며, Application 레이어에서 paging 인자의 타입으로 사용합니다:

import type { PaginationQueryType } from '@asapjs/sequelize'; // Application 레이어 public list = async (paging: PaginationQueryType, user: UserDto) => { // ... };

PaginationQueryDto 확장

추가 필터 파라미터가 필요한 경우, PaginationQueryDto를 상속하여 커스텀 쿼리 DTO를 만들 수 있습니다:

import { Dto, PaginationQueryDto, TypeIs } from '@asapjs/sequelize'; import UsersTable, { UserTypeEnum } from '../domain/entity/UsersTable'; @Dto({ name: 'get_user_list_query_dto', defineTable: UsersTable }) export default class GetUserListQueryDto extends PaginationQueryDto { @TypeIs.ENUM({ values: Object.keys(UserTypeEnum), comment: '유저 유형', }) type: UserTypeEnum; @TypeIs.STRING({ comment: '검색어 (이름, 이메일)' }) search: string; @TypeIs.BOOLEAN({ comment: '활성 상태' }) is_active: boolean; }

이렇게 하면 Swagger UI에 page, limit과 함께 type, search, is_active 파라미터가 모두 표시됩니다. 컨트롤러에서 query: GetUserListQueryDto로 지정합니다.

기본 사용법

1. 컨트롤러에서 페이지네이션 라우트 선언

import { ExecuteArgs, Get, RouterController } from '@asapjs/router'; import { UserApplication } from '../application/UserApplication'; import GetUserListQueryDto from '../dto/GetUserListQueryDto'; import UserDto from '../dto/UserDto'; export default class UserController extends RouterController { public basePath = '/users'; public tag = 'users'; private userService: UserApplication; constructor() { super(); this.registerRoutes(); this.userService = new UserApplication(); } @Get('/', { title: '사용자 목록 조회', description: '페이지네이션을 지원하는 사용자 목록을 조회합니다.', query: GetUserListQueryDto, response: UserDto, }) public getUserList = async ({ paging, user }: ExecuteArgs<{}, GetUserListQueryDto, {}>) => { const result = await this.userService.list(paging, user); return { result }; }; }

핵심 포인트:

  • query: GetUserListQueryDtoPaginationQueryDto를 상속하여 page, limit + 추가 필터를 Swagger에 문서화
  • response: UserDto — 응답 스키마를 Swagger에 등록
  • { paging, user }ExecuteArgs에서 파싱된 PaginationQueryType 객체({ page, limit })를 추출

2. Application 레이어에서 Repository로 위임

import type { PaginationQueryType } from '@asapjs/sequelize'; import UserDto from '../dto/UserDto'; import UserTableRepository from '../infra/UserTableRepository'; export class UserApplication { private usersRepository: UserTableRepository; constructor() { this.usersRepository = new UserTableRepository(); } public list = async (paging: PaginationQueryType, user: UserDto) => { const raws = await this.usersRepository.list(paging, user); return raws; }; }

Application 레이어는 PaginationQueryType을 인자 타입으로 사용하고, 실제 데이터베이스 조회는 Repository에 위임합니다.

3. Repository에서 페이지네이션 쿼리 실행

import { Repository } from '@asapjs/sequelize'; import type { PaginationQueryType } from '@asapjs/sequelize'; import UsersTable from '../domain/entity/UsersTable'; import UserDto from '../dto/UserDto'; export default class UserTableRepository extends Repository { private users: typeof UsersTable; constructor() { super(); this.users = UsersTable; } public list = async (paging: PaginationQueryType, user: UserDto) => { const users = await this.repository.findAll(this.users, { exportTo: UserDto, user, paging, }); return new UserDto().pagingMap(users); }; }

RepositoryfindAllpaging을 전달하면, 내부적으로 findAndCountAll을 실행하고 페이지네이션 메타데이터가 포함된 플랫 구조를 반환합니다. pagingMapdata 배열의 각 항목을 DTO 필드로 매핑합니다.

페이지네이션 응답 구조

Repository.findAllpaging이 전달되면, 다음과 같은 플랫 구조의 응답을 반환합니다:

{ data: T[]; // 현재 페이지의 항목 배열 page: number; // 현재 페이지 (0-based) page_size: number; // 페이지당 항목 수 (요청된 limit) max_page: number; // 마지막 페이지 인덱스: Math.ceil(total / limit) - 1 has_prev: boolean; // page > 0이면 true has_next: boolean; // max_page > page이면 true total_elements: number; // 전체 레코드 수 }
필드타입설명
dataT[]현재 페이지의 항목 배열 (DTO 매핑 후)
pagenumber현재 페이지 번호 (0-based)
page_sizenumber페이지당 항목 수
max_pagenumber마지막 페이지 인덱스 (Math.ceil(total / limit) - 1)
has_prevboolean이전 페이지 존재 여부 (page !== 0)
has_nextboolean다음 페이지 존재 여부 (max_page > page)
total_elementsnumber전체 레코드 수

참고 — Swagger 스키마와 실제 응답 키 불일치: 현재 TypeIs.PAGING이 생성하는 Swagger 스키마에서는 has_Next(대문자 N)로 기술되지만, Repository가 반환하는 실제 JSON 필드명은 has_next(소문자 n)입니다. 향후 코드 수정으로 통일될 예정입니다.

TypeIs.PAGING 응답 스키마

TypeIs.PAGING(Dto)는 페이지네이션 응답의 Swagger 스키마를 자동 생성합니다. 내부적으로 다음 구조의 OpenAPI 스키마를 만듭니다:

// TypeIs.PAGING이 생성하는 Swagger 스키마 구조 { type: 'object', properties: { data: { type: 'array', items: /* DTO의 swagger() 결과 ($ref) */, }, page: { type: 'integer', format: 'int32', description: '현재 페이지', }, page_size: { type: 'integer', format: 'int32', description: '페이지당 표시 개수', }, max_page: { type: 'integer', format: 'int32', description: '최대 페이지', }, has_prev: { type: 'boolean', description: '이전 이동가능 여부', }, has_next: { type: 'boolean', description: '다음 이동가능 여부', }, total_elements: { type: 'integer', format: 'int32', description: '전체 레코드 개수', }, }, }

참고: 위 스키마에서 has_next는 실제 코드(paging.ts)에서 has_Next로 되어 있습니다. 향후 코드 수정으로 has_next(소문자)로 통일될 예정입니다.

pagingMap을 활용한 DTO 매핑

ExtendableDtopagingMap 메서드를 사용하면 페이지네이션 결과의 data 배열을 DTO로 일괄 변환할 수 있습니다.

// ExtendableDto.pagingMap 내부 public pagingMap = (data: any): any => { const o: any = data.data; return { ...data, data: o.map((item: any) => this.map(item)) }; };

pagingMapdata 배열의 각 항목에 map()을 적용하여 DTO에 선언된 필드만 추출하고, 나머지 메타데이터(page, page_size, has_next 등)는 그대로 유지합니다.

Repository에서의 사용:

// UserTableRepository에서 public list = async (paging: PaginationQueryType, user: UserDto) => { const users = await this.repository.findAll(this.users, { exportTo: UserDto, user, paging, }); return new UserDto().pagingMap(users); };

API 호출 예시

# 기본 요청 (page=0, limit=20 적용) GET /users # 페이지네이션 파라미터 지정 GET /users?page=1&limit=10 # 필터 파라미터와 함께 (GetUserListQueryDto 확장 시) GET /users?page=0&limit=10&type=ADMIN&is_active=true

응답 예시:

{ "data": [ { "id": 1, "type": "NORMAL", "email": "user@example.com", "name": "홍길동", "phone": "010-1234-5678", "is_active": true, "created_at": "2024-01-15T09:30:00.000Z", "updated_at": "2024-01-15T09:30:00.000Z" } ], "page": 1, "page_size": 10, "max_page": 4, "has_prev": true, "has_next": true, "total_elements": 47 }

관련 문서

  • Request HandlingExecuteArgspaging 필드
  • DTOsExtendableDto, pagingMap, TypeIs.PAGING
  • Routingquery, response 옵션
  • DatabaseRepository, findAll, findOne
Last updated on