무효 클릭 IP 추적 중...
파이썬/django(장고)

[django 기초] 페이징 처리(pagination) 마스터 하기

꼬예 2022. 2. 16.

이번 포스팅에서는 페이징 처리에 대해 알아보겠습니다. 

아주 간단한 개념부터 하나씩 쌓아가는식으로 진행할것이기 때문에 처음부터 하나씩 이해해가며 읽어 주시면 좋겠습니다. 

 

특히 이번 포스팅에서는 페이징에 대해서만 다룰것이기에 이외에 css나 다른 구현들은 극단적으로 단순화할 것입니다.

 

paging 처리전 기본 세팅

간단하게 model, view, html을 간단히 구성해주시고,

# models.py

from django.db import models
from django.conf import settings

# Create your models here.

class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=250)
    content = models.TextField()
# views.py

from django.shortcuts import render
from .models import Post


def post_list(request):
    post_list = Post.objects.all()

    return render(request, 'post/post_list.html', {'post_list':post_list})
# post/templates/post_list.html

{% for post in post_list %}
    <li>{{ post.title }}</li>
{% endfor %}

 

admin page을 통해 12개의 포스팅을 만들어 보았습니다. 

 

 

출력화면에는 아래와 같은 모습이 보여집니다. 

지금은 12개 포스팅이기 때문에 크게 무리가 없지만, 이 포스팅이 100개 , 1000개가 된다면 페이징 처리는 필수겠죠? 

 

url을 통해 page인자 전달하기

당장 제 블로그 리스트에 있는 2페이지를 클릭하면

 

 url에 ?page=2 가붙고

 

4페이지를 클릭하면

?page=4가 붙는 걸 알 수 있습니다. 

 

이러한 규칙을 우리 장고에게도 넣어줘야 하는데요. 이때 사용하는것이 request.GET입니다. 

 

아래와 같이 request.GET으로 들어온 값 중 'page'라는 명으로 들어온 값을 가져오겠다라는 코드를 작성하고, 실제로 어떤 내용이 담겨 있는지 확인해보겠습니다.

 

url에 ?page=3을 넣어서 접속을 하면,

터미널 상에 3이 출력 되는 걸 알 수 있습니다. 

 

paginator 객체 불러기오기

from django.shortcuts import render
from .models import Post
from django.core.paginator import Paginator



def post_list(request):
    post_list = Post.objects.all()
    page = request.GET.get('page')

    paginator = Paginator(post_list, 2)
    page_obj = paginator.page(page)

    return render(request, 'post/post_list.html', {'post_list':post_list, 'page_obj':page_obj})

 

우선 Paginator 클래스를 import합니다. 

Paginator(첫번째인자, 두번째인자) -> 첫번째 인자에는 queryset, 두번째 인자에는 우리가 한페이지에 몇개의 포스트를 볼지를 정하는겁니다. 

 

우리 예시에서는 전체 포스팅이 12개인데 2개씩 본다고 했으니 총 6페이지가 나오겠다는걸 예상할 수 있습니다.

그렇게만든 paginator 객체를 .page라는 메소드를 통해 page_obj를 만듭니다. 

 

우리가 앞서 url을 통해 전달한 현재 페이지가 몇페이지인지를 나타내는 값을 .page()안에 넣어주면, 그 페이지에 속해있는 포스팅을 내부적으로 저장하고 있는것이죠.

 

이렇게 나온 page_obj을 template에서 전달하기 위해 딕셔너리에 전달합니다.

# post/templates/post_list.html

{% for post in page_obj %}
    <li>{{ post.title }}</li>
{% endfor %}

tempate 단에서는 기존에 사용했던 post_list가 아닌 page_obj로 변경합니다.

 

왜냐하면 post_list는페이지 상관없이 전체 포스팅이 담겨 있는 객체이고,

page_obj 페이지마다 할당된 포스팅이 담겨있는 객체이기 때문입니다.

 

실제로 page 3에 접속해보면,

해당 page_obj에 할당된 두개의 포스팅만 출력되는걸 알 수 있습니다. 

 

 

page를 입력하지 않을시 발생되는 문제 해결

하지만 위와 같이 코드를 적으면 문제가 하나있습니다.

페이지를 입력하지 않고, 그냥 접속하면 

아래와 같이 PageNotAnInterget at 이라는 에러가 발생합니다. 

paginator.page(page) 메소드에서 page에 숫자가 들어오길 기다리는데 아무값도 들어오지 않으니 발생하는 에러입니다.

 

