๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Back
NestJs
20-Pagination-Cursor Pagination

1. Pagination ์ด๋ก 

  • Pagination์ด๋ž€? : ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ€๋ถ„์ ์œผ๋กœ ๋‚˜๋ˆ ์„œ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ธฐ์ˆ 
  • ์ฟผ๋ฆฌ์— ํ•ด๋‹น๋˜๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๋ฒˆ์— ๋‹ค ๋ถˆ๋Ÿฌ์˜ค์ง€ ์•Š๊ณ  ๋ถ€๋ถ„์ ์œผ๋กœ ์ชผ๊ฐœ์„œ ๋ถˆ๋Ÿฌ์˜จ๋‹ค. e.g.) ํ•œ๋ฒˆ์— 20๊ฐœ์”ฉ
    • ์ฟ ํŒก๊ฐ™์€ ์•ฑ์˜๊ฒฝ์šฐ ์ˆ˜์–ต๊ฐœ์˜ ์ƒํ’ˆ์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋˜์–ด ์žˆ๋Š”๋ฐ,
    • ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ’ˆ ๊ฒ€์ƒ‰ ํ™”๋ฉด์„ ๋“ค์–ด๊ฐˆ๋•Œ๋งˆ๋‹ค ๋ชจ๋“  ์ƒํ’ˆ์ •๋ณด๋ฅผ ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†กํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.
  • ํ˜„๋Œ€ ํด๋ผ์šฐ๋“œ ์‹œ์Šคํ…œ์€ ๋ฐ์ดํ„ฐ ์ „์†ก์— ๋ˆ์ด ๋“ ๋‹ค!
    • ๋ˆ์ด ์•ˆ๋“ค๋”๋ผ๋„ ์ˆ˜์–ต๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๋ฒˆ์— ๋ณด๋‚ด๋ฉด ๋ถ„๋ช… ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ํ„ฐ์งˆ๊ฒƒ์ด๋‹ค!
    • ๋ˆ์ด ํ„ฐ์ง€์ง€ ์•Š๋”๋ผ๋„ ๋ฐ์ดํ„ฐ ์ „์†ก์— ์ƒ๋‹นํžˆ ์˜ค๋žœ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด๊ฒƒ์ด๋‹ค!

Pagination์€ ๋Œ€ํ‘œ์ ์œผ๋กœ 2๊ฐ€์ง€๊ฐ€ ์กด์žฌํ•œ๋‹ค.

  1. Page Based Pagination
  2. Cursor Based Pagination

1.1 Page Based Pagination

pagination-1_1

  • ํŽ˜์ด์ง€ ๊ธฐ์ค€์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ž˜๋ผ์„œ ์š”์ฒญํ•˜๋Š” Pagination
  • ์š”์ฒญ์„ ๋ณด๋‚ผ๋•Œ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ฐฏ์ˆ˜์™€ ๋ช‡๋ฒˆ์งธ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ์ง€ ๋ช…์‹œ
  • request๋กœ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ๋ณด๋‚ด๋ฉด, ์ด ํ† ํƒˆ ํŽ˜์ด์ง€์™€ ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌํ„ดํ•ด์ฃผ๋Š” ๋ฐฉ์‹์ด๋‹ค.
  • ํŽ˜์ด์ง€ ์ˆซ์ž๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋‹ค์Œ ํŽ˜์ด์ง€๋กœ ๋„˜์–ด๊ฐ€๋Š” ํ˜•ํƒœ์˜ UI์—์„œ ๋งŽ์ด ์‚ฌ์šฉ
  • ๋ฌธ์ œ์  : Pagination ๋„์ค‘์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์‚ญ์ œ๋ ๊ฒฝ์šฐ,
    • ์ €์žฅ๋˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋ˆ„๋ฝ๋˜๊ฑฐ๋‚˜ ์ค‘๋ณต๋  ์ˆ˜ ์žˆ์Œ
  • Pagination ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด ๋งค์šฐ ๊ฐ„๋‹จํ•จ
  • 100๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๋ฒˆ์— 20๊ฐœ์”ฉ ๊ฐ€์ ธ์˜จ๋‹ค๋ฉด 5๊ฐœ์˜ ํŽ˜์ด์ง€๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ๋‹ค.

