1. Django Tutorial(Airbnb) - Pagination을 만드는 3가지 방법

ID짱재·2021년 8월 5일
1

Django

목록 보기
16/43
post-thumbnail

🌈 Pagination을 만드는 3가지 방법

🔥 FBV : 수동으로 pagination 만들기

🔥 FBV : Django의 도움으로 pagination 만들기

🔥 CBV : Django의 Listview로 pagination 만들기


1. 수동으로 pagination 만들기

1) request.GET

  • urls에서 "/?key=values"의 값은 request.GET을 통해 Dict 형태로 가져올 수 있어요.
  • "http://127.0.0.1:8000/" url로 서버에 요청하면, 콘솔에 "<QueryDict: {}>"이 출력되는데요, 이는 url 뒤에 파라미터가 붙어있지 않기 때문이에요.
  • "http://127.0.0.1:8000/?page=1" 로 접근하면, <QueryDict: {'page': ['1']}> 딕셔너리 형태로 나타나는 것을 볼 수 있어요.
  • dir()을 통해 GET을 살펴보면 아래와 같이 다양한 매서드를 볼 수 있는데요,, key()와 values()를 사용하면 key값와 value값도 받아볼 수 있답니다.
from django.shortcuts import render
from . import models
def all_rooms(request):
    print(request.GET)
    print(request.GET.get('page'))    
    # print(dir(request.GET))
    all_rooms = models.Room.objects.all()[:10]
    context = {"rooms": all_rooms}
    return render(request, "rooms/home.html", context)
  • "http://127.0.0.1:8000/?page=1"에서 get을 통해 현재 요청된 url의 page값만 가져올 수도 있는데요, 첫번째 인수는 'key'값을 받고, 두번째 인수는 'dafault'값을 지정해줄 수 있어요. 'dafault'값을 지정하는 이유는 "http://127.0.0.1:8000/"와 같이 메인 페이지로 접근할 때는 "http://127.0.0.1:8000/?page=1" 처럼 작동하게 하기 위함이에요!
    • 🔎 request.GET.get('key명', 'default값')
  • "http://127.0.0.1:8000/?page=" 이런 접근은 page라는 key는 있지만 values만 없는 경우인데요,, 이럴 경우 오류가 발생해요. 이에page값이 있으면 사용하고, 없을 때는 1을 사용하게끔 해야 오류를 예방할 수 있어요! 또한 str으로 받아오기 때문에 int로 캐스팅을 해줘야해요!
    • 🔎 page = int(page or 1) 👈 or은 맨 처음 만나는 True값을 적용하기 때문에 우선 page값이 있으면 그 값을 쓰고, 없으면 1일 쓰겠다는 의미입니다.
from math import ceil
from django.shortcuts import render
from . import models
def all_rooms(request):
    # print(request.GET.get("page")) # None
    page = request.GET.get("page", 1)
    page = int(page or 1)
    page_size = 10
    limit = page_size * page
    offset = limit - page_size
    all_rooms = models.Room.objects.all()[offset:limit]
    page_count = ceil(models.Room.objects.count() / page_size)
    context = {
        "rooms": all_rooms, # 👈 page 번호에 따른 Object
        "page": page, # 👈 현재 페이지 번호
        "page_count": page_count, # 👈 전체 페이지 갯수
    }
    return render(request, "rooms/home.html", context)

2) Navigation 추가

  • page를 클릭으로 이동할 수 있도록 Navigation을 추가해도록 하죠. 우선 페이지 갯수 만큼의 배열이 필요한데요,, 템플릿에서는 range함수를 사용할 수 없어서 View를 통해 배열을 전달해볼께요.
