내장 페이지네이터#

Django Ninja 입문 가이드의 24번째 글이에요.
페이지네이션(pagination) 기능은 데이터 양이 적은 소규모 프로젝트에서도 상당히 중요해요.
페이지네이션이 없어도 API는 작동하지만, 성능에 영향을 미치게 되고 특히 데이터 양이 많을 때는 더 심각해져요.
API가 한 번에 많은 양의 데이터를 반환하면 서버에 부담이 커질 뿐만 아니라, 클라이언트 처리 속도가 느려지고 심지어 타임아웃(timeout)이나 메모리 부족과 같은 문제까지 발생할 수 있어요.
페이지네이션을 통해 한 번에 너무 많은 데이터를 전송하는 것을 피하고 API의 성능을 높이면서 동시에 사용자의 경험도 개선할 수 있답니다.
이 주제는 상, 하 두 편으로 나누어 설명할 텐데, 다양한 요구사항에 맞게 Django Ninja에서 페이지네이션 기능을 구현하는 방법(내장 페이지네이션 클래스부터 사용자 정의 페이지네이션 클래스까지)을 다룰 거예요.
이번 글의 모든 코드 변경 사항은 이 PR을 참고해 주세요.
GitHub 예제 프로젝트#
페이지네이션의 중요성#
페이지네이션의 핵심 역할은 대량의 데이터를 작게 나누어 전송하고, 매번 일부만 전달하여 성능 문제를 피하는 것이에요.
구체적으로, 페이지네이션은 다음과 같은 이점을 제공해요:
- 서버 부담 감소: 모든 데이터를 한 번에 반환할 필요가 없으며, 단일 페이지의 데이터만 처리하면 돼요.
- 네트워크 전송 속도 향상: 너무 많은 데이터는 네트워크 지연 및 데이터 손실의 위험을 높이지만, 페이지네이션을 사용하면 전송량을 효과적으로 줄일 수 있어요.
- 사용자 경험 향상: 클라이언트는 초기 데이터를 빠르게 받아 화면에 표시할 수 있어, 전체 데이터가 올 때까지 기다릴 필요가 없어요. 또한 클라이언트 측에서 방대한 데이터를 처리하는 부담도 줄일 수 있어요.
따라서 프로젝트 규모에 상관없이 효율적인 페이지네이션 전략을 구현하는 것은 API 확장성과 사용자 경험 모두에 명확한 도움이 된답니다.
페이지네이션의 중요성을 알았으니, 이제 실제로 구현해 볼까요!
이번 예제 API는 바로 "게시글 목록 조회"예요.
프로젝트 데이터베이스에는 이미 60개 이상의 게시글 데이터가 들어 있어서 이 예제에 아주 적합해요.
무엇이 없다고요? 아직 준비하지 못하셨다면 12편: 요청(4) Request Body와 Schema 소개 글 마지막의 "잠시 휴식 및 준비" 섹션을 참고해 주세요.
이번 글에서는 Django Ninja에 내장된 PageNumberPagination 페이지네이터를 사용하여 간단하고 효과적인 페이지네이션 기능을 구현해 볼게요. 커스텀 부분은 다음 글로 넘길게요.
Django Ninja 내장 페이지네이터#
Django Ninja에서 페이지네이션 기능은 내장된 paginate 데코레이터와 페이지네이터(즉, 페이지네이션 클래스)를 함께 사용하여 구현할 수 있어요.
Django Ninja는 두 가지 내장 페이지네이터를 제공하는데, 여러분의 이해를 돕기 위해 쉬운 말로 소개할게요:
LimitOffsetPagination: "어느 데이터부터 시작할지"와 "몇 개의 데이터를 가져올지"에 따라 페이징을 처리해요. 데이터 양이 많을 때 유용하죠. 예: "20번째 데이터부터 10개를 가져와".PageNumberPagination: 페이지 번호를 통해 페이징을 처리해요. 사용자는 그저 원하는 페이지 번호만 지정하면 돼요. 예: "2페이지 데이터 가져와". 매 페이지의 데이터 수는 개발자가 직접 설정할 수 있어요.
저는 개인적으로 PageNumberPagination이나 그것과 유사하게 커스텀한 버전(다음 글에서 배울 내용)을 선호해요.
하지만 기본 페이지네이터는 LimitOffsetPagination이기 때문에 paginate 데코레이터를 사용할 때 첫 번째 매개변수를 직접 명시해 주어야 해요. 이따가 보시게 될 거예요.
그전에 "게시글 목록 조회" API의 현재 상황을 살펴볼게요.
API 현재 상황#
보시다시피 페이지네이션 기능이 아직 없기 때문에, 이 API는 모든 게시글 데이터를 한 번에 반환해요:
@router.get('/posts/', response=list[PostListResponse], ...)
def get_posts(
request: HttpRequest,
title: None | str = Query(None, min_length=2, max_length=10),
) -> QuerySet[Post]:
"""
게시글 목록 조회
"""
posts = Post.objects.all()
if title:
posts = posts.filter(
title__icontains=title).select_related('author')
return posts
게시글 수가 적을 때는 문제없이 동작하겠지만 데이터량이 늘어날수록 성능에 영향을 미치게 될 거예요.
이제 Django Ninja의 내장 페이지네이터를 사용해 이 문제를 개선해 봅시다.
PageNumberPagination을 이용한 페이지네이션 구현#
내장 PageNumberPagination을 사용하면 API에 아주 쉽게 페이지네이션 기능을 추가할 수 있어요.
뷰 함수 위에 @paginate 데코레이터를 붙이고 매개변수를 넣기만 하면 돼요:
from ninja.pagination import PageNumberPagination, paginate
...
@router.get(...)
@paginate(PageNumberPagination, page_size=10) # 페이지네이션 구현
def get_posts(...) -> QuerySet[Post]:
"""
게시글 목록 조회
"""
...
나머지 대부분은 생략하고 여기서는 페이지네이션 구현에만 집중해 주세요.
앞서 언급했듯이 PageNumberPagination이라는 첫 번째 매개변수는 반드시 직접 명시해 주어야 해요. LimitOffsetPagination을 쓴다면 그럴 필요 없지만요.
이렇게 구현하면 한 페이지당 10개의 게시글을 보여주게 되며, 이 숫자는 page_size 매개변수로 조정할 수 있어요.
어, 그럼 페이지 이동은 어떻게 하나요? PageNumberPagination의 소스 코드를 확인해 볼까요:
class PageNumberPagination(AsyncPaginationBase):
class Input(Schema):
page: int = Field(1, ge=1)
...
Input은 요청의 쿼리 매개변수(다음 글에서 자세히 설명할게요)를 나타내요. 다시 말해 URL의 쿼리 매개변수(query parameters)에서 page 매개변수를 사용하여 '페이지 번호'를 지정하면 페이지 이동 효과를 낼 수 있다는 뜻이에요.
페이지네이션 효과 테스트#
API를 호출할 때 ?page=2를 쿼리 매개변수로 주고 결과를 살펴봅시다:

예상대로 잘 작동해요!
응답에 표시된 것은 정확히 2페이지 내용이며, 게시글 id가 11번부터 시작해서 총 10개의 데이터가 나타나요.
내장 페이지네이터의 장점과 한계#
장점#
- 아주 간단하게 데코레이터 + 내장 페이지네이터만으로 페이징을 구현할 수 있고 매 페이지 개수를 조절할 수 있어 무척 편리하고 실용적이에요.
- 복잡한 커스터마이징이 필요하지 않은 경우에 안성맞춤이에요.
한계#
- 유연성이 부족해요. 예를 들어 사용자가 한 페이지에 몇 개의 데이터를 표시할지 지정할 수 없어요.
- 반환되는 응답의 필드와 형식이 고정되어 있고 다소 간략해요.
요약#
Django Ninja의 내장 페이지네이터를 활용하면 API에 신속하게 페이지네이션 기능을 추가하여 단순한 요구사항을 당장 해결할 수 있어요.
하지만 내장 페이지네이터는 제어 유연성이 다소 떨어지고 커스텀 응답을 만들 수 없어요. 페이지네이션 요구사항이 복잡해질 경우에는 다소 부족하게 느껴질 수 있죠.
이럴 때 사용자 정의(커스텀) 페이지네이터가 더 나은 해결책이 될 수 있어요.
다음 글에서는 Django Ninja에서 사용자 정의 페이지네이션 클래스를 만들어 이러한 고급 요구사항을 충족하는 방법에 대해 살펴볼게요.