IT/WEB

[Django] 페이지네이션(페이징)

개발자 두더지 2022. 7. 9. 00:12
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 직역 및 오역, 의역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

 대량의 데이터 목록을 표시할 경우 여러 개의 페이지로 나눠서 표시하게 된다. 이 때 Django의 경우, 페이지네이션(페이징)이라는 기술이 사용된다. 

 예를 들어, Web 어플리케이션에서 자사의 유저 목록을 표시하는 경우, 유저가 10명 정도라면 문제없다. 그러나 몇 백명인 경우, 페이지를 나눠서 목록을 표시하는 것이 사용하기나 읽기에 편리하다. 또한, 블로그 포스트의 수가 100개 이상인 경우 1개의 페이지에 포스트 모두를 표시하는 경우는 없다.

 Django의 페이지를 여러 개로 나누는 페이지 네이션(페이징)을 사용하여 한 화면에 표시하는 데이터 수를 제한해보자. 설명할 때 사용한 환경은 Python 3.7.5, Django 3.0.0이다.

 

 

Django에서의 페이지네이션(페이징)


Django에는 페이지 네이션을 서포트하는 기능이 기본적으로 구현되어 있다. 페이지네이션 기능을 사용하기 위해선, view.py에서 Django.core.paginator 모듈의 Paginator, EmptyPage, PageNotAnInteger을 사용한다.

 

 

Paginator 클래스


Paginator 클래스의 구문은 다음과 같다.

class Paginator(object_list, per_page, orphans=0, allow_empty_first_page=True)
인수 설명
object_list 페이지 표시하고 싶은 리스트 혹은 튜플, QuerySet을 지정
per_page 한 페이지에 표시하는 항목 수 를 지정
orphans 마지막 페이지의 항목 수가 지정한 수 이하인 경우, 이전 페이지에 항목을 포함
allow_empty_first_page 맨 처음의 페이지가 비어있어도 허용할 것인가 여부

 이러한 Paginator으로 분할하는 오브젝트 리스트, 1페이지당 표시할 데이터의 갯수를 지정하여 인스턴스를 작성한다.

 예를 들어, Article모델에서 리스트를 모두를 획득해, 1페이지에 20건의 데이터를 표시하려고 하는 경우는 다음과 같이 작성할 수 있다.

articles = Article.objects.all()
paginator = Paginator(articles, 20)

 Django에 표준으로 준비되어 있는 페이지네이션 기능을 사용하기 위해서는 위와 같이 Paginator 오브젝트를 생성해야할 필요가 있다.

 

Paginator오브젝트의 메소드

생성된 Paginator 오브젝트에는 get_page메소드와 page()메소드 2개가 존재한다.

get_page(number)

Django2.0에서 부터 추가된 메소이다. Paginator클래스에 다음과 같이 정의되어 있다.

def get_page(self, number):
    try:
        number = self.validate_number(number)
    except PageNotAnInteger:
         number = 1
    except EmptyPage:
        number = self.num_pages
    return self.page(number)

 인수 number에는 열고 싶은 페이지 번호를 지정한다. 혹시 무효한 페이지 번호나 숫자가 아닌 문자가 지정된 경우에도 예외 처리로 맨 처음 페이지 혹은 마지막 페이지를 반환한다.

 Pagenator 오브젝트에는 get_page 메소드를 사용하는 것보다. Page 오브젝트를 생한다.

page(number)

page() 메소드는 Page 오브젝트를 생성하기 위해 사용된다. 혹시 지정된 페이지가 존재하지 않는 페이지인 경우 InvalidPage 에러가 발생한다. 

 

Pageinator 오브젝트의 속성

count

Paginator 오브젝트의 conunt 속성을 사용하면 모든 페이지의 오브젝트 갯수를 획득 할 수 있다.

Paginator.count

num_pages

Paginator 오브젝트의 num_pages 속성을 사용하면 페이지의 총 갯수를 획득할 수 있다.

Paginator.num_pages

page_range

Paginator 오브젝트의 page_range 속성을 사용하면 페이지의 iterator을 획득할 수 있다.

Paginator.page_range

 

 

페이지 번호는 URL에서 얻어오기