1.1.1 Page Based Pagination ๋ฌธ์ œ์ 

(1) ๋ฐ์ดํ„ฐ ์‚ฝ์ž…

pagination-1_2

Page Based Pagination์˜ ๋ฌธ์ œ์ ์€ ๋ฐ์ดํ„ฐ ์‹ ๊ทœ ์‚ฝ์ž… ์‹œ ์ค‘๋ณต๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

  • ์œ„ ๊ทธ๋ฆผ์—์„œ 1ํŽ˜์ด์ง€์—์„œ ํ™”๋ฉด์— 1, 2, 3, 4์—์„œ 3.5๋ž€ ์‹ ๊ทœ ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฝ์ž…๋˜์—ˆ๋‹ค.
  • 3.5 ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฝ์ž…๋˜๋ฉด์„œ 4๋ฒˆ ๋ฐ์ดํ„ฐ๊ฐ€ 1ํŽ˜์ด์ง€์™€ 2ํŽ˜์ด์ง€์— ์ค‘๋ณต๋˜์–ด ๋‚˜ํƒ€๋‚œ๋‹ค.

(2) ๋ฐ์ดํ„ฐ ์‚ญ์ œ

pagination-1_3

์ด๋ฒˆ์—๋Š” id=4๊ฐ€ ์‚ญ์ œ๋œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด, id=5๊ฐ€ ๋ˆ„๋ฝ๋œ๋‹ค.


1.2 Cursor Based Pagination

pagination-1_4

  • ๊ฐ€์žฅ ์ตœ๊ทผ์— ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” Pagination
  • ์š”์ฒญ์„ ๋ณด๋‚ผ๋•Œ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ์˜ ๊ธฐ์ค€๊ฐ’(ID๋“ฑ Unique ๊ฐ’)๊ณผ ๋ช‡๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ์ง€ ๋ช…์‹œ
  • ์Šคํฌ๋กค ํ˜•ํƒœ์˜ ๋ฆฌ์ŠคํŠธ์—์„œ ์ž์ฃผ ์‚ฌ์šฉ e.g.) ์•ฑ์˜ ListView
  • ์žฅ์  : ์ตœ๊ทผ ๋ฐ์ดํ„ฐ์˜ ๊ธฐ์ค€๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฟผ๋ฆฌ๊ฐ€ ์ž‘์„ฑ๋˜๊ธฐ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋ˆ„๋ฝ๋˜๊ฑฐ๋‚˜ ์ค‘๋ณต๋  ํ™•๋ฅ ์ด ์ ์Œ
  • e.g. ๋งˆ์ง€๋ง‰์œผ๋กœ ๋ถˆ๋Ÿฌ์˜จ ๋ฐ์ดํ„ฐ๊ฐ€ 20๋ฒˆ์ด์—ˆ๋‹ค๋ฉด ๋‹ค์Œ ์š”์ฒญ์€ 21๋ฒˆ๋ถ€ํ„ฐ ๊ฐ€์ ธ์˜จ๋‹ค

(1) ๋ฐ์ดํ„ฐ ์‚ฝ์ž…

pagination-1_5

  • ํŠน์ • ์ปค์„œ ์œ„์น˜๋ถ€ํ„ฐ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ค‘๋ณต๋œ ๋ฐ์ดํ„ฐ ์—†์ด ์ผ๊ด€๋œ ์ •๋ณด๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.
  • ์‚ฌ์šฉ์ž๊ฐ€ ํ˜„์žฌ๊นŒ์ง€ ๋กœ๋“œํ•œ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ์˜ ์œ„์น˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„ ๋ถ€ํ•˜๊ฐ€ ์ค„์–ด๋“ ๋‹ค.

