[Django] 페이지네이션 커스텀, Pagination Custom

오제욱·2023년 2월 15일
2
post-thumbnail

1. DRF pagination


1-1 페이지네이션의 필요성

페이지네이션이란?

서버에서 클라이언트로 데이터를 전달할 때 데이터를 일정 기준으로 분할하여 전달하는 것을 의미한다.

우리가 게시판 형식의 웹에서 흔하게 보이는 것 처럼 많은 데이터를 한 화면에서 다 보여줄 수 없을 때 페이지를 넘겨 일정 양의 데이터로 끊어 이전페이지, 다음 페이지의 형태로 끊어서 보여주게 되는 형식이다.


페이지네이션을 사용했을 때의 장단점은 다음과 같다.

장점

  • 필요한 양만큼의 데이터를 불러오기 때문에 전체의 데이터를 불러올 때 보다 서버의 부담이 적다.
  • 많은 데이터 중에서 일정 부분만 클라이언트에서 띄워주기 때문에 사용자가 브라우저를 통해 데이터를 봤을 때 가독성이 올라간다.
  • 현재 사용자 자신이 어느 정도의 페이지에서 정보를 보고 있는지 알 수 있기 때문에 웹의 사용감의 측면에서도 좋다.

단점

  • 페이지네이션은 프론트앤드와 백앤드 양쪽에서 모두 추가적인 작업이 필요하다.
  • 페이지네이션을 통해 한페이지에 들어가는 정보의 양을 제한하기 때문에 한번에 많은 양의 정보를 비교하지 못하고 충분한 정보 탐색을 위해서는 여러 페이지를 방문 해야한다.

Django를 통해 API를 개발하다 보면 프론트에 보낼 데이터를 페이지네이션을 통해 페이지로 구분해서 보내줘야 할 필요성이 생긴다.

1-2 DRF의 기본 제공 페이지 네이션

Django REST Framework에서는 기본적으로

BasePagination
CursorPagination
LimitOffsetPagination
PageNumberPagination

총 4가지의 pagination을 제공한다.

이번 글에서는 맨 마지막의 PageNumberPagination의 사용에 대해 기술한다.

1-3 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개씩 나누어 주는 페이지네이션 클래스의 코드이다.

하나씩 뜯어보면

  • DRF의 PageNumberPagination 을 상속받아 클래스를 선언한다.
class ProductsListAPIPageNumberPagination(PageNumberPagination):

  • 한 페이지 당 10개씩의 데이터를 보여줌을 나타낸다.
page_size = 10 # 페이지네이션 페이지 사이즈

  • PageNumberPaginationget_paginated_response 함수를 오버라이딩 한다.
def get_paginated_response(self, data):

  • 현재 페이지를 기준으로 이전 페이지와 다음페이지의 번호를 지정한다.
  • 현재 페이지가 맨 마지막이거나 첫번째 페이지라서 다음페이지나 이전페이지가 없을경우 None으로 둔다.
	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
}

이런 형식으로 데이터가 나오게 된다.

2. example setting


2-1 Model

이번 글에서 사용하는 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개의 모델이 존재한다.

Category

카테고리

  • name : 카테고리 이름

Product

상품

  • name : 상품의 이름
  • price : 가격
  • category : 카테고리 FK(Foreign Key)

2-2 API

현재 페이지네이션을 사용하는 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 모델에서 전체 값을 가져와서 페이지네이션 클래스를 통해 데이터를 나눠서 보낸다.

2-3 전체 폴더 구조

├── __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 앱의 폴더 전체 구조는 위와 같다.

3. Pagination Custom


3-1 전체 데이터 수 추가

현재 페이지네이션을 통해 클라이언트에게 보내는 값은 다음과 같다.

  • 상품 데이터
  • 총 페이지 수
  • 현재 페이지
  • 이전페이지
  • 다음페이지

서버에서 보내는 값을 확인해 보면 전체 데이터의 값을 확인 할 수 없다.
하지만 클라이언트에서 작업하다보면 순번을 메기거나 하는 등의 작업에 보내는 리스트 데이터의 전체 아이템의 개수가 필요 할 때가 있다.
이때는 API와 Pagination 클래스를 수정하여 해결한다.

pagination class

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),
                ]
            )
        )

apis.py

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)

API

  • 해당 api에서 사용하는 쿼리를 카운트 하여 면수에 담아 get_paginated_response 함수에 인자로 넘겨준다.
  • get_paginated_response 함수를 오버라이딩하여 pagination class에서 사용하는 self.paginator.get_paginated_response함수에 전체 count 수를 인자로 넘겨준다

pagination class

  • 인자로 받은 total_countOrderedDicttotalCnt라는 이름으로 값을 넘겨준다.

그렇게 하면 다음과 같은 데이터를 얻을 수 있다

{
	"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 를 통해 리스트 전체의 아이템 개수를 클라이언트에게 넘겨 줄 수 있다.

3-2 FK, name 값 넘기기

프론트에 넘겨주는 값을 확인하면 카테고리의 값을 넘겨주지만 카테고리의 id값을 보내주기 때문에 이를 카테고리 이름으로 변경하여 넘겨줄 필요가 있다.

{
	"id": 1,
	"name": "사과",
	"price": 100,
	"category": 1
}

데이터에 카테고리의 이름 값을 추가하기 위해 pagination class 를 수정한다.

pagination

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값과 이름 모두 보내고 있음이 보인다.

3. 결론

페이지네이션을 통해 데이터를 넘겨 줄 때 페이지네이션 클래스를 수정하거나 DRF 에서 제공하는 내장함수를 잘 확인하여 오버라이딩 한다면 원하는 형식대로 데이터를 보낼 수 있다

profile
Django Python 개발자

0개의 댓글