from math import ceil
from django.shortcuts import render
from . import models
def all_rooms(request):
    page = request.GET.get("page", 1)
    page = int(page or 1)
    page_size = 10
    limit = page_size * page
    offset = limit - page_size
    all_rooms = models.Room.objects.all()[offset:limit]
    page_count = ceil(models.Room.objects.count() / page_size)
    context = {
        "rooms": all_rooms,
        "page": page,
        "page_count": page_count,
        "page_range": range(1, page_count), # 👈 [1,2,3,...,page_count-1]
    }
    return render(request, "rooms/home.html", context)
  • 페이지 번호를 나열하기 위해 템플릿을 전달 받은 page_range를 for문을 통해 풀어주고, a태그를 통해 링크를 연결해주었어요:)
{% extends "base.html" %}
{% block page_name %}
    Home
{% endblock page_name %}
{% block content %}
    {% for room in potato  %}
        <h1>{{room.name}} / ${{room.price}}</h1>
    {% endfor %}
    <h5> Page {{page}} of {{page_count}}</h5>
    {% for page in page_range %}
        <a href="?page={{page}}">{{page}}</a>
    {% endfor %}
{% endblock content %}
  • 페이지 번호를 나열하는 방법도 있지만, 이전 페이지와 다음 페이지만 링크 나타나게 할 수도 있어요. 단 여기서 주의할 점은 맨 처음 페이지에서는 이전 페이지 버튼을 숨겨주고, 맨 마지막 페이지에서는 다음 페이지 버튼을 숨겨줘야해요!
  • a태그 안에 특이한 문법(|add:)이 보이는데요,,, 이것은 변수에 덧셈을해주는 템플릿 filter에요.
  • 템플릿에서 if문을 통해 현재 페이지(page)가 1보다 작아지거나, 최대 페이지 수(page_count)를 넘어서면 Prev, Next 버튼을 나타나지 않게 할 수 있어요:)
{% extends "base.html" %}
    {% block page_title %}
        Home
    {% endblock page_title %}
    {% block content %}
        {% for room in rooms %}
            <h1>{{room.name}} / ${{room.price}}</h1>
        {% endfor %}
        <h5>
            {% if page is not 1 %}
                <a href="?page={{page|add:-1}}">Prev</a>
            {% endif %}
            Page {{page}} of {{page_count}}
            {% if page is not page_count %}
                <a href="?page={{page|add:1}}">Next</a>
            {% endif %}
        </h5>
    {% endblock content %}


2. Django의 도움을 받아 pagination 만들기

1) Use Pagination & get_page()

  • Django가 제공하는 Pagination을 사용하면, 이 기능을 더 손쉽게 만들 수 있어요. Django의 Paginator는 아래 위치에 있답니다.
    • 🔎 from django.core.paginator import Paginator
  • url 정보에 있는 QueryDict 값("http://127.0.0.1:8000/?page=값")을 얻기 위해 request 정보를 가져오는 것은 필요하지만, Default는 필요없어요! Pagination이 알아서 처리해준답니다.
    • 🔎 page = request.GET.get("page")
  • 객실의 모든 정보를 가져오는 QuerySet을 생성 후, 이를 page 당 제한할 갯수와 함께 Pagination에 전달해 줄꺼에요.
    • 🔎 room_list = models.Room.objects.all()
    • 🔎 pagination = Pagination(room_list, 10) 👈 10개씩 보여줄꺼에요:)
  • 현재 페이지에 대한 url 정보(QueryDict 값)를 get_page를 통해 pagination에 전달해주어요!
    • 🔎 rooms = paginator.get_page(page)
from django.shortcuts import render
from django.core.paginator import Paginator
from . import models
def all_rooms(request):
    page = request.GET.get("page")  
    room_list = models.Room.objects.all()  
    paginator = Paginator(room_list, 10)  # 👈 모든 객실 정보를 10개씩 제한함
    rooms = paginator.get_page(page) # 👈 url에 있는 현재 page값 get_page로 전달
    # print(vars(rooms)) # 👈 'object_list', 'number' 등 
    # print(vars(rooms.paginator)) # 👈 'object_list', count, 'num_pages'
    context = {
        "page": rooms,
    }
    return render(request, "rooms/home.html", context)

