이종관

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

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

2025년 1월 12일4 min read
문서 목차

Django 기본 Paginator 이해

대량의 데이터를 한 번에 렌더링하면 응답 시간이 길어지고 메모리 사용량이 증가한다. Pagination은 데이터를 일정 단위로 나누어 요청할 때마다 한 페이지 분량만 반환하는 기법이다.

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

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