아래 글은 Django 클래스 기반 뷰(CBV) 탐구 시리즈의 마지막 편으로, ListView
를 심화 활용하여 페이징, 검색, 정렬과 같이 실무에서 자주 사용되는 기능을 구현하는 방법을 소개합니다. 이전 7편까지 CBV의 기본부터 Mixin 활용까지 살펴보았다면, 이번 편에서는 이를 종합하여 실제 웹 개발에서 마주하는 일반적인 요구사항들을 CBV로 어떻게 깔끔하게 처리할 수 있는지 알아봅니다.
전편 바로가기는 아래의 링크를 클릭하세요!
클래스 기반 뷰(CBV) 탐구 시리즈 ⑦ - Mixin 활용 및 권한 관리
“Django ListView를 확장하여 강력한 목록 기능을 구현하고,
사용자 경험을 향상시키세요!”
1. ListView, 그 이상의 가능성
ListView
는 데이터 목록을 보여주는 데 매우 유용한 Generic View입니다. 하지만 실제 웹 애플리케이션에서는 단순히 목록을 보여주는 것 외에도 많은 데이터를 효율적으로 관리하기 위한 페이징, 원하는 정보를 빠르게 찾기 위한 검색, 그리고 사용자의 선호도에 따른 정렬 기능이 필수적입니다.
이번 장에서는 ListView
를 확장하여 이러한 실무적인 요구사항들을 어떻게 CBV의 장점을 살려 쉽고 elegant하게 구현할 수 있는지 살펴보겠습니다.
2. 페이징(Pagination) 적용하기
많은 양의 데이터를 한 번에 보여주는 것은 사용자 경험에 좋지 않습니다. 페이징은 데이터를 여러 페이지로 나누어 보여줌으로써 사용자의 로딩 부담을 줄이고, 원하는 정보를 더 쉽게 찾도록 돕습니다.
2.1 기본 페이징 설정
ListView
는 내장된 페이징 기능을 제공합니다. 뷰 클래스에서 paginate_by
속성을 설정하기만 하면 됩니다.
views.py 예시
from django.views.generic import ListView
from .models import Article
class ArticleListView(ListView):
model = Article
template_name = 'articles/article_list.html'
context_object_name = 'articles'
paginate_by = 10 # 한 페이지에 10개의 Article 객체 표시
paginate_by = 10
: 위와 같이 설정하면ListView
는 자동으로 결과를 10개씩 나누어 페이지를 구성하고, 템플릿 컨텍스트에 페이징 관련 객체(paginator
,page_obj
,is_paginated
)를 넘겨줍니다.
2.2 템플릿에서 페이징 UI 구현하기
템플릿에서는 컨텍스트로 넘어온 page_obj
를 사용하여 페이지 번호 링크를 표시하고, 이전/다음 페이지 버튼 등을 구현할 수 있습니다.
articles/article_list.html 예시
<ul>
{% for article in articles %}
<li>{{ article.title }} - {{ article.created_at }}</li>
{% empty %}
<li>아직 게시된 글이 없습니다.</li>
{% endfor %}
</ul>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">« 처음</a>
<a href="?page={{ page_obj.previous_page_number }}">이전</a>
{% endif %}
<span class="current">
페이지 {{ page_obj.number }} / {{ paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">다음</a>
<a href="?page={{ paginator.num_pages }}">마지막 »</a>
{% endif %}
</span>
</div>
page_obj
: 현재 페이지의 데이터를 담고 있는 객체입니다.has_previous
,has_next
,previous_page_number
,next_page_number
,number
등의 속성을 제공합니다.paginator
: 전체 페이지 관련 정보를 담고 있는 객체입니다.num_pages
(총 페이지 수) 등의 속성을 제공합니다.
2.3 URL 연결 유지
페이징 링크를 생성할 때 ?page=2
와 같이 page
쿼리 파라미터를 사용하는 것을 확인할 수 있습니다. ListView
는 이 파라미터를 자동으로 인식하여 해당 페이지의 데이터를 보여줍니다.
3. 검색(Search) 기능 추가하기
웹사이트에서 원하는 정보를 빠르게 찾는 것은 매우 중요합니다. ListView
를 확장하여 검색 기능을 구현해 보겠습니다.
3.1 폼(Form) 정의
먼저 검색어를 입력받을 간단한 폼을 정의합니다.
forms.py 예시
from django import forms
class ArticleSearchForm(forms.Form):
search_term = forms.CharField(label='검색어', required=False)
3.2 ListView 수정
ListView
의 get_queryset()
메서드를 오버라이드하여 검색 기능을 구현합니다.
views.py 예시
from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm
class ArticleListView(ListView):
model = Article
template_name = 'articles/article_list.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset()
self.form = ArticleSearchForm(self.request.GET) # GET 요청에서 폼 데이터 가져오기
if self.form.is_valid():
search_term = self.form.cleaned_data.get('search_term')
if search_term:
# 제목이나 내용에 검색어가 포함된 Article 필터링
queryset = queryset.filter(
Q(title__icontains=search_term) | Q(content__icontains=search_term)
)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_form'] = self.form # 템플릿에 폼 전달
return context
get_queryset()
: 이 메서드를 오버라이드하여 기본 쿼리셋을 수정합니다.- 폼 인스턴스를 생성하고, 유효성 검사를 통과하면
cleaned_data
에서 검색어를 가져옵니다. Q 객체
를 사용하여 제목(title
) 또는 내용(content
)에 검색어가 포함된Article
객체들을 필터링합니다 (__icontains
는 대소문자 구분 없이 포함 여부를 확인합니다).get_context_data()
: 템플릿에서 폼을 렌더링할 수 있도록 폼 인스턴스를 컨텍스트에 추가합니다.
3.3 템플릿에서 검색 폼 표시 및 결과 유지
템플릿에 검색 폼을 추가하고, 검색 결과를 유지하면서 페이징이 가능하도록 URL을 구성합니다.
articles/article_list.html
<form method="get">
{{ search_form }}
<button type="submit">검색</button>
</form>
<ul>
{% for article in articles %}
<li>{{ article.title }} - {{ article.created_at }}</li>
{% empty %}
<li>검색 결과가 없습니다.</li>
{% endfor %}
</ul>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}">« 처음</a>
<a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">이전</a>
{% endif %}
<span class="current">
페이지 {{ page_obj.number }} / {{ paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">다음</a>
<a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}">마지막 »</a>
{% endif %}
</span>
</div>
- 폼의
method
를get
으로 설정하여 검색어를 URL 쿼리 파라미터로 전달합니다. - 페이지 링크를 생성할 때 현재 검색어(
search_form.search_term.value
)를 함께 URL 파라미터에 포함시켜 검색 결과를 유지하면서 페이징이 이루어지도록 합니다.|default:''
필터를 사용하여 검색어가 없을 경우 빈 문자열을 사용합니다.
4. 정렬(Ordering) 기능 추가하기
사용자들이 데이터를 원하는 기준으로 정렬하여 볼 수 있도록 정렬 기능을 추가해 보겠습니다.
4.1 정렬 선택 폼 추가 (선택 사항)
정렬 기준을 선택할 수 있는 드롭다운 폼을 추가할 수도 있습니다. 간단하게 URL 파라미터만으로 처리할 수도 있습니다. 여기서는 URL 파라미터를 사용하는 방법을 설명합니다.
4.2 ListView 수정
get_queryset()
메서드를 수정하여 order_by()
를 적용합니다.
views.py 예시 (검색 기능 포함)
from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm
class ArticleListView(ListView):
model = Article
template_name = 'articles/article_list.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset()
self.form = ArticleSearchForm(self.request.GET)
ordering = self.request.GET.get('ordering', '-created_at') # 기본 정렬: 최신순
if self.form.is_valid():
search_term = self.form.cleaned_data.get('search_term')
if search_term:
queryset = queryset.filter(
Q(title__icontains=search_term) | Q(content__icontains=search_term)
)
queryset = queryset.order_by(ordering)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_form'] = self.form
context['ordering'] = self.request.GET.get('ordering', '-created_at') # 현재 정렬 기준 템플릿에 전달
return context
self.request.GET.get('ordering', '-created_at')
: URL 쿼리 파라미터에서ordering
값을 가져옵니다. 값이 없으면 기본적으로-created_at
(최신순)으로 정렬합니다.queryset = queryset.order_by(ordering)
: 가져온ordering
값을 사용하여 쿼리셋을 정렬합니다. 모델 필드 이름 앞에-
를 붙이면 역순으로 정렬됩니다.
4.3 템플릿에서 정렬 링크 제공
템플릿에 각 필드별로 정렬할 수 있는 링크를 제공합니다.
articles/article_list.html 예시
<form method="get">
{{ search_form }}
<button type="submit">검색</button>
</form>
<table>
<thead>
<tr>
<th><a href="?ordering=title&search_term={{ search_form.search_term.value|default:'' }}">제목</a></th>
<th><a href="?ordering=-created_at&search_term={{ search_form.search_term.value|default:'' }}">작성일</a></th>
</tr>
</thead>
<tbody>
{% for article in articles %}
<tr>
<td>{{ article.title }}</td>
<td>{{ article.created_at }}</td>
</tr>
{% empty %}
<tr><td colspan="2">검색 결과가 없습니다.</td></tr>
{% endfor %}
</tbody>
</table>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">« 처음</a>
<a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">이전</a>
{% endif %}
<span class="current">
페이지 {{ page_obj.number }} / {{ paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">다음</a>
<a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">마지막 »</a>
{% endif %}
</span>
</div>
- 각 테이블 헤더에 정렬 기준(
ordering
) 파라미터를 포함한 링크를 제공합니다. - 페이지 링크를 생성할 때 현재 검색어뿐만 아니라 현재 정렬 기준(
ordering
)도 함께 URL 파라미터에 포함시켜 정렬 상태를 유지하면서 페이징이 이루어지도록 합니다.
5. Mixin으로 기능 결합하기
앞서 배운 Mixin을 활용하면 페이징, 검색, 정렬 기능을 더욱 깔끔하게 관리할 수 있습니다. 예를 들어, 검색 기능을 Mixin으로 분리할 수 있습니다.
mixins.py 예시
from django.db.models import Q
from .forms import ArticleSearchForm
class ArticleSearchMixin:
def get_queryset(self):
queryset = super().get_queryset()
self.form = ArticleSearchForm(self.request.GET)
if self.form.is_valid():
search_term = self.form.cleaned_data.get('search_term')
if search_term:
queryset = queryset.filter(
Q(title__icontains=search_term) | Q(content__icontains=search_term)
)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_form'] = getattr(self, 'form', ArticleSearchForm()) # 폼이 없을 경우 기본 폼 사용
return context
views.py 예시 (Mixin 적용)
from django.views.generic import ListView
from .models import Article
from .mixins import ArticleSearchMixin
class ArticleListView(ArticleSearchMixin, ListView):
model = Article
template_name = 'articles/article_list.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset()
ordering = self.request.GET.get('ordering', '-created_at')
queryset = queryset.order_by(ordering)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['ordering'] = self.request.GET.get('ordering', '-created_at')
return context
ArticleSearchMixin
을 만들어 검색 관련 로직을 분리했습니다.ArticleListView
는2ArticleSearchMixin
과ListView
를 상속받아 검색 기능과 목록 기능을 모두 갖게 됩니다.
6. CBV의 장점과 한계 (시리즈 마무리)
8편에 걸친 CBV 탐구 시리즈를 통해 우리는 함수 기반 뷰(FBV)에서 클래스 기반 뷰(CBV)로 전환하는 이유, CBV의 기본적인 구조와 활용법, 다양한 Generic View의 사용법, 그리고 Mixin을 활용한 기능 확장에 이르기까지 CBV의 핵심적인 내용을 살펴보았습니다.
CBV의 주요 장점:
- 코드 재사용성: Generic View와 Mixin을 통해 반복되는 코드를 줄이고 효율적인 개발이 가능합니다.
- 구조적인 코드: 클래스 기반의 코드는 로직을 명확하게 분리하고 관리하기 용이하게 합니다.
- 확장성: 상속과 Mixin을 통해 기존 기능을 쉽게 확장하고 새로운 기능을 추가할 수 있습니다.
- 유지보수성: 코드가 모듈화되어 있어 유지보수가 용이하고, 변경 사항이 전체 코드에 미치는 영향을 줄일 수 있습니다.
- 개발 생산성 향상: Django가 제공하는 다양한 Generic View와 Mixin을 활용하여 빠르게 개발할 수 있습니다.
CBV의 한계 및 고려 사항:
- 초기 학습 곡선: FBV에 비해 클래스 기반 구조에 대한 이해가 필요할 수 있습니다.
- 과도한 상속: 너무 많은 Mixin을 사용할 경우 코드의 흐름을 파악하기 어려워질 수 있습니다. 적절한 수준의 Mixin 활용이 중요합니다.
- 단순한 로직에는 오버엔지니어링: 아주 간단한 페이지나 기능의 경우 FBV가 더 간결할 수 있습니다. 상황에 따라 적절한 뷰 방식을 선택하는 것이 좋습니다.
결론적으로, Django CBV는 복잡하고 다양한 기능을 요구하는 웹 애플리케이션을 개발하는 데 매우 강력하고 유용한 도구입니다. CBV의 장점을 이해하고 적절하게 활용한다면, 더욱 효율적이고 유지보수가 용이한 코드를 작성할 수 있을 것입니다.
지금까지 클래스 기반 뷰(CBV) 탐구 시리즈를 읽어주셔서 감사합니다! 이 시리즈가 Django CBV를 이해하고 실제 프로젝트에 적용하는 데 조금이나마 도움이 되었기를 바랍니다.
이전 글 다시 보기
- 클래스 기반 뷰(CBV) 탐구 시리즈 #1 – FBV에서 CBV로 가는 이유와 개발자의 자세
- 클래스 기반 뷰(CBV) 탐구 시리즈 #2 – Django의 기본 View 클래스 이해하기
- 클래스 기반 뷰(CBV) 탐구 시리즈 #3 – FormView로 폼 처리 쉽게 하기
- 클래스 기반 뷰(CBV) 탐구 시리즈 #4 – ListView & DetailView 활용법
- 클래스 기반 뷰(CBV) 탐구 시리즈 #5 – CreateView, UpdateView, DeleteView로 CRUD 구현하기
- 클래스 기반 뷰(CBV) 탐구 시리즈 #6 – TemplateView & RedirectView 활용법
- 클래스 기반 뷰(CBV) 탐구 시리즈 #7 – Mixin 활용 및 권한 관리
“ListView 확장을 통해 더욱 풍부하고 사용자 친화적인 목록 기능을 구현하고,
CBV를 마스터하여 Django 개발 실력을 한 단계 업그레이드하세요!”
댓글이 없습니다.