(2) ๋ฐ์ดํ„ฐ ์‚ญ์ œ

pagination-1_6


1.3 ์š”์ฒญ ํ˜•ํƒœ

1
http://localhost:3000/posts?order__createdAt=ASC&where__id_more_than=3&take=20
2
3
// ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ, id๊ฐ€ 3๋ณด๋‹ค ํฐ ๋ฐ์ดํ„ฐ๋ฅผ, 20๊ฐœ์˜ ํฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

{property}__{filter}ํ˜•์‹์œผ๋กœ ๊ตฌํ˜„

  • order__createAt : ๋‚ด๋ฆผ/์˜ค๋ฆ„์ฐจ ์ •๋ ฌ
  • where__id_more_than : ์–ด๋–ค ID ์ดํ›„๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•  ๊ฑด์ง€
  • take : ๋ช‡ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ• ๊ฑด์ง€

1.4 ์‘๋‹ต ํ˜•ํƒœ

1
{
2
// ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ›๋Š” ๋ถ€๋ถ„
3
"data": [
4
{
5
"id": 4,
6
"updatedAt": "2023-06-06T15:41:33.928Z",
7
"createdAt": "2023-06-06T15:41:33.928Z",
8
"title": "test",
9
"content": "test123",
10
"likeCount": 0,
11
"commentCount": 0,
12
"author": {
13
"id": 1,
14
"nickname": "codefactory5",
15
"email": "test5@codefactory.ai",
16
"role": "USER"
17
}
18
}
19
],
20
// paging ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๋Š” ๊ณณ
21
"paging": {
22
// ๋‹ค์Œ ์ปค์„œ์—๋Œ€ํ•œ ์ •๋ณด
23
"cursor": {},
24
"after": 4,
25
// ์ด ๋ช‡๊ฐœ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์‘๋‹ต์™”๋Š”์ง€
26
"count": 20,
27
// ๋‹ค์Œ ์š”์ฒญ URL
28
"next": "http://localhost:3000/post?order__createdAt=ASC&take=20&where__id__more_than=4"
29
}
30
}

2. PaginationPostDto ์ƒ์„ฑ

posts/dto/paginate-post.dto.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

paginate-post.dto.ts
1
import { IsIn, IsNumber, IsOptional } from 'class-validator'
2
3
export class PaginatePostDto {
4
/*** ์ด์ „ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ์˜ ID
5
* ์ด ํ”„๋กœํผํ‹ฐ์— ์ž…๋ ฅ๋œ ID๋ณด๋‹ค ๋†’์€ ID๋ถ€ํ„ฐ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ธฐ
6
*/
7
@IsNumber()
8
@IsOptional()
9
where__id_more_than?: number
10
11
/*** ์ •๋ ฌ
12
* createdAt : ์ƒ์„ฑ๋œ ์‹œ๊ฐ„์˜ ๋‚ด๋ฆผ์ฐจ/์˜ค๋ฆ„์ฐจ ์ˆœ์œผ๋กœ ์ •๋ ฌ
13
*/
14
@IsIn(['ASC']) // ๋ฆฌ์ŠคํŠธ์— ์žˆ๋Š” ๊ฐ’๋“ค๋งŒ ํ—ˆ์šฉ
15
@IsOptional()
16
// eslint-disable-next-line @typescript-eslint/prefer-as-const
17
order__createAt: 'ASC' = 'ASC'
18
19
/*** ๊ฐ–๊ณ ์˜ฌ ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜
20
* ๋ช‡ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ›์„์ง€
21
*/
22
@IsNumber()
23
@IsOptional()
24
take: number = 20
25
}

posts ์ปจํŠธ๋กค๋Ÿฌ์— getPosts๋ฅผ ์ˆ˜์ •ํ•œ๋‹ค.