이를 해결하기위해  아무것도 안들어올경우는 그냥 1페이지로 인식하도록 코드를 수정해보겠습니다. 

 

from django.shortcuts import render
from .models import Post
from django.core.paginator import Paginator, PageNotAnInteger



def post_list(request):
    post_list = Post.objects.all()
    page = request.GET.get('page')

    paginator = Paginator(post_list, 2)
    
    try:
        page_obj = paginator.page(page)
    except PageNotAnInteger:
        page = 1
        page_obj = paginator.page(page)

    return render(request, 'post/post_list.html', {'post_list':post_list, 'page_obj':page_obj})

PageNotAnInteger를 import 하고 try, except 구문을 통해 에러가 발생시

1페이지를 보겠다는 코드를 삽입합니다. 

 

 

 

 

page를 너무 크게 입력해서 발생하는 경우 

만약 실제로 우리에게 존재하지 않을 정도의 큰 page를 입력할 경우 발생하는 에러입니다.

(물론 url을 통해 page에 접속하는 사람은 거의 없겠지만, 만에하나를 대비합시다!)

 

해당하는 오류는 우리가 가지고있는 가장 마지막 페이지로 자동 접속하도록 코드를 수정해보겠습니다.

from django.shortcuts import render
from .models import Post
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage



def post_list(request):
    post_list = Post.objects.all()
    page = request.GET.get('page')

    paginator = Paginator(post_list, 2)
    
    try:
        page_obj = paginator.page(page)
    except PageNotAnInteger:
        page = 1
        page_obj = paginator.page(page)
    except EmptyPage:
        page = paginator.num_pages
        page_obj = paginator.page(page)

    return render(request, 'post/post_list.html', {'post_list':post_list, 'page_obj':page_obj})

EmptyPage 에러를 처리할 클래스를 import 합니다. 

그 후 해당 에러가 발생할시 paginator.num_pages 를 통해 가장 마지막page를 추출하여,

page_obj를 갱신합니다.

 

 

 

 

실제 pagination 버튼 구현 하기

 

사실 지금까지 뭔가 이상함을 느끼셨겠지만, 실제로 url를 통해 각 page를 접속하는 사람이 없습니다. 

모두가 버튼을 눌러서 각 페이지에 접속하죠.

 

이제부터 어떻게 page버튼을 구현하는지 알아보도록 하겠습니다. 

#views.py 

from django.shortcuts import render
from .models import Post
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage


def post_list(request):
    post_list = Post.objects.all()
    page = request.GET.get('page')

    paginator = Paginator(post_list, 2)
    
    try:
        page_obj = paginator.page(page)
    except PageNotAnInteger:
        page = 1
        page_obj = paginator.page(page)
    except EmptyPage:
        page = paginator.num_pages
        page_obj = paginator.page(page)

    return render(request, 'post/post_list.html', {'post_list':post_list, 'page_obj':page_obj, 'paginator':paginator})

지금까지는 현재 페이지를 담은 page_obj만 넘겼다면, 추가적으로paginator 객체를 넘겨줍니다.

# post/templates/post_list.html

{% for post in page_obj %}
    <li>{{ post.title }}</li>
{% endfor %}

<br/>
<br/>
<div>
    <ul style="display:flex;list-style: none;"> 
    {% for page in paginator.page_range %}
        <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page}}">{{page}}</a></li>
    {% endfor %}
    </ul>
</div>

 

그 후 전달받은 paginator객체에서  paginator.page_range를 이용하여 내부적으로 저장되어있는 page를 만들어줍니다. 

 

paginator.page_range는 우리 홈페이지에 페이지가 몇개 있는지를 range 형태로 담고 있는 객체라고 보시면 됩니다. 

만약 총 6페이지라면 range(1,7)를 가지고 있을것이고,

다시말해 for loop를 돌리면 1, 2, 3, 4, 5, 6 이라는 숫자가 나오겠죠.

 

이러한 특징을 이용해 각각의 숫자를 버튼으로 만들어주고, url로 넘겨줄 페이지주소를 넣어줍니다.

 

 

페이지상에는 아래와 같이 간단한 형태가 구성이되고 클릭해보시면 해당 페이지로 잘 이동하는걸 알 수 있습니다. 

 

 

현재 page 강조하기

지금까지 만든 페이지는 클릭해도 변화가 없다보니 내가 어떤 페이지를 눌렀는지 구분이 안됩니다.