2) object_list

  • 위에서 print(vars(rooms))로 출력된 내용을 살펴보면, 10개씩 제한된 객실정보가 "object_list"를 key값으로 가지는 QuerySet으로 나타나네요.
  • number가 1로 나타나는 것은 현재 페이지를 의미하는데요,, "http://127.0.0.1:8000/"로 접근했기 때문이죠. url정보를 가져올 때 Default값을 주지않았는데도 1로 설정된걸 볼 수 있어요:)
  • print(vars(rooms.paginator))에서는 전체 객실 갯수(count)와, 최대 페이지 숫자(num_pages)도 알 수 있어요.
{
'object_list': <QuerySet [<Room: 84420 Justin Village Apt. 616
Lake Ronaldtown, ME 06980>, <Room: 24793 Sanchez Land
Dominguezshire, KY 99190>, <Room: 538 Traci Mount Suite 066
Mariachester, WV 68670>, <Room: 005 Amber Junctions
Lake Kellystad, WA 17428>, <Room: 822 Chandler Fort
Lowerybury, LA 17857>, <Room: 7255 Romero Rest Suite 871
Port James, KS 99016>, <Room: 698 Gonzales Pass
Griffinbury, AZ 62361>, <Room: 4258 Shelton Fort
New Tina, CO 27801>, <Room: 7931 Perez Mall
West Tylermouth, PA 01138>, <Room: 5577 Samantha Falls Apt. 555
Byrdbury, MS 80327>]>, 
'number': 1,
'paginator': <django.core.paginator.Paginator object at 0x7f80013c03a0>
}
  • 모든 정보를 갖고 있는 "rooms"을 "page"로 템플릿에 render하였으니 이를 활용해볼텐데요, 우선 객실에 대한 객체가 "object_list"라는 key값에 QuerySet으로 담겨 있어요. for문을 이용하여 템플리셍 나타나게할 수 있어요.
  • url에서 가져온 현재 페이지 정보는 page.number 에서 가지고 있고, 최대 페이지는 page.paginator.num_pages에 담겨 있군요.
  • 이전 페이지와 다음 페이지 element를 만들 때, 현재페이지 번호(page.number)와 최대 페이지 번호(page.paginator.num_pages)를 사용해 처리해줄 수 있지만, Django의 pagination에서는 has_privious와 has_next도 제공한답니다:)
    • 🔎 {% if page.has_previous %} = {% if page.number is not 1 %}
    • 🔎 {% if page.has_next %} = {% if page.number is not rooms.paginator.num_pages %}
  • 뿐만아니라 이전 페이지 번호("previous_page_number"), 다음 페이지 번호("next_page_number")도 제공하기 때문에 간편하게 도움을 받을 수 있답니다.
    • 🔎 <a href="?page={{page.previous_page_number}}">Prev</a> = <a href="?page={{page.number|add:-1}}">Prev</a>
    • 🔎 <a href="?page={{page.next_page_number}}">Next</a> = <a href="?page={{page.number|add:1}}">Next</a>
{% extends "base.html" %}
    {% block page_title %}
        Home
    {% endblock page_title %}
    {% block content %}
        {% for room in page.object_list %}  👈 page.object_list에 QuerySet이 존재해요:)
            <h1>{{room.name}} / ${{room.price}}</h1>
        {% endfor %}
        <h5>
            {% if page.has_previous %} 👈 이전 페이지가 있는지 check해줘요:)
                <a href="?page={{page.previous_page_number}}">Prev</a>
            {% endif %}
            Page {{page.number}} of {{page.paginator.num_pages}}
            {% if page.has_next %} 👈 다음 페이지가 있는지 check해줘요:)
                <a href="?page={{page.next_page_number}}">Next</a>
            {% endif %}
        </h5>
    {% endblock content %}