posts.controller.ts
1
// posts.controller.ts ์ƒ๋žต
2
@Get()
3
getPosts(
4
@Query() query: PaginatePostDto, //
5
) {
6
return this.postsService.getAllPosts()
7
}

3. MoreThan๊ณผ Order๋กœ ํ•„ํ„ฐ๋ง

posts ์„œ๋น„์Šค์— paginatePosts์„ ์ž‘์„ฑํ•œ๋‹ค.

posts.service.ts
1
// posts.service.ts ์ƒ๋žต
2
/***
3
* 1) ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜๋Š” pagination๋งŒ ๊ตฌํ˜„ํ•œ๋‹ค
4
*/
5
async paginatePosts(dto: PaginatePostDto) {
6
const posts = await this.postsRepository.find({
7
where: {
8
// ๋” ํฌ๋‹ค / ๋” ๋งŽ๋‹ค
9
id: MoreThan(dto.where__id_more_than ?? 0),
10
},
11
order: {
12
createdAt: dto.order__createAt,
13
},
14
take: dto.take,
15
})
16
17
/*** Response
18
* data : Data[],
19
* cursor : {
20
* after: ๋งˆ์ง€๋ง‰ Data์˜ ID
21
* }
22
* count: ์‘๋‹ตํ•œ ๋ฐ์ดํ„ฐ์˜ ๊ฐœ์ˆ˜
23
* next: ๋‹ค์Œ ์š”์ฒญ์„ ํ•  ๋–„ ์‚ฌ์šฉํ•  URL
24
*/
25
return {
26
data: posts,
27
}
28
}

posts ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ž‘์„ฑํ•œ paginatePosts๋ฅผ ์ ์šฉํ•œ๋‹ค.

posts.controller.ts
1
// posts.controller.ts
2
/*** 1) GET /posts
3
* ๋ชจ๋“  post๋ฅผ ๋‹ค ๊ฐ€์ ธ์˜จ๋‹ค
4
*/
5
@Get()
6
getPosts(
7
@Query() query: PaginatePostDto, //
8
) {
9
return this.postsService.paginatePosts(query)
10
}

์ •๋ ฌ ์˜ต์…˜์ด ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋ ค๋ฉด main.ts์— tranform ์˜ต์…˜์„ true๋กœ ๋ฐ”๊ฟ”์ค˜์•ผ ํ•œ๋‹ค.

main.ts
1
// ์ƒ๋žต
2
async function bootstrap() {
3
const app = await NestFactory.create(AppModule)
4
app.useGlobalPipes(
5
new ValidationPipe({
6
transform: true,
7
}),
8
)
9
await app.listen(3000)
10
}
11
bootstrap()
  • ์ด๋ ‡๊ฒŒ ํ•ด์ฃผ๋Š” ์ด์œ ๋Š” posts ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋งŒ์•ฝ query์— ๊ฐ’์„ ์‹ค์ œ๋กœ ๋„ฃ์€ ์ ์ด ์—†๋‹ค๋ฉด,
    • ๋„ฃ์ง€ ์•Š์•˜์œผ๋‹ˆ๊นŒ, ๋„ฃ์ง€ ์•Š์€ ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•ด์ค€๋‹ค.
    • ์ด๊ฒŒ classValidator์™€ classTransformer๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.
  • ๊ทธ๋Ÿฐ๋ฐ ๊ฐ’์„ ๋„ฃ์ง€ ์•Š๋”๋ผ๋„ ์„ ์–ธํ•œ ๊ธฐ๋ณธ ๊ฐ’๋“ค์„ ๋„ฃ์€ ์ฑ„๋กœ, ํด๋ž˜์Šค๋ฅผ ๋ณ€ํ˜•ํ•ด DTO๋ฅผ ํ˜•์„ฑํ•ด ์คฌ์œผ๋ฉด ํ•˜๊ธฐ ๋•Œ๋ฌธ์—
    • main.ts์—์„œ transform : true๋ฅผ ๋„ฃ์–ด์คŒ์œผ๋กœ์จ
    • ๊ฐ’์„ ๋„ฃ์ง€ ์•Š์•˜๋‹ค๋ฉด ๋””ํดํŠธ ๊ฐ’๋“ค์ด DTO์—๋‹ค๊ฐ€ ๋„ฃ์€ ์ฑ„๋กœ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ด๋„ ๋œ๋‹ค๊ณ  ํ—ˆ๊ฐ€๋ฅผ ํ•ด์ฃผ๋Š” ๊ฒƒ.
    • ์ฆ‰, ๋ณ€ํ™˜ํ•˜๋Š” ์ž‘์—…์„ ํ•ด๋„ ๊ดœ์ฐฎ๋‹ค๋ผ๋Š” ์˜ต์…˜์„ ๋„ฃ์–ด์ค€ ๊ฒƒ์ด๋‹ค.

