이종관
글 목록으로

Django 커스텀 페이지네이션 구현하기

Django 기본 Paginator의 한계와 커서 기반 커스텀 페이지네이션 구현

2025년 1월 12일·4 min read·
backend
django
pagination
drf

Django 기본 Paginator 이해

Django에서 가장 간단하게 페이지네이션을 적용하려면 Paginator 클래스를 사용한다.

python
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
from .models import Post
 
def post_list_view(request):
    post_qs = Post.objects.all().order_by('-created_at')
 
    # Paginator 객체 생성 (페이지당 10개)
    paginator = Paginator(post_qs, 10)
 
    page = request.GET.get('page', 1)
 
    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        posts = paginator.page(1)
    except EmptyPage:
        posts = paginator.page(paginator.num_pages)
 
    return render(request, 'blog/post_list.html', {'posts': posts})

핵심 개념

  • Paginator(queryset, per_page): queryset과 페이지당 개수를 인자로 받음
  • paginator.page(number): 해당 페이지의 Page 객체 반환
  • 예외 처리: PageNotAnInteger, EmptyPage 예외를 적절히 처리

템플릿 예시

html
{% for post in posts %}
  <h2>{{ post.title }}</h2>
  <p>{{ post.content }}</p>
{% endfor %}
 
<div class="pagination">
  {% if posts.has_previous %}
    <a href="?page={{ posts.previous_page_number }}">이전</a>
  {% endif %}
 
  <span>{{ posts.number }} / {{ posts.paginator.num_pages }}</span>
 
  {% if posts.has_next %}
    <a href="?page={{ posts.next_page_number }}">다음</a>
  {% endif %}
</div>

커스텀 Paginator가 필요한 상황

기본 Paginator로 대부분 처리할 수 있지만, 다음 경우에는 커스텀이 필요하다:

  1. 페이지 번호 대신 다른 식별자 사용 (해시값, 날짜, 슬러그 등)
  2. Infinite Scroll 구현 시 동적 데이터 로딩
  3. paginate_by가 동적으로 변경되어야 할 때
  4. 복잡한 필터/검색/정렬 조건과 함께 사용

Paginator 상속으로 커스텀하기

created_at 기준 페이지네이션

python
from django.core.paginator import Paginator
 
class CreatedAtPaginator(Paginator):
    """created_at 기준 커스텀 Paginator"""
 
    def __init__(self, object_list, per_page, **kwargs):
        super().__init__(object_list, per_page, **kwargs)
 
    def page_by_timestamp(self, timestamp):
        """timestamp 이후 데이터를 per_page만큼 반환"""
        if timestamp:
            filtered_qs = self.object_list.filter(created_at__lt=timestamp)
        else:
            filtered_qs = self.object_list
 
        return filtered_qs.order_by('-created_at')[:self.per_page]

뷰에서 사용

python
def post_list_by_timestamp(request):
    timestamp = request.GET.get('timestamp', None)
 
    post_qs = Post.objects.all()
    paginator = CreatedAtPaginator(post_qs, 10)
    posts = paginator.page_by_timestamp(timestamp)
 
    next_timestamp = posts.last().created_at.isoformat() if posts.exists() else None
 
    return render(request, 'blog/post_list.html', {
        'posts': posts,
        'next_timestamp': next_timestamp,
    })

Django Rest Framework 페이지네이션

DRF에서는 클래스 기반 Pagination을 제공한다:

  • PageNumberPagination: 페이지 번호 기반
  • LimitOffsetPagination: limit, offset 파라미터 사용
  • CursorPagination: 커서 기반 (보안성 높음)

커스텀 PageNumberPagination

python
from rest_framework.pagination import PageNumberPagination
 
class CustomPageNumberPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'  # ?page_size=20 가능
    max_page_size = 100

설정 방법

python
# settings.py - 전역 설정
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'myapp.paginations.CustomPageNumberPagination',
    'PAGE_SIZE': 10,
}
 
# 또는 개별 ViewSet에서
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    pagination_class = CustomPageNumberPagination

정리

방식사용 사례
기본 Paginator단순 페이지 번호 기반
커스텀 Paginator시간/슬러그 기반, 특수 로직
DRF PaginationAPI 응답, 동적 page_size

프로젝트 요구사항에 따라 적절한 방식을 선택하면 된다.