get_page()메소드와 page()메소드의 인수에는 페이지 번호를 지정한다. 이 페이지 번호는, 리퀘스트된 페이지의 리스트를 표시할 경우, URL에서 획득한다.

 GET송신된 office54.net/article/?page=4 와 같은 URL에서 페이지 번호를 다음의 방법으로 얻을 수 있다.

page = request.GET.get('page')

 획득된 페이지 번호를 get_page() 혹은 page()의 인수에 지정하여, 리퀘스트된 페이지의 리스트를 획득한다.

pages = paginator.page(page)

리퀘스트된 페이지의 리스트가 저장된 pages는 Page 오브젝트라고 불린다. Page 오브젝트의 메소드는 후에 설명하도록 하겠다.

 여기까지가 view.py의 Paginator 클래스의 기본적인 사용 방법이다.

 

 

Page오브젝트


 모두 위에서 설명했지만, Page 오브젝트에는 Paginator.page()로 생성된다. Page 오브젝트는 템플릿(html파일)내에서 사용한다. 

 Page 오브젝트를 사용하여 현재의 페이지를 기점으로 다음 페이지의 유무, 이전 페이지의 유무, 다른 페이지의 유무 등을 얻을 수 있다.

  page 오브젝트를 아무런 가공없이 템플릿에 그대로 넣으면 모든 페이지 수와 현재 페이지를 표시한다 (예: Page 2 of 4)

<p>{{ pages }}</p>

 

page 오브젝트의 메소드

 아래의 표는 메소드 및 속성의 목록을 나타낸 것이다.

메소드 설명
has_next() 다음 페이지가 있는 경우 : True
has_previous()
이전 페이지가 있는 경우 : True
has_other_pages()
전후 어느쪽의 페이지가 존재하는 경우 : True
next_page_number()
다음 페이지의 페이지 번호를 반환(다음 페이지가 존재하지 않아도)
previous_page_number()
이전 페이지의 페이지 번호를 반환(이전 페이지가 존재하지 않아도)
start_index() 페이지의 맨 앞 번호(1)를 반환
end_index() 페이지의 맨 마지막 페이지를 반환
속성 설명
object_list Page 오브젝트의 리스트를 반환
paginator Page 오브젝트와 관련된 Paginator 오브젝트를 반환
number 현재 페이지 번호를 반환

 

 

예외 처리


 리퀘스트 된 페이지 번호가 유효하지 않은 경우에는 PageNotAnInteger와 EmptyPage를 사용한다.

PageNotAnInteger는 정수가 아닌 값이 리퀘스트된 경우 호출된다. EmptyPage는 유효한 값이 리퀘스트됐지만, 그 페이지가 존재하지 않는 경우에 호출된다.

 

 

페이지네이션(페이징) 샘플 프로그램(1)


 그럼 실제로 Web 어플리케이션을 작성해보자.  프로젝트명은 myproject, 어플리케이션 명은 blog이다.  myproject의 urls.py, blog어플의urls.py, models.py, views.py, index.html는 다음과 같이 되어 있다.

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
]
# blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
    path('', views.index, name='index'),
]
# blog/models.py
from django.db import models
from django.utils import timezone

class Post(models.Model):
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.title
# blog/views.py
from django.shortcuts import render
from .models import Post

def index(request):
    return render(request, 'index.html', {})
# blog/template/index.html
<html>
<head>
</head>
<body style="margin:20px;">
  <h1>페이지네이션 확인 페이지</h1>
</body>
</html>

 

views.py의 변경

그러면 views.py를 편집하여 템플릿에 Page 오브젝트가 전달되도록 하자.

# blog/views.py
from django.shortcuts import render
from .models import Post
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def index(request):
    Posts = Post.objects.order_by('created_date').reverse()
    paginator = Paginator(Posts, 3)
    page = request.GET.get('page', 1)
    try:
    	pages = paginator.page(page)
    except PageNotAnInteger:
    	pages = paginator.page(1)
    except EmptyPage:
    	pages = paginator.page(1)
    context = {'pages': pages}
    return render(request, 'index.html', context)