4. ๋žœ๋ค ๋ฐ์ดํ„ฐ ์ƒ์„ฑํ•˜๋Š” ๋กœ์ง ๋งŒ๋“ค๊ธฐ

ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก, ์ž„์˜๋กœ ์ถฉ๋ถ„ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ posts ์„œ๋น„์Šค์— ๋งŒ๋“ ๋‹ค.

posts.service.ts
1
// posts.service.ts
2
/*** ํŽ˜์ด์ง€๋„ค์ด์…˜์šฉ ํ…Œ์ŠคํŠธ ํฌ์ŠคํŠธ ์ƒ์„ฑ
3
*
4
*/
5
async generatePosts(userId: number) {
6
for (let i = 0; i < 100; i++) {
7
await this.createPost(userId, {
8
title: `์ž„์˜๋กœ ์ƒ์„ฑ๋œ ํฌ์ŠคํŠธ ์ œ๋ชฉ ${i}`,
9
content: `์ž„์˜๋กœ ์ƒ์„ฑ๋œ ํฌ์ŠคํŠธ ๋‚ด์šฉ ${i}`,
10
})
11
}
12
}

๊ทธ๋ฆฌ๊ณ  ํŽ˜์ด์ง€๋„ค์ด์…˜ ํ…Œ์ŠคํŠธ์šฉ ์—”๋“œํฌ์ธํŠธ๋ฅผ posts ์ปจํŠธ๋กค๋Ÿฌ์— ์ถ”๊ฐ€ํ•œ๋‹ค.

posts.controller.ts
1
// posts.controller.ts ์ƒ๋žต
2
/*** POST /posts/random
3
*
4
*/
5
@Post('random')
6
@UseGuards(AccessTokenGuard)
7
async postPostsRandom(@User() user: UsersModel) {
8
await this.postsService.generatePosts(user.id)
9
return true
10
}
  • cf. ํ”„๋กœ๋•์…˜(์‹ค์ œ ๋ฐฐํฌ) ๋‹จ๊ณ„์—์„œ๋Š” ์ด๋Ÿฐ ํ…Œ์ŠคํŠธ API๋“ค์„ ๋ฐ˜๋“œ์‹œ ์ง€์›Œ์ค˜์•ผ ํ•œ๋‹ค.
  • ํฌ์ŠคํŠธ๋งจ์—์„œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

5. Type Annotation ์‚ฌ์šฉ & Implicit Conversion ์ ์šฉ

  • URL์— ๋ณด๋‚ด๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋Š” String์œผ๋กœ ๋ฐ›๋Š”๋‹ค.
  • ๊ทธ๋ž˜์„œ URL์— string์œผ๋กœ ๋“ค์–ด์˜จ ๊ฐ’์„ number๋กœ ๋ณ€ํ™˜ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

paginate-post.dto.ts์— Type ๋ณ€ํ™˜ ์–ด๋…ธํ…Œ์ด์…˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

