[DRF] - Pagination

오동훈·2022년 11월 13일
0

Django

목록 보기
11/23
post-custom-banner

1. Pagination이란

pagination은 다들 경험해 보았을 것입니다. 대표적으로 게시판 같은 곳을 살펴보면 1페이지, 2페이지, ... , 마지막 페이지 등 페이지 별로 한정된 글의 양이 나타나 있는 것을 확인할 수 있습니다.

왜 이걸 구현할까?

pagination이 뭔지는 알겠는데 이걸 왜 구현할까?

이유는 단순합니다. 불러오는 데이터 양 때문에 그렇습니다. 데이터의 양이 적으면 모든 게시물을 불러오는데 무리가 없겠지만 서비스가 커지고 데이터가 많아지면 이 모든 데이터를 불러오는데 막대한 시간과 데이터베이스에 성능 저하의 주 원인이 될 수 있기 때문입니다.

그래서 우리는 pagination을 구현해 한정적 데이터를 보여주고 성능적으로 영향이 없도록 만들어 보려합니다.

사용법

Django에서 Pagination를 구현하는 방법은 여러가지가 있겠지만 대표적으로 2가지를 보여드리면 다음과 같습니다.

page가 입력되는 방식

page의 수는 http://127.0.0.1:8000/?page=20와 같이 URL 끝에 Query string 방식으로 입력받게 됩니다.

1. 수작업

먼저 첫 번째로 수작업입니다.

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)

우선 먼저 나눠서 살펴봐보면 다음과 같습니다.

1. Query string 방식으로 입력받은 파라미터를 받아 client가 어떤 페이지를 원하고 있는지 파악합니다.

try:
    page = request.query_params.get("page", 1)
    page = int(page)
except ValueError:
    page = 1

2. 한 페이지 당 몇 개의 게시물을 보여줄건지 정하고(PAGE_SIZE), 시작점과 마지막 점을 지정해줍니다.

page_size = settings.PAGE_SIZE
start = (page - 1) * page_size
end = start + page_size

3. ORM에 limit을 달아주고 데이터를 보내주면 끝입니다.

serializer = ReviewSerializer(
    room.reviews.all()[start:end],
    many=True,
)
return Response(serializer.data)

2. DRF Pagination 이용하기

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)
        .
        .
        .

이것도 나누어 살펴보면 다음과 같습니다.

1. 데이터를 불러오고 한 페이지에 담길 게시물 수를 두 번째 인자로 넘겨줍니다.

Paginator는 2개의 인자를 필요로 하는데
1. 첫 번째 인자 : 페이지로 분할될 객체
2. 두 번째 인자 : 한 페이지에 담길 객체의 수를 필요로 합니다.

room = Room.reviews.all()
paginator = Paginator(room, PAGE_SIZE)

2. client가 어떤 페이지를 원하고 있는지 파악하고, 해당 페이지를 paginator.get_page()에 넘겨줍니다.

그러면 get_page 함수는 페이지 번호를 받아 해당 페이지를 리턴하게 됩니다. 그 후 serializer를 이용해 정보를 반환해주면 성공입니다.

page = request.query_params.get("page", 1)
posts = paginator.get_page(page)

2. Template 연동

Django에서 pagination 기능을 구현한 것과 Template에서 작동되기까지 연동을 시켜보려고 합니다. 부분부분 살펴보기 이전에 우선 전체적인 HTML 코드입니다.

1. Django 코드

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})

[ Pagination 함수 ]

1. Paginator(object_list, per_page)

paginator = Paginator(object_list=instance, per_page=10)

Paginator("QuerySet", "페이지 당 보여질 목록의 개수")를 호출하면 Paginator라는 객체가 생성됩니다.

2. Paginator.get_page(number)

posts = paginator.get_page(page)

Paginator 객체get_page(number)를 통해 해당하는 페이지에 보여질 objects들을 queryset 형태로 반환합니다.

3. Paginator.page(number)

number에 해당하는 인덱스 객체를 반환합니다.

  • 해당 number를 정수로 변환할 수 없는 경우 PageNotAnInteger를 발생
  • 주어진 페이지 번호가 존재하지 않으면 EmptyPage 발생

[ Pagination 속성 ]

1. Paginator.count

모든 페이지에 있는 개체의 총 수입니다.

2. Paginator.num_pages

총 페이지 수입니다.

3. Paginator.page_range

페이지 범위가 나타납니다.
ex) [1, 2, 3, 4]


Custom range

하지만 위의 범위를 가지고 표현을 하기에는 부적절합니다. 페이지 개수가 수없이 많고 그 범위를 모두 노출한다면 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

2. HTML 코드

여기서 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>

1. posts.has_previous

posts.has_previous는 이전 페이지가 존재하는지 확인합니다. 만약 페이지가 1이라면 이전 페이지가 없으므로 존재할 때만 보여주게끔 설정해 주었습니다.

2. posts.has_next

마찬가지로 posts.has_next는 다음 페이지 존재 여부를 확인해 줍니다. 똑같이 다음 페이지가 존재할 때만 보여주게끔 설정해 주었습니다.


참고 자료 📩

Django - pagination 공식 문서
Django - pagination MTV 처리하기

profile
삽질의 기록들🐥
post-custom-banner

0개의 댓글