3) orphans

  • print(vars(rooms.paginator))이 살펴보면, orphans가 0으로 지정된 것이 나타나는데요,, 이는 orphans를 설정하지 않았기 때문에 default 값으로 0을 갖고 있는 거에요.
  • 예를 들어 객실 object가 104개 있을 때 페이지 당 10개씩 나타내면, 총 11페이지 중에서 마지막 페이지에는 4개의 object가 존재할텐데요,, 마지막 페이지에 남은 자투리를 object를 숨기고 싶을 때 사용하는 것이 orphans에요.
  • 즉, 자투리 처리법이죠:)
  • orphans는 Paginator의 속성이기 때문에 아래 처럼 처리해줄 수 있어요.
    • 🔎 paginator = Paginator(room_list, 10, orphans=5) 👈 room_list의 QuerySet을 10개씩 제한하고, 자투리 5개까지는 페이지로 남겨주지 않아요. 6개부터는 마지막 페이지에 남겨준답니다:)
{
'object_list': <QuerySet [<Room: 84420 Justin Village Apt. 616
Lake Ronaldtown, ME 06980>, <Room: 24793 Sanchez Land
Dominguezshire, KY 99190>, <Room: 538 Traci Mount Suite 066
Mariachester, WV 68670>, <Room: 005 Amber Junctions
Lake Kellystad, WA 17428>, <Room: 822 Chandler Fort
Lowerybury, LA 17857>, <Room: 7255 Romero Rest Suite 871
Port James, KS 99016>, <Room: 698 Gonzales Pass
Griffinbury, AZ 62361>, <Room: 4258 Shelton Fort
New Tina, CO 27801>, <Room: 7931 Perez Mall
West Tylermouth, PA 01138>, <Room: 5577 Samantha Falls Apt. 555
Byrdbury, MS 80327>, <Room: 482 Walls Corner
West Maureenmouth, ND 32132>, <Room: 214 Marshall Cliffs Suite 753
North Sarah, MA 05509>, <Room: 40360 Cynthia Manor Suite 158
Port Ashleyville, OR 15798>, <Room: 24343 Cardenas Lake
Angieview, NM 37258>, <Room: 1037 Shirley Village
Jennifershire, CA 14875>, <Room: 488 Tonya Fall
Allenport, NH 71146>, <Room: 5804 Bennett Junctions Suite 741
Mariobury, AL 43417>, <Room: 26046 Melissa Divide Suite 336
Collinsmouth, HI 32032>, <Room: 8016 Melissa Throughway
West Chelsea, MA 16404>, <Room: USNS Mcintyre
FPO AE 36529>, '...(remaining elements truncated)...']>,
'per_page': 10,
'orphans': 0,
'allow_empty_first_page': True,
'count': 100,
'num_pages': 10
}

4) Use Pagination & page()

  • Paginator 생성한 클래스 객체의 정보를 가져올 때, 위에서는 get_page() 매서드를 사용했는데요,, page() 메서드도 존재합니다.
  • get_page()는 url에서 가져온 page값이 없을 땐, Default를 주지않아도 자동으로 1페이지를 가져오고 request의 page값이 전체 페이지 값보다 크다면 마지막 페지이를 반환해 주기 때문에 편리하죠.
  • 하지만, url이 매우 지저분하다는 단점도 있어요. 예를들어 10페이지 밖에 없는 상황에서 엄청 큰 페이지 번호를 요청해 했을 때, 오류가나지 않지만 url이 엉망이죠.
  • page() 매서드는 Default값을 주거나 'allow_empty_first_page'를 False로 설정할 수도 있죠. 또한 존재하지 않는 page 값이 요청되면 자동으로 처리해주는게 아니라 "EmptyPage" 에러를 발생시켜 줍니다. 이때 try~except 구문으로 예외처리를 해주면, Paginator를 보다 잘 컨트롤 할 수 있겟죠:)
  • 참고로,, page() 매서드는 int형으로 url의 파라미터 값을 전달해줘야 합니다.