3행에서는 django.core.paginator모듈에서 Paginator, EmptyPage, PageNotAnInteger를 임포트하고 있다. 6번째 행에선 Post 모듈에서 오브젝트 전체를 획득해, created_date를 순서로 정렬하고 있다.

 그리고 7번째 행의 paginator = Paginator(Posts, 3)에서는 획득한 오브젝트의 리스트를 1페이당 3건씩 표시하고자 하고 있다.

 8번째 행의 request.GET.get('page', 1)에서는 현재 페이지 번호를 URL에서 획득하고 있다.

 10번째 행의 pages = paginator.page(page)에서는 리퀘스트된 페이지 번호의 리스트를 획득해, pages 변수에 저장하고 있다.

 다음은 화면에 표시될 템플릿 index.html을 편집해가자.

 

표시할 html파일의 설정

 템플릿의 index.html를 편집해 페이지 네이션이 표시되도록 하고 있다. BOOTSTRAP4를 사용하였기에 그 스타일에 준하게 작성됐다.

<html>
<head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="… " crossorigin="anonymous">
</head>
<body style="margin:20px;">
  <h1>페이지네이션 확인 페이지</h1>
  {% for page in pages %}
  <p>{{ page.title }} : {{ page.text }}</p>
  {% endfor %}
  {% if pages.has_other_pages %}
    <nav aria-label="Page navigation example">
        <ul class="pagination">
            {% if pages.has_previous %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.previous_page_number }}"><<</a></li>
            {% else %}
                <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#"><<</div></li>
            {% endif %}

            {% for page_num in pages.paginator.page_range %}
                {% if page_num %}
                    {% if page_num == pages.number %}
                        <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#">{{ page_num }}</div></li>
                    {% else %}
                        <li><a class="page-link text-primary d-inline-block" href="?page={{ page_num }}">{{ page_num }}</a></li>
                    {% endif %}
                {% else %}
                    <li class="disabled"><a class="page-link text-secondary d-inline-block text-muted" href="#">・・・</a></li>
                {% endif %}
            {% endfor %}

            {% if pages.has_next %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.next_page_number }}">>></a></li>
            {% else %}
                <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#">>></div></li>
            {% endif %}
        </ul>
    </nav>
  {% endif %}
</body>
</html>

 모든 코드가 적용됐다면 다음과 같이 페이지네이션이 구현됐을 것이다.

 

 

페이지네이션(페이징) 샘플 프로그램(2)


 위 샘플 코드에서는 페이지 수가 늘어가는 만큼 그 페이지 번호가 나열되게 되어 페이지 수가 많은 경우 사용할 수 없다.

 그러한 경우에는 다음과 같이 페이지네이션을 표시하도록 변경할 수 있다.

 

 

그러기 위해선 템플릿에 다음과 같이 기재한다.

<html>
<head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="… " crossorigin="anonymous">
</head>
<body style="margin:20px;">
  <h1>페이지네이션 확인 페이지</h1>
  {% for page in pages %}
  <p>{{ page.title }} : {{ page.text }}</p>
  {% endfor %}
  {% if pages.has_other_pages %}
    <nav aria-label="Page navigation example">
        <ul class="pagination">
            {% if pages.has_previous %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.previous_page_number }}"><<</a></li>
            {% else %}
                <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#"><<</div></li>
            {% endif %}

            {% if pages.has_previous %}
                {% if pages.previous_page_number != 1 %}
                    <li><a class="page-link text-primary d-inline-block" href="?page=1">1</a></li>
                    <li>…</li>
                {% endif %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.previous_page_number }}">{{ pages.previous_page_number }}</a></li>
            {% endif %}
            <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#">{{ pages.number }}</div></li>
            {% if pages.has_next %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.next_page_number }}">{{ pages.next_page_number }}</a></li>
                {% if pages.next_page_number != pages.paginator.num_pages %}
                    <li>…</li>
                    <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.paginator.num_pages }}">{{ pages.paginator.num_pages }}</a></li>
                {% endif %}
            {% endif %}
            {% if pages.has_next %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.next_page_number }}">>></a></li>
            {% else %}
                <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#">>></div></li>
            {% endif %}
        </ul>
    </nav>
  {% endif %}
</body>
</html>

참고자료

https://office54.net/python/django/pagination-paginator

728x90