클래스 기반 뷰 (CBV) 소개
지금까지 함수 기반 뷰(FBV)를 사용했지만, Django는 클래스 기반 뷰(CBV)도 제공합니다. CBV는 재사용성과 상속을 통한 확장성이 뛰어나며, 일반적인 패턴을 쉽게 구현할 수 있습니다.
1. FBV vs CBV
함수 기반 뷰 (FBV)
def post_list(request):
if request.method == 'GET':
posts = Post.objects.all()
return render(request, 'blog/post_list.html', {'posts': posts})
elif request.method == 'POST':
# POST 처리
pass
클래스 기반 뷰 (CBV)
from django.views.generic import ListView
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
2. 기본 View 클래스
View 클래스 사용
from django.views import View
from django.shortcuts import render
class PostView(View):
def get(self, request, *args, **kwargs):
posts = Post.objects.all()
return render(request, 'blog/post_list.html', {'posts': posts})
def post(self, request, *args, **kwargs):
# POST 요청 처리
title = request.POST.get('title')
content = request.POST.get('content')
# ... 처리 로직
return redirect('blog:post_list')
# urls.py
path('posts/', PostView.as_view(), name='post_list'),
TemplateView
from django.views.generic import TemplateView
class HomeView(TemplateView):
template_name = 'home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['latest_posts'] = Post.objects.filter(status='published')[:5]
context['popular_posts'] = Post.objects.order_by('-view_count')[:5]
return context
# 더 간단한 사용
path('about/', TemplateView.as_view(template_name='about.html'), name='about'),
3. 제네릭 뷰 - 조회
ListView
from django.views.generic import ListView
from django.db.models import Q
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
ordering = ['-created_at']
def get_queryset(self):
queryset = super().get_queryset()
# 검색 기능
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(title__icontains=search) | Q(content__icontains=search)
)
# 공개된 포스트만
return queryset.filter(status='published')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search'] = self.request.GET.get('search', '')
context['categories'] = Category.objects.all()
return context
DetailView
from django.views.generic import DetailView
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
# 공개된 포스트만 조회 가능
return super().get_queryset().filter(status='published')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 조회수 증가
self.object.view_count += 1
self.object.save(update_fields=['view_count'])
# 댓글 추가
context['comments'] = self.object.comments.filter(is_approved=True)
context['related_posts'] = Post.objects.filter(
category=self.object.category
).exclude(pk=self.object.pk)[:3]
return context
4. 제네릭 뷰 - 생성/수정/삭제
CreateView
from django.views.generic import CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
template_name = 'blog/post_form.html'
fields = ['title', 'content', 'category', 'tags']
success_url = reverse_lazy('blog:post_list')
def form_valid(self, form):
# 작성자 자동 설정
form.instance.author = self.request.user
messages.success(self.request, '포스트가 작성되었습니다.')
return super().form_valid(form)
def get_form(self, form_class=None):
form = super().get_form(form_class)
# 폼 커스터마이징
form.fields['content'].widget.attrs.update({'rows': 10})
form.fields['tags'].widget.attrs.update({'class': 'select2'})
return form
UpdateView
from django.views.generic import UpdateView
from django.contrib.auth.mixins import UserPassesTestMixin
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
template_name = 'blog/post_form.html'
fields = ['title', 'content', 'category', 'tags', 'status']
def test_func(self):
# 작성자만 수정 가능
post = self.get_object()
return self.request.user == post.author
def form_valid(self, form):
messages.success(self.request, '포스트가 수정되었습니다.')
return super().form_valid(form)
def get_success_url(self):
return reverse('blog:post_detail', kwargs={'pk': self.object.pk})
DeleteView
from django.views.generic import DeleteView
class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('blog:post_list')
def test_func(self):
post = self.get_object()
return self.request.user == post.author
def delete(self, request, *args, **kwargs):
messages.success(request, '포스트가 삭제되었습니다.')
return super().delete(request, *args, **kwargs)
5. FormView
from django.views.generic import FormView
from .forms import ContactForm
class ContactView(FormView):
template_name = 'contact.html'
form_class = ContactForm
success_url = reverse_lazy('contact_success')
def form_valid(self, form):
# 이메일 전송
send_mail(
subject=form.cleaned_data['subject'],
message=form.cleaned_data['message'],
from_email=form.cleaned_data['email'],
recipient_list=['admin@example.com'],
)
messages.success(self.request, '메시지가 전송되었습니다.')
return super().form_valid(form)
def get_initial(self):
initial = super().get_initial()
if self.request.user.is_authenticated:
initial['email'] = self.request.user.email
initial['name'] = self.request.user.get_full_name()
return initial
6. Mixin 활용
커스텀 Mixin 생성
class AuthorRequiredMixin(UserPassesTestMixin):
"""작성자만 접근 가능"""
def test_func(self):
obj = self.get_object()
return self.request.user == obj.author
class PublishedPostMixin:
"""공개된 포스트만 조회"""
def get_queryset(self):
return super().get_queryset().filter(status='published')
# 사용 예제
class PostUpdateView(LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
model = Post
fields = ['title', 'content']
class PublicPostListView(PublishedPostMixin, ListView):
model = Post
paginate_by = 10
여러 Mixin 조합
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic.detail import SingleObjectMixin
class CommentCreateView(LoginRequiredMixin, SingleObjectMixin, FormView):
model = Post
form_class = CommentForm
template_name = 'blog/comment_form.html'
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def form_valid(self, form):
comment = form.save(commit=False)
comment.post = self.object
comment.author = self.request.user
comment.save()
return super().form_valid(form)
def get_success_url(self):
return self.object.get_absolute_url()
7. 날짜 기반 뷰
from django.views.generic.dates import YearArchiveView, MonthArchiveView
class PostYearArchiveView(YearArchiveView):
queryset = Post.objects.filter(status='published')
date_field = 'created_at'
make_object_list = True
allow_future = False
template_name = 'blog/post_archive_year.html'
class PostMonthArchiveView(MonthArchiveView):
queryset = Post.objects.filter(status='published')
date_field = 'created_at'
month_format = '%m'
template_name = 'blog/post_archive_month.html'
# urls.py
path('archive/<int:year>/', PostYearArchiveView.as_view(), name='post_year_archive'),
path('archive/<int:year>/<int:month>/', PostMonthArchiveView.as_view(), name='post_month_archive'),
8. RedirectView
from django.views.generic import RedirectView
class OldPostRedirectView(RedirectView):
permanent = True
query_string = True
pattern_name = 'blog:post_detail'
def get_redirect_url(self, *args, **kwargs):
post = get_object_or_404(Post, old_slug=kwargs['old_slug'])
return super().get_redirect_url(pk=post.pk)
# 간단한 사용
path('old-about/', RedirectView.as_view(url='/about/', permanent=True)),
9. CBV 커스터마이징
메서드 오버라이드
class CustomListView(ListView):
model = Post
def get(self, request, *args, **kwargs):
# get 메서드 전체 오버라이드
self.object_list = self.get_queryset()
# 특별한 처리
if request.user.is_authenticated:
self.object_list = self.object_list.select_related('author')
context = self.get_context_data()
return self.render_to_response(context)
def dispatch(self, request, *args, **kwargs):
# 모든 HTTP 메서드 전에 실행
if not request.user.is_staff:
return redirect('login')
return super().dispatch(request, *args, **kwargs)
동적 폼 처리
class DynamicFormView(CreateView):
model = Post
template_name = 'blog/post_form.html'
def get_form_class(self):
if self.request.user.is_staff:
return PostAdminForm
else:
return PostUserForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
10. 실습: API 뷰 만들기
from django.views import View
from django.http import JsonResponse
import json
class PostAPIView(View):
def get(self, request, pk=None):
if pk:
# 상세 조회
post = get_object_or_404(Post, pk=pk)
data = {
'id': post.pk,
'title': post.title,
'content': post.content,
'author': post.author.username,
'created_at': post.created_at.isoformat(),
}
else:
# 목록 조회
posts = Post.objects.filter(status='published')
data = [{
'id': post.pk,
'title': post.title,
'author': post.author.username,
'created_at': post.created_at.isoformat(),
} for post in posts]
return JsonResponse(data, safe=False)
def post(self, request):
if not request.user.is_authenticated:
return JsonResponse({'error': 'Authentication required'}, status=401)
data = json.loads(request.body)
post = Post.objects.create(
title=data['title'],
content=data['content'],
author=request.user
)
return JsonResponse({
'id': post.pk,
'title': post.title,
'message': 'Post created successfully'
}, status=201)
CBV 장단점
장점
- 재사용성: 상속과 Mixin으로 코드 재사용
- 구조화: 일관된 패턴과 구조
- 확장성: 메서드 오버라이드로 쉬운 커스터마이징
- 간결함: 제네릭 뷰로 반복 코드 감소
단점
- 학습 곡선: 초기 이해가 어려움
- 복잡성: 단순한 뷰에는 과도할 수 있음
- 디버깅: 상속 체인이 길어지면 디버깅 어려움
정리
CBV vs FBV 선택 기준: - CBV 사용: CRUD 같은 일반적인 패턴, 재사용이 필요한 경우 - FBV 사용: 단순한 로직, 특수한 처리가 필요한 경우 - 혼용: 프로젝트 내에서 상황에 맞게 선택
FastAPI와 비교: - FastAPI는 주로 함수 기반 (데코레이터 사용) - Django CBV는 객체지향적 접근 - 두 프레임워크 모두 각자의 방식으로 코드 재사용성 제공
다음 장에서는 지금까지 배운 내용을 활용해 완성된 블로그를 만들어보겠습니다!