paginate-post.dto.ts
1
import { Type } from 'class-transformer'
2
import { IsIn, IsNumber, IsOptional } from 'class-validator'
3
4
export class PaginatePostDto {
5
/*** ์ด์ „ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ์˜ ID
6
* ์ด ํ”„๋กœํผํ‹ฐ์— ์ž…๋ ฅ๋œ ID๋ณด๋‹ค ๋†’์€ ID๋ถ€ํ„ฐ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ธฐ
7
*/
8
@Type(() => Number) // URL์— string์œผ๋กœ ๋“ค์–ด์˜จ ๊ฐ’์„ number๋กœ ๋ณ€ํ™˜ํ•ด์คŒ
9
@IsNumber()
10
@IsOptional()
11
where__id_more_than?: number
12
13
// ์ƒ๋žต
14
}

๊ทผ๋ฐ ์ด๋ ‡๊ฒŒ Type ์–ด๋…ธํ…Œ์ด์…˜๋ง๊ณ , main.ts์—์„œ ์˜ต์…˜์„ ์ด์šฉํ•ด์ค„ ์ˆ˜๋„ ์žˆ๋‹ค.

  • ์œ„์—์„œ ์ถ”๊ฐ€ํ•œ @Type(() => Number)๋ฅผ ์ง€์šด๋‹ค.
  • main.ts๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.
main.ts
1
// main.ts ์ƒ๋žต
2
async function bootstrap() {
3
const app = await NestFactory.create(AppModule)
4
app.useGlobalPipes(
5
new ValidationPipe({
6
transform: true,
7
transformOptions: {
8
// ์ž„์˜๋กœ ๋ณ€ํ™˜์„ ํ—ˆ๊ฐ€
9
enableImplicitConversion: true,
10
},
11
}),
12
)
13
await app.listen(3000)
14
}
15
bootstrap()
  • ์ด๋ ‡๊ฒŒ true๋กœ ์˜ต์…˜์„ ์„ค์ •ํ•ด์ฃผ๋ฉด,
  • paginate-post.dto์™€ ๊ฐ™์€ ํŒŒ์ผ์˜ IsNumber ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ์œผ๋ฉด,
  • ์ˆซ์ž๋กœ ๋ณ€ํ™˜๋˜๋Š” ๊ฒƒ์ด ์ •์ƒ์ธ์ง€ ์•Œ์•„์„œ ํŒ๋‹จํ•˜๊ณ , ์ž๋™์œผ๋กœ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
  • ์ฆ‰, ์ž…๋ ฅ๋œ type์„ ๊ธฐ์ค€์œผ๋กœ ์ž๋™์œผ๋กœ class-transformer๊ฐ€ ์ž‘์šฉํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค.

6. CursorPagination ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์„ฑ

common/const/env.const.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

common/const/env.const.ts
1
export const PROTOCOL = 'http'
2
export const HOST = 'localhost:3000'

posts ์„œ๋น„์Šค์— paginatePosts๋ฅผ ์ˆ˜์ •ํ•œ๋‹ค.