from django.shortcuts import render, redirect # 👈 redierct 위치
from django.core.paginator import Paginator, EmptyPage # 👈 Emptypage 위치
from . import models
def all_rooms(request):
    page = request.GET.get("page", 1) # 👈 page값이 없을 때를 대비해서 Defualt값이 필요해요.
    room_list = models.Room.objects.all()
    paginator = Paginator(room_list, 10, orphans=5)
    try:
        rooms = paginator.page(int(page))  # 👈 int형으로 request값을 전달해야해요:)
        context = {
            "page": rooms,
        }
        return render(request, "rooms/home.html", context)
    except EmptyPage: # EmptyPage 대신 Exception을 사용하면 모든 에러를 다룹니다.
        return redirect("/") # 👈 EmptyPage에러가 발생하면 "http://127.0.0.1:8000/"로 이동

3. Django의 Listview로 pagination 만들기

1) as_view()

  • urls.py에서 url 경로와 view.py의 함수를 매핑해줄 때, 함수만 가능하기 때문에 Class를 view로 이용하는 경우에는 as_view()를 붙여줘야해요. 그래야 Django가 Class를 View로써 인식할 수 있답니다.
    • 🔎 path([url경로], [파일 경로].[Class명].as_view())
  • "http://127.0.0.1:8000/" 로 접근하면, all_rooms() 함수가 작동되게끔 매핑되었던 것을 이제 HomeView라는 CBV가 호출될 수 있도록 as_view()를 붙여줍니다.
from django.urls import path
from rooms import views as room_views
urlpatterns = [
    # path("", room_views.all_rooms) # 👈 FBV 사용 시,
    path("", room_views.HomeView.as_view()) # 👈 CBV 사용 시, as_view()가 필요해요:)
]

2) ListView

  • 상세 보기를 CBV로 만들 때, Django에서 제공하는 ListView를 상속받아 사용하면 편리합니다.
from django.views.generic import ListView
class HomeView(ListView):
    """HomeView Definition"""
    pass

3) "model = "

  • "http://127.0.0.1:8000/" 접근하니, QuerySet을 찾을 수 없다는 에러가 발생합니다. 어떤 model을 이용할지 지정해 줄께요.
    • 🔎 model = models.[테이블명]
from django.views.generic import ListView
from . import models
class HomeView(ListView):
    """HomeView Definition"""
    model = models.Room
  • QuerySet으로 사용할 Model을 지정하고 다시 url을 접근해보니, rooms 디렉토리에 room_list.html 파일을 찾을 수 없다는 에러가 발생합니다. 분명 request.Get.get(), return render() 등을 해준 적이 없는데도 불구하구요!
class HomeView(ListView):
    """HomeView Definition"""
    model = models.Room
  • 위에서 만든 home.html을 room_list.html으로 이름을 수정해 볼텐데요,, 아래와 같이 페이지가 render되는 것을 볼 수 있습니다.

4) object_list

  • 공식문서를 살펴보면, object_list를 사용하라 되어있는데요,, context로 QuerySet을 전달하지 않았는데 불구하고 모든 Object가 화면에 출력되는걸 볼 수 있습니다.
  • 즉, Model을 지정하고 템플릿에서 object_list를 사용하면, Django가 알아서 해당 모델의 Object를 리스트화하기 때문이에요!
{% extends "base.html" %}
    {% block page_title %}
        Home
    {% endblock page_title %}
    {% block content %}
        {% for room in object_list %}
            <h1>{{room.name}} / ${{room.price}}</h1>
        {% endfor %}
    {% endblock content %}

5) page_obj

