페이지네이션
ASAPJS는 페이지네이션을 프레임워크 레벨에서 지원합니다. 모든 라우트 핸들러에 paging 객체가 자동으로 제공되며, PaginationQueryDto와 TypeIs.PAGING을 조합하면 Swagger 문서까지 한 번에 생성됩니다.
동작 원리
클라이언트가 ?page=0&limit=10 쿼리 파라미터를 보내면, @asapjs/router의 Wrapper 함수가 이를 자동으로 파싱하여 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 };| 파라미터 | 기본값 | 설명 |
|---|---|---|
page | 0 | 페이지 번호 (0부터 시작) |
limit | 20 | 한 페이지당 항목 수 |
참고:
page는 0-based 인덱스입니다. 첫 번째 페이지는page=0이며,Repository는 내부적으로offset = limit * page로 계산합니다.
참고:
paging은query옵션에PaginationQueryDto를 지정하지 않아도 모든 핸들러에서 사용할 수 있습니다.Wrapper가 모든 요청에 대해 자동으로 생성합니다.
PaginationQueryDto와 PaginationQueryType
PaginationQueryDto
@asapjs/sequelize에서 제공하는 내장 DTO로, page와 limit 두 필드를 가집니다. 라우트 데코레이터의 query 옵션에 전달하면, Swagger UI에 page와 limit 쿼리 파라미터가 자동으로 문서화됩니다.
export default class PaginationQueryDto extends ExtendableDto {
@TypeIs.INT({ comment: '페이지' })
page: number;
@TypeIs.INT({ comment: '한 페이지당 표시 개수' })
limit: number;
}PaginationQueryType
PaginationQueryType은 PaginationQueryDto에서 page와 limit만 추출한 타입 별칭입니다:
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: GetUserListQueryDto—PaginationQueryDto를 상속하여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);
};
}Repository의 findAll에 paging을 전달하면, 내부적으로 findAndCountAll을 실행하고 페이지네이션 메타데이터가 포함된 플랫 구조를 반환합니다. pagingMap은 data 배열의 각 항목을 DTO 필드로 매핑합니다.
페이지네이션 응답 구조
Repository.findAll에 paging이 전달되면, 다음과 같은 플랫 구조의 응답을 반환합니다:
{
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; // 전체 레코드 수
}| 필드 | 타입 | 설명 |
|---|---|---|
data | T[] | 현재 페이지의 항목 배열 (DTO 매핑 후) |
page | number | 현재 페이지 번호 (0-based) |
page_size | number | 페이지당 항목 수 |
max_page | number | 마지막 페이지 인덱스 (Math.ceil(total / limit) - 1) |
has_prev | boolean | 이전 페이지 존재 여부 (page !== 0) |
has_next | boolean | 다음 페이지 존재 여부 (max_page > page) |
total_elements | number | 전체 레코드 수 |
참고 — 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 매핑
ExtendableDto의 pagingMap 메서드를 사용하면 페이지네이션 결과의 data 배열을 DTO로 일괄 변환할 수 있습니다.
// ExtendableDto.pagingMap 내부
public pagingMap = (data: any): any => {
const o: any = data.data;
return { ...data, data: o.map((item: any) => this.map(item)) };
};pagingMap은 data 배열의 각 항목에 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 Handling —
ExecuteArgs와paging필드 - DTOs —
ExtendableDto,pagingMap,TypeIs.PAGING - Routing —
query,response옵션 - Database —
Repository,findAll,findOne