posts.service.ts
1
// posts.service.ts ์ƒ๋žต
2
/***
3
* 1) ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜๋Š” pagination๋งŒ ๊ตฌํ˜„ํ•œ๋‹ค
4
*/
5
async paginatePosts(dto: PaginatePostDto) {
6
const posts = await this.postsRepository.find({
7
where: {
8
// ๋” ํฌ๋‹ค / ๋” ๋งŽ๋‹ค
9
id: MoreThan(dto.where__id_more_than ?? 0),
10
},
11
order: {
12
createdAt: dto.order__createAt,
13
},
14
take: dto.take,
15
})
16
17
/****
18
* ํ•ด๋‹น๋˜๋Š” ํฌ์ŠคํŠธ๊ฐ€ 0๊ฐœ ์ด์ƒ์ด๋ฉด, ๋งˆ์ง€๋ง‰ ํฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ 
19
* ์•„๋‹ˆ๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
20
*/
21
const lastItem = posts.length > 0 ? posts[posts.length - 1] : null
22
const nexttUrl = lastItem && new URL(`${PROTOCOL}://${HOST}/posts`)
23
24
/**** dto์˜ ํ‚ค๊ฐ’๋“ค์„ ๋ฃจํ•‘ํ•˜๋ฉด์„œ
25
* ํ‚ค๊ฐ’์— ํ•ด๋‹น๋˜๋Š” ๋ฒจ๋ฅ˜๊ฐ€ ์กด์žฌํ•˜๋ฉด, parame์— ๊ทธ๋Œ€๋กœ ๋ถ™์—ฌ๋„ฃ๋Š”๋‹ค.
26
* ๋‹จ, where__id_more_than ๊ฐ’๋งŒ lastItem์˜ ๋งˆ์ง€๋ง‰ ๊ฐ’์œผ๋กœ ๋„ฃ์–ด์ค€๋‹ค.
27
*/
28
if (nexttUrl) {
29
for (const key of Object.keys(dto)) {
30
if (dto[key]) {
31
if (key !== 'where__id_more_than') {
32
nexttUrl.searchParams.append(key, dto[key])
33
}
34
}
35
}
36
nexttUrl.searchParams.append('where__id_more_than', lastItem.id.toString())
37
}
38
39
/*** Response
40
* data : Data[],
41
* cursor : {
42
* after: ๋งˆ์ง€๋ง‰ Data์˜ ID
43
* }
44
* count: ์‘๋‹ตํ•œ ๋ฐ์ดํ„ฐ์˜ ๊ฐœ์ˆ˜
45
* next: ๋‹ค์Œ ์š”์ฒญ์„ ํ•  ๋–„ ์‚ฌ์šฉํ•  URL
46
*/
47
return {
48
data: posts,
49
cursor: {
50
after: lastItem?.id,
51
},
52
count: posts.length,
53
nest: nexttUrl?.toString(),
54
}
55
}

7. ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ๋กœ์ง ์กฐ๊ฑด ์ถ”๊ฐ€

posts.service.ts
1
// posts.service.ts์˜ paginatePosts ์ƒ๋žต
2
/****
3
* ํ•ด๋‹น๋˜๋Š” ํฌ์ŠคํŠธ๊ฐ€ 0๊ฐœ ์ด์ƒ์ด๋ฉด, ๋งˆ์ง€๋ง‰ ํฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ 
4
* ์•„๋‹ˆ๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
5
*/
6
const lastItem = posts.length > 0 && posts.length === dto.take ? posts[posts.length - 1] : null
7
const nexttUrl = lastItem && new URL(`${PROTOCOL}://${HOST}/posts`)

8. ๋‹ค์Œ ์ปค์„œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ๋–„ undefined๋Œ€์‹  null

posts.service.ts
1
// posts.service.ts์˜ paginatePosts ์ƒ๋žต
2
/*** Response
3
* data : Data[],
4
* cursor : {
5
* after: ๋งˆ์ง€๋ง‰ Data์˜ ID
6
* }
7
* count: ์‘๋‹ตํ•œ ๋ฐ์ดํ„ฐ์˜ ๊ฐœ์ˆ˜
8
* next: ๋‹ค์Œ ์š”์ฒญ์„ ํ•  ๋–„ ์‚ฌ์šฉํ•  URL
9
*/
10
return {
11
data: posts,
12
cursor: {
13
after: lastItem?.id ?? null,
14
},
15
count: posts.length,
16
nest: nexttUrl?.toString() ?? null,
17
}

9. ๋‚ด๋ฆผ์ฐจ์ˆœ Next ํ† ํฐ ๋กœ์ง ์ž‘์„ฑ