from django.views.generic import ListView
from . import models
class HomeView(ListView):
    """HomeView Definition"""
    model = models.Room
    paginate_by = 10  # 👈 한 페이지에 제한할 Object 수
    paginate_orphans = 5  # 👈 짜투리 처리
    ordering = "created"  # 👈 정렬 기준
    page_kwarg = "page" # 👈 페이징할 argument
  • 모든 Object는 "object_list"에 담고 있는데요, 페이징 정보는 "page_obj"에 담겨있다고 하네요:) 다른 이름으로 템플렛 변수를 사용하고 싶다면 "context_object_name"을 지정해주면 됩니다.
  • 기존에 "page":rooms를 통해 render하여 사용했던 부분을 모두 "page_obj"로 바꿔볼께요. 모두 다 잘 작동됩니다.
{% extends "base.html" %}
    {% block page_title %}
        Home
    {% endblock page_title %}
    {% block content %}
        {% for room in object_list %}
            <h1>{{room.name}} / ${{room.price}}</h1>
        {% endfor %}
        <h5>
            {% if page_obj.has_previous %}
                <a href="?page={{page_obj.previous_page_number}}">Prev</a>
            {% endif %}
            Page {{page_obj.number}} of {{page_obj.paginator.num_pages}}
            {% if page_obj.has_next %}
                <a href="?page={{page_obj.next_page_number}}">Next</a>
            {% endif %}
        </h5>
    {% endblock content %}

6) get_context_data()

  • CBV와 FBV에 대해 여러 논쟁이 존재한다고 해요. CBV는 너무 마법처럼 간단하게 해결해준다는 장점이 있지만 그 로직이 정확히 드러나지 않거든요. 또한 CBV에 속성이나 매서드로 존재하지 않는 것들은 결국 만들어야하는 측면도 있어요. 뭐가 더 좋고 나쁘기 보다는 필요할 때 적재적소로 사용하면 좋을 것 같습니다:)
  • 위에서 Model의 모든 Object를 가져오기 위해 object_list를 사용했는데, 이 또한 원하는 이름으로 바꿀 수 있어요.
    • 🔎 context_object_name = "사용할 이름"
  • FBV에서는 변수를 context로 전달했는데요,, 필요한 기능이 CBV에 이미 만들어져있지 않는다면 FBV에서 처럼 만들어 변수로 전달하면 좋겠죠. 이는 "get_context_data()" 매서드를 통해 가능합니다.
  • 현재 시간을 화면에 출력하고자 해요,, 기존에 화면에 나타났던 Object를 모두 잃어버리면 안되겠죠? "get_context_data()"에서 기존에 CBV에서 갖고 있던 속성과 메서드를 가지고 옵니다.
    • 🔎 context = super().get_context_data(**kwargs)
  • context는 FVC에서도 Dict 형태로 전달해줬는데요, 이미 상속받은 context에 현재 시간을 추가해볼 께요.
    • 🔎 context["now"] = now
from django.utils import timezone # 👈 Django 서버시간 가져오기
from django.views.generic import ListView
from . import models
class HomeView(ListView):
    """HomeView Definition"""
    model = models.Room
    paginate_by = 10  
    paginate_orphans = 5 
    ordering = "created"  
    page_kwarg = "page"
    context_object_name = "rooms" # 👈 list_object의 이름을 변경할 수 있어요:)
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)  # 기존의 CBV에서 만들어진 것을 가져옵니다:)
        now = timezone.now()
        context["now"] = now
        return context
  • 이제 템플릿에서 템플릿 변수({{now}})를 통해 현재 시간을 원하는 위치에 나타낼 수 있어요:)
profile
Keep Going, Keep Coding!

2개의 댓글

comment-user-thumbnail
2021년 9월 23일

저만의 pagination 구현을 위해 여러 사이트, 게시글들 찾아 다니며 공부하며 고생했는데, 여기서 깨닫고 갑니다! 설명이 정말 간단하면서 디테일하네요!
정말 감사합니다!

답글 달기
comment-user-thumbnail
2024년 5월 2일

소중한 포스트 감사합니다. 도움이 되었습니다.

답글 달기