이 문제도 해결해보겠습니다.

# post/templates/post_list.html

{% for post in page_obj %}
    <li>{{ post.title }}</li>
{% endfor %}

<br/>
<br/>
<div>
    <ul style="display:flex;list-style: none;"> 
    {% for page in paginator.page_range %}

    {% if page == page_obj.number %}
        <li style="margin:3px;"><a style="text-decoration:none; color: red;" href="?page={{page}}">{{page}}</a></li>
    {% else %}
        <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page}}">{{page}}</a></li>
    {% endif %}
    {% endfor %}
    </ul>
</div>

여기서 추가된 코드는 {% if page == page_obj.number %}를 통한 조건절 분기라고 보시면됩니다. 

 

여기서 page_obj.number은 현재 페이지를 가르킵니다. 현재페이지가 page랑 같은 경우에만 color 를 red로 바꾸는 코드 입니다. 

 

 

실제로 4페이지 접속하면 아래와 같이 정상적으로 작동되는걸 알 수 있습니다. 

이전페이지 다음페이지 구현하기

일반적으로 pagination 에는 숫자만 있기보다 아래처럼 이전페이지 다음페이지도 있습니다.

이부분을 구현해보도록 하겠습니다. 

 

먼저 이전페이지로 가는 버튼입니다.

# post/templates/post_list.html

{% for post in page_obj %}
    <li>{{ post.title }}</li>
{% endfor %}

<br/>
<br/>
<div>
    <ul style="display:flex;list-style: none;"> 

    {% if page_obj.has_previous %}
    <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page_obj.previous_page_number}}">&#10094; Prev</a></li>
    {% endif %}

    {% for page in paginator.page_range %}
    {% if page == page_obj.number %}
        <li style="margin:3px;"><a style="text-decoration:none; color: red;" href="?page={{page}}">{{page}}</a></li>
    {% else %}
        <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page}}">{{page}}</a></li>
    {% endif %}
    {% endfor %}


    </ul>
</div>

page_obj.has_previous 를 통해서 이전 페이지가 있을경우에만 해당 버튼이 생기도록 조건문을 겁니다. 

그리고 해당 버튼 href에는 page_obj.has_previous_page_number 를 통해 이전페이지의 페이지 번보를 추출해서 넣습습니다. 

 

실제 출력 결과를 보면 

이전 페이지가 없는 1페이지 에서는 버튼이 사라지고 그외 버튼에서는 생긴걸 확인할 수 있습니다. 

마찬가지로 next 버튼도 만들어 보겠습니다.

# post/templates/post_list.html

{% for post in page_obj %}
    <li>{{ post.title }}</li>
{% endfor %}

<br/>
<br/>
<div>
    <ul style="display:flex;list-style: none;"> 

    {% if page_obj.has_previous %}
    <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page_obj.previous_page_number}}">&#10094; Prev</a></li>
    {% endif %}

    {% for page in paginator.page_range %}
    {% if page == page_obj.number %}
        <li style="margin:3px;"><a style="text-decoration:none; color: red;" href="?page={{page}}">{{page}}</a></li>
    {% else %}
        <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page}}">{{page}}</a></li>
    {% endif %}
    {% endfor %}

    {% if page_obj.has_next %}
    <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page_obj.next_page_number}}">&#10095; Next</a></li>
    {% endif %}

    </ul>
</div>

이전 페이지버튼과 원리는 같습니다. 

다음페이지가 있는지 확인하는 page_obj.has_next와 다음 페이지의 페이지번호를 알려주는 page_obj.next_page_number를 이용하면 됩니다.

 

 

1 페이지만 있을경우

우리가 한페이지에 20개에 포스팅을 보고 싶다고 변경하고 싶다고 합시다. 

그럼 paginator = Paginator(post_list,20)으로 코드를 수정하고, 결과를 보겠습니다.

#views.py 

from django.shortcuts import render
from .models import Post
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage



def post_list(request):
    post_list = Post.objects.all()
    page = request.GET.get('page')

    paginator = Paginator(post_list, 20)
    
    try:
        page_obj = paginator.page(page)
    except PageNotAnInteger:
        page = 1
        page_obj = paginator.page(page)
    except EmptyPage:
        page = paginator.num_pages
        page_obj = paginator.page(page)

    return render(request, 'post/post_list.html', {'post_list':post_list, 'page_obj':page_obj, 'paginator':paginator})

