pagination
은 다들 경험해 보았을 것입니다. 대표적으로 게시판 같은 곳을 살펴보면 1페이지, 2페이지, ... , 마지막 페이지 등 페이지 별로 한정된 글의 양이 나타나 있는 것을 확인할 수 있습니다.
pagination
이 뭔지는 알겠는데 이걸 왜 구현할까?
이유는 단순합니다. 불러오는 데이터 양 때문에 그렇습니다. 데이터의 양이 적으면 모든 게시물을 불러오는데 무리가 없겠지만 서비스가 커지고 데이터가 많아지면 이 모든 데이터를 불러오는데 막대한 시간과 데이터베이스에 성능 저하의 주 원인이 될 수 있기 때문입니다.
그래서 우리는 pagination
을 구현해 한정적 데이터를 보여주고 성능적으로 영향이 없도록 만들어 보려합니다.
Django에서 Pagination를 구현하는 방법은 여러가지가 있겠지만 대표적으로 2가지를 보여드리면 다음과 같습니다.
page의 수는 http://127.0.0.1:8000/?page=20
와 같이 URL 끝에 Query string
방식으로 입력받게 됩니다.
먼저 첫 번째로 수작업입니다.
class RoomReviews(APIView):
.
.
.
def get(self, request, pk):
try:
page = request.query_params.get("page", 1)
page = int(page)
except ValueError:
page = 1
page_size = settings.PAGE_SIZE
start = (page - 1) * page_size
end = start + page_size
room = self.get_object(pk)
serializer = ReviewSerializer(
room.reviews.all()[start:end],
many=True,
)
return Response(serializer.data)
우선 먼저 나눠서 살펴봐보면 다음과 같습니다.
Query string
방식으로 입력받은 파라미터를 받아 client가 어떤 페이지를 원하고 있는지 파악합니다.try:
page = request.query_params.get("page", 1)
page = int(page)
except ValueError:
page = 1
page_size = settings.PAGE_SIZE
start = (page - 1) * page_size
end = start + page_size
serializer = ReviewSerializer(
room.reviews.all()[start:end],
many=True,
)
return Response(serializer.data)
DRF는 우리가 이러한 작업을 하며 시간을 뺏기는걸 싫어하는 거 같습니다. 알면 알수록 놀라운데 구현되어 있는게 거의 없는 거 같아요...
어떻게 구현되어 있는지 한 번 살펴봐보죠.
from django.core.paginator import Paginator
class RoomReviews(APIView):
.
.
.
def get(self, request, pk):
room = Room.reviews.all()
paginator = Paginator(room, PAGE_SIZE)
page = request.query_params.get("page", 1)
posts = paginator.get_page(page)
.
.
.
이것도 나누어 살펴보면 다음과 같습니다.
Paginator는 2개의 인자를 필요로 하는데
1. 첫 번째 인자 : 페이지로 분할될 객체
2. 두 번째 인자 : 한 페이지에 담길 객체의 수를 필요로 합니다.
room = Room.reviews.all()
paginator = Paginator(room, PAGE_SIZE)
그러면 get_page 함수는 페이지 번호를 받아 해당 페이지를 리턴하게 됩니다. 그 후 serializer를 이용해 정보를 반환해주면 성공입니다.
page = request.query_params.get("page", 1)
posts = paginator.get_page(page)
Django에서 pagination 기능을 구현한 것과 Template에서 작동되기까지 연동을 시켜보려고 합니다. 부분부분 살펴보기 이전에 우선 전체적인 HTML 코드입니다.
def custom_paginator(request, instance):
page = request.GET.get('page')
if not page:
page = 1
page = int(page)
paginator = Paginator(object_list=instance, per_page=10)
posts = paginator.get_page(page)
""" 10 page 씩 보여주게끔 설정한 부분 """
start_page = int(((page - 1) / 10)) * 10
end_page = start_page + 10
custom_range = range(start_page + 1, end_page + 1)
if end_page + 1 > paginator.page_range[-1]:
custom_range = range(start_page + 1, paginator.page_range[-1] + 1)
""" 1페이지, 마지막 페이지 """
first_page = paginator.page_range[0]
last_page = paginator.page_range[-1]
""" 10 page 앞, 뒤 페이지 """
page_ten_move_forward = custom_range[0] - 10
page_ten_move_back = custom_range[0] + 10
""" 0 - 미노출, 1 - 노출 """
move_forward_flag = 1
move_back_flag = 1
if page <= 10:
page_ten_move_forward = 1
move_forward_flag = 0
if custom_range[0] + 10 > paginator.page_range[-1]:
page_ten_move_back = paginator.page_range[-1]
move_back_flag = 0
move_items = {
"first": first_page, "last": last_page,
"move_forward": page_ten_move_forward, "move_back": page_ten_move_back,
"move_forward_flag": move_forward_flag, "move_back_flag": move_back_flag,
}
return posts, paginator, custom_range, move_items
def 다른 함수:
.
.
.
posts, paginator, custom_range = custom_paginator(request, patterns)
return render(request, 'HTML Page", {'posts': posts, 'paginator': paginator, "custom_range": custom_range})
paginator = Paginator(object_list=instance, per_page=10)
Paginator("QuerySet", "페이지 당 보여질 목록의 개수")
를 호출하면 Paginator
라는 객체가 생성됩니다.
posts = paginator.get_page(page)
Paginator 객체
는 get_page(number)
를 통해 해당하는 페이지에 보여질 objects들을 queryset
형태로 반환합니다.
number
에 해당하는 인덱스 객체를 반환합니다.
- 해당 number를 정수로 변환할 수 없는 경우
PageNotAnInteger
를 발생- 주어진 페이지 번호가 존재하지 않으면
EmptyPage
발생
모든 페이지에 있는 개체의 총 수입니다.
총 페이지 수입니다.
페이지 범위가 나타납니다.
ex) [1, 2, 3, 4]
하지만 위의 범위를 가지고 표현을 하기에는 부적절합니다. 페이지 개수가 수없이 많고 그 범위를 모두 노출한다면 UI도 원하는 대로 표현이 안될 것입니다. 그래서 저는 원하는 범위만을 노출시키기 위해 커스텀 범위를 지정해주었습니다.
지금 아래 보이는 사진은 무신사 스토어 페이지네이션 처리한 부분입니다. 이와 같이 한 번 만들어보려 합니다.
""" 10 page 씩 보여주게끔 설정한 부분 """
start_page = int(((page - 1) / 10)) * 10
end_page = start_page + 10
custom_range = range(start_page + 1, end_page + 1)
if end_page + 1 > paginator.page_range[-1]:
custom_range = range(start_page + 1, paginator.page_range[-1] + 1)
""" 1페이지, 마지막 페이지 """
first_page = paginator.page_range[0]
last_page = paginator.page_range[-1]
""" 10 page 앞, 뒤 페이지 """
page_ten_move_forward = custom_range[0] - 10
page_ten_move_back = custom_range[0] + 10
""" 0 - 미노출, 1 - 노출 """
move_forward_flag = 1
move_back_flag = 1
if page <= 10:
page_ten_move_forward = 1
move_forward_flag = 0
if custom_range[0] + 10 > paginator.page_range[-1]:
page_ten_move_back = paginator.page_range[-1]
move_back_flag = 0
move_items = {
"first": first_page, "last": last_page,
"move_forward": page_ten_move_forward, "move_back": page_ten_move_back,
"move_forward_flag": move_forward_flag, "move_back_flag": move_back_flag,
}
return posts, paginator, custom_range, move_items
여기서 flag는 크게 신경쓰지 않으셔도 됩니다. 코드 로직 상 분기해야 될 부분들이 생겨 처리해놓은 부분이고 그 외에 값 들이 어떻게 들어가고 표현되는지만 체크해주시면 됩니다.
<div class="col-4">
<nav class="d-flex">
<ul class="pagination mx-auto">
<!-- 진짜 맨 앞으로 이동하는 버튼 시작 -->
{% if move_items.move_forward_flag %}
<li class="page-item">
{% if flag == 0 %}
<a class="page-link" href="?page={{ move_items.first }}"><<</a>
{% elif flag == 1 %}
<a class="page-link" href="?created_at={{ date }}&type={{ type }}&page={{ move_items.first }}"><<</a>
{% elif flag == 2 %}
<a class="page-link" href="?created_at={{ date | date:"Y-m-d" }}&malware_type={{ malware_type }}&package={{ package }}&page={{ move_items.first }}"><<</a>
{% endif %}
</li>
{% endif %}
<!-- 진짜 맨 앞으로 이동하는 버튼 끝 -->
<!-- 10 page 앞으로 이동하는 버튼 시작 -->
{% if posts.has_previous %}
<li class="page-item">
{% if flag == 0 %}
<a class="page-link" href="?page={{ move_items.move_forward }}"><</a>
{% elif flag == 1 %}
<a class="page-link" href="?created_at={{ date }}&type={{ type }}&page={{ move_items.move_forward }}"><</a>
{% elif flag == 2 %}
<a class="page-link" href="?created_at={{ date | date:"Y-m-d" }}&malware_type={{ malware_type }}&package={{ package }}&page={{ move_items.move_forward }}"><</a>
{% endif %}
</li>
{% endif %}
<!-- 10 page 앞으로 이동하는 버튼 끝 -->
<!-- page 표현되는 부분 시작 -->
<li class="page-item active">
{% for page in custom_range %}
{% if page == posts.number %}
{% if flag == 0 %}
<li class="page-item active"><a class="page-link" href="?page={{page}}">{{ page }}</a></li>
{% elif flag == 1 %}
<li class="page-item active"><a class="page-link" href="?created_at={{ date }}&type={{ type }}&page={{ page }}">{{ page }}</a></li>
{% elif flag == 2 %}
<li class="page-item active"><a class="page-link" href="?created_at={{ date | date:"Y-m-d" }}&malware_type={{ malware_type }}&package={{ package }}&page={{ page }}">{{ page }}</a></li>
{% endif %}
{% else %}
{% if flag == 0 %}
<li class="page-item"><a class="page-link" href="?page={{page}}">{{ page }}</a></li>
{% elif flag == 1 %}
<li class="page-item"><a class="page-link" href="?created_at={{ date }}&type={{ type }}&page={{ page }}">{{ page }}</a></li>
{% elif flag == 2 %}
<li class="page-item"><a class="page-link" href="?created_at={{ date | date:"Y-m-d" }}&malware_type={{ malware_type }}&package={{ package }}&page={{ page }}">{{ page }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
</li>
<!-- page 표현되는 부분 끝 -->
<!-- 10 page 뒤로 이동하는 버튼 시작 -->
{% if posts.has_next %}
<li class="page-item">
{% if flag == 0 %}
<a class="page-link" href="?page={{ move_items.move_back }}">></a>
{% elif flag == 1 %}
<a class="page-link" href="?created_at={{ date }}&type={{ type }}&page={{ move_items.move_back }}">></a>
{% elif flag == 2 %}
<a class="page-link" href="?created_at={{ date | date:"Y-m-d" }}&malware_type={{ malware_type }}&package={{ package }}&page={{ move_items.move_back }}">></a>
{% endif %}
</li>
{% else %}
<li class="page-item"></li>
{% endif %}
<!-- 10 page 뒤로 이동하는 버튼 끝-->
<!-- 진짜 맨 뒤로 이동하는 버튼 시작-->
{% if move_items.move_back_flag %}
{% if posts.has_next %}
<li class="page-item">
{% if flag == 0 %}
<a class="page-link" href="?page={{ move_items.last }}">>></a>
{% elif flag == 1 %}
<a class="page-link" href="?created_at={{ date }}&type={{ type }}&page={{ move_items.last }}">>></a>
{% elif flag == 2 %}
<a class="page-link" href="?created_at={{ date | date:"Y-m-d" }}&malware_type={{ malware_type }}&package={{ package }}&page={{ move_items.last }}">>></a>
{% endif %}
</li>
{% else %}
<li class="page-item"></li>
{% endif %}
{% endif %}
<!-- 진짜 맨 뒤로 이동하는 버튼 끝 -->
</ul>
</nav>
</div>
posts.has_previous
는 이전 페이지가 존재하는지 확인합니다. 만약 페이지가 1이라면 이전 페이지가 없으므로 존재할 때만 보여주게끔 설정해 주었습니다.
마찬가지로 posts.has_next
는 다음 페이지 존재 여부를 확인해 줍니다. 똑같이 다음 페이지가 존재할 때만 보여주게끔 설정해 주었습니다.
참고 자료 📩