BLOG
Python

Django ListView에서 Pagination 활용하기 (Page, Paginator 클래스)


Oct. 24, 2021, 5:25 p.m.



Django에서 Class 중점 개발을 할때 ListView를 많이 활용합니다. ListView를 사용하면 모델을 나열하는 페이지를 보다 편리하게 만들 수 있죠. 만약 ListView를 이용해서 블로그 같은 페이지를 만들 때 포스트가 굉장히 많다면 어떨까요? ListView를 그냥 사용하게 되면 수많은 페이지를 한 화면에 보여주게 되어 페이지 길이도 굉장히 길어지고 로딩 시간도 길어질 것입니다. 그럴때는 페이지를 나누어서 페이지당 정해진 수의 게시글만 보여주는 식으로 하여 이러한 문제를 해결할 수 있습니다.

Django의 ListView에서는 Paination을 이용하여 이를 구현할 수 있습니다. 이번 포스트에서는 Django ListView에서 Pagination을 십분 활용하는 방법들에 대해서 소개하겠습니다.

1. Pagination 설정


ListView에서 Pagination을 사용하려면 ListView Class에 paginate_by 변수를 추가해주면 됩니다. 아래의 예시를 보겠습니다.

from django.views.generic import ListView

from myapp.models import Contact

class ContactListView(ListView):
    paginate_by = 5
    model = Contact

Contact라는 모델을 사용하는 ListView 클래스에 paginate_by = 5 라는 변수를 설정해 주었습니다. 이렇게 되면 이 ListView는 한 페이지당 5개의 모델 오브젝트만 제공합니다.

2. Page 클래스


Pagination을 사용하면 템플릿으로 전달되는 context에 Page 클래스가 page_obj 라는 이름으로 포함되게 됩니다.

Page 클래스는 아래와 같은 메서드를 지원합니다.

  • Page.has_next() (다음 페이지가 있으면 True 반환)
  • Page.has_previous() (이전 페이지가 있으면 True 반환)
  • Page.has_other_pages() (이전 혹은 다음 페이지가 있으면 True 반환)
  • Page.next_page_number() (다음 페이지의 페이지 번호를 반환)
  • Page.previous_page_number() (이전 페이지의 페이지 번호를 반환)
  • Page.start_index() (이 페이지의 모델 인덱스 반환)
  • Page.end_index() (이 페이지의 모델 마지막 인덱스 반환)

그리고 3개의 변수를 가지고 있습니다.

  • Page.object_list (이 페이지에 표시될 모델 오브젝트 리스트)
  • Page.number (페이지 번호)
  • Page.paginator (이 페이지를 관리하는 Paginator 클래스)

Paginator 클래스에 대해서는 후에 알아보도록 하겠습니다.

그럼 위의 정보를 이용해서 실제로 템플릿에서는 어떻게 활용할까요?

먼저 for 문을 page_obj에 적용해서 모델 오브젝트를 가져올 수 있습니다. 아래와 같이요.

{% for contact in page_obj %}
    <p>{{ contact.full_name }}</p>
    ...
{% endfor %}

object_list를 불러올 필요없이 바로 for문을 적용해도 된다는 점이 중요합니다.

그럼 이전, 다음 페이지로 이동하는 부분은 어떻게 만들까요? 아래의 예제를 보겠습니다.

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1">&laquo; first</a>
            <a href="?page={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}

        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">next</a>
        {% endif %}
    </span>
</div>

보시면 앞서 보았던 Page 오브젝트의 메서드를 이용해서 다음, 이전 페이지가 존재하면 그 페이지로 이동할 수 있는 링크를 만드는 모습입니다. 해당 페이지로 이동하려면 페이지 주소에 쿼리문으로 페이지 번호를 넘겨주어야 한다는 점을 잊지 마세요.

하지만 여기서도 아쉬운 점이 있습니다. 지금은 단지 이전, 다음 페이지로만 이동할 수 있으며 특정 페이지로 직접 이동하는 버튼은 없습니다. 이어서 Paginator 클래스를 이용해서 Pagination을 더 우아하게 하는 방법을 소개하겠습니다.

3. Paginator 클래스


Paginator 클래스는 Pagination의 핵심이 되는 클래스입니다. Paginator 클래스가 모델을 페이지 별로 나누어 Page 오브젝트를 만듭니다. 여기서 Paginator 클래스를 모두 설명하지는 않겠습니다. 나머지는 공식 문서를 확인하세요!

https://docs.djangoproject.com/en/3.2/ref/paginator/#django.core.paginator.Paginator

대신에 그 중에서 쓸만한 기능 2가지를 중점으로 보겠습니다.

  • Paginator.orphans
  • Paginator.get_elided_page_range(number, *, on_each_side, on_ends)

먼저 orphans는 한 페이지를 구성하기에는 부족해서 맨 마지막 페이지에 붙일 모델의 개수를 의미합니다. 만약 한 페이지에 5개씩인데 orphans가 3일때, 총 모델의 개수가 13개라면 3페이지가 아니라 나머지 3개의 모델이 2페이지로 붙어 총 2페이지가 되는 것입니다.