여전히 pagination이 존재합니다. 

 

우리는 1페이지만 있을때는 pagination 기능을 없애는 코드를 template 단에 추가해보도록 하겠습니다. 

 

# post/templates/post_list.html

{% for post in page_obj %}
    <li>{{ post.title }}</li>
{% endfor %}

<br/>
<br/>
<div>
    {% if page_obj.has_other_pages %}
        <ul style="display:flex;list-style: none;"> 

        {% if page_obj.has_previous %}
        <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page_obj.previous_page_number}}">&#10094; Prev</a></li>
        {% endif %}

        {% for page in paginator.page_range %}
        {% if page == page_obj.number %}
            <li style="margin:3px;"><a style="text-decoration:none; color: red;" href="?page={{page}}">{{page}}</a></li>
        {% else %}
            <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page}}">{{page}}</a></li>
        {% endif %}
        {% endfor %}

        {% if page_obj.has_next %}
        <li style="margin:3px;"><a style="text-decoration:none; color: blue;" href="?page={{page_obj.next_page_number}}">&#10095; Next</a></li>
        {% endif %}

        </ul>
    {% endif%}
</div>

page_obj.has_other_pages 를 통해 현재 페이지외에 다른 페이지가 있다면 pagination을띄우고 아니면 안띄우는 조건절을 만들어 해당 문제를 해결합니다.

 

 

포스팅갯수가 수없이 많을때 발생되는문제

지금까지 우리가 작성한 코드는 포스팅이 수업이 많아지면 그만큼 pagination 버튼도 아래와 같이 많아집니다.  

이 문제를 해결하기 위해서 기존에 paginator.page_range 대신 custom으로 range를 만들어 줘야 합니다.

 

다양한 방법이 있겠지만 저는 현재 페이지를 기준으로 앞으로 2개 뒤로 2개 총 5개가 기본적으로 보이는 pagination 을 만들고 싶습니다. 

 

#views.py 

from django.shortcuts import render
from .models import Post
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage



def post_list(request):
    post_list = Post.objects.all()
    page = request.GET.get('page')

    paginator = Paginator(post_list, 2)
    
    try:
        page_obj = paginator.page(page)
    except PageNotAnInteger:
        page = 1
        page_obj = paginator.page(page)
    except EmptyPage:
        page = paginator.num_pages
        page_obj = paginator.page(page)

    leftIndex = (int(page) - 2)
    if leftIndex < 1:
        leftIndex = 1
    
    rightIndex = (int(page) + 2)

    if rightIndex > paginator.num_pages:
        rightIndex = paginator.num_pages 

    custom_range = range(leftIndex, rightIndex+1)
    return render(request, 'post/post_list.html', {'post_list':post_list, 'page_obj':page_obj, 'paginator':paginator, 
                'custom_range':custom_range})

우선 custom_range 라는 이름으로 range(leftIndex, rightIndex+1) 객체를 만들어 줍니다. 

range는 마지막 숫자를 포함하지 않기 때문에 마지막 숫자까지넣어주기 +1를 해주었습니다.

 

그리고 각 index값을 지정해주는데, 

우리는 앞서 현재 페이지를 기준으로 앞으로 2개 뒤로 2개를 넣고 싶기 때문에 leftindex-2

rightindex+2 를 해줍니다. 

 

이때 주의할점은 만약 현재페이지가 1일경우 -2를 하면 마이너스가 되어버리니 최소값은 1로 설정을 해주고, 같은 원리로 rightIndex에 +2를 했는데 우리가 가진 페이지보다 많은 경우가 생기면 안되니까 최댓값을 우리가 가진 마지막 페이지로 지정해주는 코드를 조건문을 통해 추가 해줍니다.

 

완료후 실행을 하면 우리가 원하는 모습이 구현되는걸 알 수 있습니다.

 

필요한 기능을 하나씩 붙히다보니 view 꽤나 복잡해졌네요.

실무에서는 해당 페이징 부분은 다른파일로 따로 떼어 모듈 형식으로 사용하시길 추천드립니다.

 

 

추가적인 정보는 django 공식문서 pagitation을 참고하기 바랍니다.

https://docs.djangoproject.com/en/4.0/topics/pagination/

  • 트위터 공유하기
  • 페이스북 공유하기
  • 카카오톡 공유하기
이 컨텐츠가 마음에 드셨다면 커피 한잔(후원) ☕

댓글