paginate-post.dto.ts์— ๋‚ด๋ฆผ์ฐจ์ˆœ์„ ์œ„ํ•œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

paginate-post.dto.ts
1
import { IsIn, IsNumber, IsOptional } from 'class-validator'
2
3
export class PaginatePostDto {
4
@IsNumber()
5
@IsOptional()
6
where__id_less_than?: number
7
8
/*** ์ด์ „ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ์˜ ID
9
* ์ด ํ”„๋กœํผํ‹ฐ์— ์ž…๋ ฅ๋œ ID๋ณด๋‹ค ๋†’์€ ID๋ถ€ํ„ฐ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ธฐ
10
*/
11
@IsNumber()
12
@IsOptional()
13
where__id_more_than?: number
14
15
/*** ์ •๋ ฌ
16
* createdAt : ์ƒ์„ฑ๋œ ์‹œ๊ฐ„์˜ ๋‚ด๋ฆผ์ฐจ/์˜ค๋ฆ„์ฐจ ์ˆœ์œผ๋กœ ์ •๋ ฌ
17
*/
18
@IsIn(['ASC', 'DESC']) // ๋ฆฌ์ŠคํŠธ์— ์žˆ๋Š” ๊ฐ’๋“ค๋งŒ ํ—ˆ์šฉ
19
@IsOptional()
20
order__createAt: 'ASC' | 'DESC' = 'ASC'
21
22
/*** ๊ฐ–๊ณ ์˜ฌ ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜
23
* ๋ช‡ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ›์„์ง€
24
*/
25
@IsNumber()
26
@IsOptional()
27
take: number = 20
28
}

posts ์„œ๋น„์Šค์— ๋‚ด๋ฆผ์ฐจ์ˆœ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

posts.service.ts
1
// posts.service.ts์˜ paginatePosts ์ƒ๋žต
2
/**** dto์˜ ํ‚ค๊ฐ’๋“ค์„ ๋ฃจํ•‘ํ•˜๋ฉด์„œ
3
* ํ‚ค๊ฐ’์— ํ•ด๋‹น๋˜๋Š” ๋ฒจ๋ฅ˜๊ฐ€ ์กด์žฌํ•˜๋ฉด, parame์— ๊ทธ๋Œ€๋กœ ๋ถ™์—ฌ๋„ฃ๋Š”๋‹ค.
4
* ๋‹จ, where__id_more_than ๊ฐ’๋งŒ lastItem์˜ ๋งˆ์ง€๋ง‰ ๊ฐ’์œผ๋กœ ๋„ฃ์–ด์ค€๋‹ค.
5
*/
6
if (nexttUrl) {
7
for (const key of Object.keys(dto)) {
8
if (dto[key]) {
9
if (key !== 'where__id_more_than' && key !== 'where__id_less_than') {
10
nexttUrl.searchParams.append(key, dto[key])
11
}
12
}
13
}
14
let key = null
15
if (dto.order__createAt === 'ASC') {
16
key = 'where__id_more_than'
17
} else {
18
key = 'where__id_less_than'
19
}
20
nexttUrl.searchParams.append(key, lastItem.id.toString())
21
}

10. ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ Where ์ฟผ๋ฆฌ ์ž‘์„ฑ

posts.service.ts
1
// posts.service.ts์˜ paginatePosts ์ƒ๋žต
2
async paginatePosts(dto: PaginatePostDto) {
3
const where: FindOptionsWhere<PostsModel> = {}
4
5
if (dto.where__id_less_than) {
6
where.id = LessThan(dto.where__id_less_than)
7
} else if (dto.where__id_more_than) {
8
where.id = MoreThan(dto.where__id_more_than)
9
}
10
11
const posts = await this.postsRepository.find({
12
where,
13
order: {
14
createdAt: dto.order__createAt,
15
},
16
take: dto.take,
17
})
18
19
// ์ƒ๋žต
20
}