페이지네이션이란?
서버에서 클라이언트로 데이터를 전달할 때 데이터를 일정 기준으로 분할하여 전달하는 것을 의미한다.
우리가 게시판 형식의 웹에서 흔하게 보이는 것 처럼 많은 데이터를 한 화면에서 다 보여줄 수 없을 때 페이지를 넘겨 일정 양의 데이터로 끊어 이전페이지, 다음 페이지의 형태로 끊어서 보여주게 되는 형식이다.
페이지네이션을 사용했을 때의 장단점은 다음과 같다.
Django REST Framework에서는 기본적으로
BasePagination
CursorPagination
LimitOffsetPagination
PageNumberPagination
총 4가지의 pagination을 제공한다.
이번 글에서는 맨 마지막의 PageNumberPagination
의 사용에 대해 기술한다.
PageNumberPagination
을 다음과 같은 방법으로 사용했다.
from rest_framework.pagination import PageNumberPagination
from collections import OrderedDict
from rest_framework.response import Response
# 페이지네이션 예시
class ProductsListAPIPageNumberPagination(PageNumberPagination):
page_size = 10 # 페이지네이션 페이지 사이즈
def get_paginated_response(self, data):
try:
previous_page_number = self.page.previous_page_number()
except:
previous_page_number = None
try:
next_page_number = self.page.next_page_number()
except:
next_page_number = None
return Response(
OrderedDict(
[
("data", data),
("pageCnt", self.page.paginator.num_pages),
("curPage", self.page.number),
("nextPage", next_page_number),
("previousPage", previous_page_number),
]
)
)
위의 코드는 데이터를 10개씩 나누어 주는 페이지네이션 클래스의 코드이다.
하나씩 뜯어보면
PageNumberPagination
을 상속받아 클래스를 선언한다.class ProductsListAPIPageNumberPagination(PageNumberPagination):
page_size = 10 # 페이지네이션 페이지 사이즈
PageNumberPagination
의 get_paginated_response
함수를 오버라이딩 한다.def get_paginated_response(self, data):
try:
previous_page_number = self.page.previous_page_number()
except:
previous_page_number = None
try:
next_page_number = self.page.next_page_number()
except:
next_page_number = None
data
: 페이지네이션 클래스를 통해 분할된 정보들이 리스트 형식으로 들어간다.pageCnt
: 전체 페이지 수를 나타낸다.curPage
: 현재 페이지 위치를 나타낸다.nextPage
: 다음 페이지 정보를 나타낸다.previousPage
: 이전 페이지 정보를 나타낸다.return Response(
OrderedDict(
[
("data", data),
("pageCnt", self.page.paginator.num_pages),
("curPage", self.page.number),
("nextPage", next_page_number),
("previousPage", previous_page_number),
]
)
)
{
"data": [
{
"id": 1,
"name": "사과",
"price": 100,
"category": 1
},
{
"id": 2,
"name": "배",
"price": 200,
"category": 1
},
....
{
"id": 10,
"name": "키보드",
"price": 400,
"category": 2
}
],
"pageCnt": 2,
"curPage": 1,
"nextPage": 2,
"previousPage": null
}
이런 형식으로 데이터가 나오게 된다.
이번 글에서 사용하는 model이다
from django.db import models
class Category(models.Model):
"""Category Model Definition"""
name = models.CharField(max_length=200)
def __str__(self):
return self.name
class Product(models.Model):
"""Product Model Definition"""
name = models.CharField(max_length=200)
price = models.IntegerField()
category = models.ForeignKey(
"products.Category", related_name="products", on_delete=models.SET_NULL, null=True
)
def __str__(self):
return self.name
총 2개의 모델이 존재한다.
카테고리
name
: 카테고리 이름상품
name
: 상품의 이름price
: 가격category
: 카테고리 FK(Foreign Key)현재 페이지네이션을 사용하는 API 이다.
from rest_framework import generics
from .serializers import *
from .pagination import *
class ProductsListAPI(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
pagination_class = ProductsListAPIPageNumberPagination
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
Product
모델에서 전체 값을 가져와서 페이지네이션 클래스를 통해 데이터를 나눠서 보낸다.
├── __init__.py
├── admin.py
├── apis.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│
│ └── __init__.py
├── models.py
├── pagination.py
├── serializers.py
├── tests.py
├── urls.py
└── views.py
products
앱의 폴더 전체 구조는 위와 같다.
현재 페이지네이션을 통해 클라이언트에게 보내는 값은 다음과 같다.
- 상품 데이터
- 총 페이지 수
- 현재 페이지
- 이전페이지
- 다음페이지
서버에서 보내는 값을 확인해 보면 전체 데이터의 값을 확인 할 수 없다.
하지만 클라이언트에서 작업하다보면 순번을 메기거나 하는 등의 작업에 보내는 리스트 데이터의 전체 아이템의 개수가 필요 할 때가 있다.
이때는 API와 Pagination 클래스를 수정하여 해결한다.
from rest_framework.pagination import PageNumberPagination
from collections import OrderedDict
from rest_framework.response import Response
# 페이지네이션 예시
class ProductsListAPIPageNumberPagination(PageNumberPagination):
page_size = 10 # 페이지네이션 페이지 사이즈
def get_paginated_response(self, data, total_count):
try:
previous_page_number = self.page.previous_page_number()
except:
previous_page_number = None
try:
next_page_number = self.page.next_page_number()
except:
next_page_number = None
return Response(
OrderedDict(
[
("data", data),
("pageCnt", self.page.paginator.num_pages),
("totalCnt", total_count),
("curPage", self.page.number),
("nextPage", next_page_number),
("previousPage", previous_page_number),
]
)
)
from rest_framework import generics
from .serializers import *
from .pagination import *
class ProductsListAPI(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
pagination_class = ProductsListAPIPageNumberPagination
def get_paginated_response(self, data, total_count):
assert self.paginator is not None
return self.paginator.get_paginated_response(data, total_count)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
total_count = queryset.count()
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data, total_count)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
get_paginated_response
함수에 인자로 넘겨준다.get_paginated_response
함수를 오버라이딩하여 pagination class
에서 사용하는 self.paginator.get_paginated_response
함수에 전체 count 수를 인자로 넘겨준다total_count
를 OrderedDict
에 totalCnt
라는 이름으로 값을 넘겨준다.그렇게 하면 다음과 같은 데이터를 얻을 수 있다
{
"data": [
{
"id": 1,
"name": "사과",
"price": 100,
"category": 1
},
{
"id": 2,
"name": "배",
"price": 200,
"category": 1
},
...
{
"id": 10,
"name": "키보드",
"price": 400,
"category": 2
}
],
"pageCnt": 2,
"totalCnt": 13,
"curPage": 1,
"nextPage": 2,
"previousPage": null
}
위와 같이 totalcnt
를 통해 리스트 전체의 아이템 개수를 클라이언트에게 넘겨 줄 수 있다.
프론트에 넘겨주는 값을 확인하면 카테고리의 값을 넘겨주지만 카테고리의 id값을 보내주기 때문에 이를 카테고리 이름으로 변경하여 넘겨줄 필요가 있다.
{
"id": 1,
"name": "사과",
"price": 100,
"category": 1
}
데이터에 카테고리의 이름 값을 추가하기 위해 pagination class
를 수정한다.
from rest_framework.pagination import PageNumberPagination
from collections import OrderedDict
from rest_framework.response import Response
from .models import *
# 페이지네이션 예시
class ProductsListAPIPageNumberPagination(PageNumberPagination):
page_size = 10 # 페이지네이션 페이지 사이즈
def get_paginated_response(self, data, total_count):
try:
previous_page_number = self.page.previous_page_number()
except:
previous_page_number = None
try:
next_page_number = self.page.next_page_number()
except:
next_page_number = None
for i in data:
category = Category.objects.filter(id=i["category"]).first()
if category:
i["category_name"] = category.name
else:
i["category_name"] = ""
return Response(
OrderedDict(
[
("data", data),
("pageCnt", self.page.paginator.num_pages),
("totalCnt", total_count),
("curPage", self.page.number),
("nextPage", next_page_number),
("previousPage", previous_page_number),
]
)
)
pagination class
의 데이터 리스트에서 카테고리의 id값으로 Category
모델에서 name
값을 조회하여 데이터에 category_name
이라는 이름으로 데이터에 추가한다.
그럼 다음과 같은 데이터를 얻을 수 있다.
{
"data": [
{
"id": 1,
"name": "사과",
"price": 100,
"category": 1,
"category_name": "음식"
},
{
"id": 2,
"name": "배",
"price": 200,
"category": 1,
"category_name": "음식"
},
...
{
"id": 10,
"name": "키보드",
"price": 400,
"category": 2,
"category_name": "사무용품"
}
],
"pageCnt": 2,
"totalCnt": 13,
"curPage": 1,
"nextPage": 2,
"previousPage": null
}
위의 데이터를 보면 카테고리의 id값과 이름 모두 보내고 있음이 보인다.
페이지네이션을 통해 데이터를 넘겨 줄 때 페이지네이션 클래스를 수정하거나
DRF
에서 제공하는 내장함수를 잘 확인하여 오버라이딩 한다면 원하는 형식대로 데이터를 보낼 수 있다