이 기능이 필요한 이유는 여러가지지만 디자인 상 페이지에 오브젝트가 한두 개 있을 경우 페이지 구조 상 비어 보이거나 디자인이 깨지는 경우가 있기에 이 기능이 필요한 것입니다. 제 블로그도 지금 이 기능을 사용하고 있죠.

ListView에서는 이를 쉽게 설정할 수 있습니다. ListView 클래스에 paginate_orphans 변수를 추가해주세요.

class PostList(ListView):
  model = Post
  ordering = '-created_at'
  paginate_by = 5
  paginate_orphans = 3

그러면 설정 끝!

마지막으로 get_elided_page_range() 에 대해서 알아보겠습니다.

블로그와 같은 사이트들을 보면 Paginator 역할을 하는 부분에 이전, 다음 버튼도 있지만 현재 페이지를 중심으로 주변의 여러 페이지들로 이동할 수 있게 버튼을 제공합니다. 현재 페이지가 5페이지라면 [2, 3, 4, 5, 6, 7, 8] 이런식으로 주변 페이지로 바로 이동할 수 있게요.

get_elided_page_range() 함수가 바로 이런 역할을 합니다. 함수에 들어가는 인수는 아래와 같습니다.

  • number
  • on_each_side
  • on_ends

number에는 현재 페이지를 넣습니다. on_each_side는 현재 페이지를 기준으로 앞뒤 몇 페이지까지 표시 할지 정합니다. on_ends는 추가로 맨앞, 맨뒤 페이지로 부터 몇 페이지까지 표시 할지 정합니다.

예를 들어 총 페이지의 수가 50이고 number가 21, on_each_side가 3, on_ends가 2라면 아래와 같은 리스트를 반환합니다.

[1, 2, '...', 18,19, 20, 21, 22, 23, 24, '...', 49, 50]

이 기능을 이용하면 훨씬 더 좋은 Pagination을 구현할 수 있습니다. 실제로 ListView에 적용해 볼까요?

아쉽게도 이 기능은 ListView 클래스에 변수를 추가하는 것으로 적용이 되지 않습니다. 직접 context에 get_elided_page_range()로 받아온 리스트를 넣어주는 방식을 써보겠습니다.

ListView 클래스를 아래와 같이 수정해주세요.

class PostList(ListView):
  ...
  paginate_by = 5
  paginate_orphans = 3

  def get_context_data(self, **kwargs):
    context = super(PostList, self).get_context_data()
    ...
    page = context['page_obj']
    paginator = page.paginator
    pagelist = paginator.get_elided_page_range(page.number, on_each_side=3, on_ends=0)
    context['pagelist'] = pagelist
    ... 
    return context

먼저 기존의 get_context_data() 함수를 통해 context를 가져옵니다. 이 context에는 page_obj라는 해당 페이지의 Page 클래스가 포함되어 있습니다. Page.paginator를 통해 Page 클래스와 연관된 Paginator클래스를 가져오고, get_elided_page_range() 함수에 Page.number와 기타 인수를 넣은 반환값을 pagelist로 context에 담았습니다.

이를 템플릿에서 적용하면 이런 모습이 될 것입니다.

<ul class="pagination justify-content-center mb-4">
    {% if page_obj.has_previous %}
    <li class="page-item">
        <a class="page-link" href="?page={{ page_obj.previous_page_number }}">&larr;</a>
    </li>
    {% else %}
        <li class="page-item disabled">
        <a class="page-link" href="#">&larr;</a>
    </li>
        {% endif %}

    {% for index in pagelist %}
        {% if index == page_obj.number %}
    <li class="page-item active">
        <span class="page-link">{{ index }}</span>
    </li>
        {% else %}
        <li class="page-item">
            <a class="page-link" href="?page={{ index }}">{{ index }}</a>
        </li>
        {% endif %}
    {% endfor %}

        {% if page_obj.has_next %}
    <li class="page-item">
        <a class="page-link" href="?page={{ page_obj.next_page_number }}">&rarr;</a>
    </li>
        {% else %}
        <li class="page-item disabled">
        <a class="page-link" href="#">&rarr;</a>
    </li>
        {% endif %}
</ul>

부트스트랩4의 Pagination 컴포넌트를 활용하였습니다.

먼저 이전 페이지가 있다면 이전 페이지 버튼을 활성화 시키고, 없다면 비활성화 시킵니다.

그 다음 pagelist로 넘어간 리스트를 활용해 주변 페이지로 이동할 수 있는 버튼을 만듭니다. 또한 현재 페이지는 알아 볼 수 있게 합니다.

마찬가지로 다음 페이지가 있다면 다음 페이지 버튼을 활성화 시키고, 없다면 비활성화 시킵니다.

결과적으로 이런 모습의 Pagination을 구현할 수 있게 됩니다. (1페이지라고 가정했을 때 모습)

여러분도 Paginator와 Page 클래스를 십분 활용해서 빠르고 편리한 사이트를 만들 수 있길 바랍니다!

Django ListView Page Paginator


jellyho   

Pagination

Oct. 24, 2021, 10:14 p.m.



Search