오늘도 개발

[1차 프로젝트 록차] 상품 목록 GET api 제작기 4 - django paginator로 페이지네이션 구현하기 본문

TIL & 프로젝트 회고

[1차 프로젝트 록차] 상품 목록 GET api 제작기 4 - django paginator로 페이지네이션 구현하기

Sueeeeeee 2022. 7. 23. 01:32

필터는 어느정도 구현을 마쳤는데 페이지네이션을 해야 했다.

페이지네이션은 데이터를 원하는 만큼 끊어서 보여주는 기능이다.

 

클라이언트가 쿼리 파라미터로 몇 개씩 끊어 보여달라고 요청하면

그걸 받아 응답할 Queryset에 적용하면 될 거라는 생각은 들었는데

정확히 어떻게 구현해야 할 지는 감이 잡히지 않았다.

 

각 HTTP 통신은 stateless한데 내 api가 '다음 페이지를 보여줘'라는 요청을 어떻게 이해할 수 있을까?

클라이언트가 현재 보고있는 페이지를 몇 번인지 내 api가 어떻게 추적할 수 있을까?

두 가지로 고민을 했는데 검색을 해보니 페이지네이션을 하겠다고 클라이언트의 위치를 내 api로 추적할 필요는 없었다.

 

페이지네이션의 기본 동작

우선 클라이언트는 그냥 '다음 페이지를 보여줘'라고 요청하지 않는다.

클라이언트는 쿼리 파라미터에 몇 개씩 끊어 보여달라(limit)는 정보와

현재 페이지 넘버(offset)를 담아 요청한다고 가정해야 한다.

 

그럼 데이터가 총 10개 있는데

chars = ['a', 'b', 'c', 'd', 'e']

클라이언트가 /chars?limit=3&offset=2라고 요청하면 두 번째 페이지는 항상 ['d', 'e']가 된다.

클라이언트의 위치를 추적할 필요가 없는 것이다.

 

즉, 백에서는 응답 메시지에

1) limit을 적용했을 때 나오는 총 페이지 개수

2) 현재 offset에서 보여줄 데이터 리스트

두 가지만 담아 보내주면 된다.

 

django Paginator class

django에서는 페이지네이션을 구현할 수 있는 Paginator 클래스를 제공한다.

Paginator 클래스에 오브젝트 리스트와 각 페이당 보여줄 데이터 개수를 입력하면

Paginator 인스턴스를 만들 수 있다.

from django.core.paginator import Paginator

objects = ['apple', 'orange', 'bannana', 'pear', 'mango']
p = Paginator(objects, 2)

생성한 Paginator 인스턴스에는 Paginator가 제공하는 각종 메서드를 적용할 수 있다.

# 전체 데이터 개수
p.count
# 5

# 페이지 개수
p.num_pages
# 3

# 페이지의 range 오브젝트(iteration할 수 있음)
p.page_range
# range(1, 4)

# 페이지 하나씩 가져오기
page1 = p.page(1)
print(first_page)
# <Page 1 of 3>

# 한 페이지에 포함된 데이터 리스트
page1.object_list
# ['apple', 'orange']

# 다음 페이지가 있는지 확인하기
page1.has_next()
# True

# 이전 페이지가 있는지 확인하기
page1.has_previous()
# False

# 이전 페이지나 다음 페이지가 있는지 확인하기
page1.has_other_pages()
# True

page1.previous_page_number()
# 이전이나 다음 페이지가 없는데 호출하면 EmptyPage 에러남

p.page(0)
# 없는 페이지를 호출해도 EmptyPage 에러남

 

페이지네이션 구현

공부한 내용을 바탕으로 다음과 같이 페이지네이션을 구현했다.

쿼리 파라미터로 한 페이지에 보여줄 상품 개수를 명시하지 않는다면 디폴트로 10개씩 끊어 보여주며,

쿼리 파라미터로 특정한 페이지를 명시하지 않는다면 1페이지를 보여준다.

 

from django.core.paginator import Paginator

class ProductListView(View):
   def get(self, request):
   	  # ... 윗줄 생략
      	limit              = int(request.GET.get("limit", 10))
        offset             = int(request.GET.get("offset", 1))
        products = Product.objects.filter(queries).order_by(ordering).distinct()
        
        # 페이지네이터 오브젝트 생성
        p = Paginator(products, limit)
        pages_count = p.num_pages
		
        # 존재할 수 없는 페이지를 요청받은 경우 오류 반환
        if offset < 1 or offset > pages_count:
            return JsonResponse({'result': 'INVALID_PAGE'}, status=404)
		
        # 총 상품 개수, 총 페이지 수, 현재 페이지 수, 페이지 당 상품 수 정보 반환
        total = {
            'total_items' : products.count(),
            'total_pages' : pages_count,
            'current_page': offset,
            'limit'       : limit
        }
        
        # 해당 페이지의 상품만 반환
        page_items = p.page(offset) 
        products = [{
            'id'              : page_item.id,
            'title'           : page_item.title,
            'price'           : page_item.price,
            'stock'           : page_item.stock,
            'thumbnail_images': [image.url for image in page_item.thumbnail_images.all()],
            'types'           : [type.name for type in page_item.types.all()]
        } for page_item in page_items]

        return JsonResponse({'total' : total, 'products': products}, status=200)

 

참고

Django 공식문서 - pagination

Pagination made easy with Django Rest Framework