Skip to content

클래스 기반 뷰 (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는 객체지향적 접근 - 두 프레임워크 모두 각자의 방식으로 코드 재사용성 제공

다음 장에서는 지금까지 배운 내용을 활용해 완성된 블로그를 만들어보겠습니다!

Comments