Skip to main content
Summary

같은 기능, 다른 표현 방식

앞 장에서는 함수 기반 View로 게시판 CRUD를 완성했다. 목록, 상세, 작성, 수정, 삭제가 모두 잘 동작하지만 코드를 다시 보면 반복되는 부분이 많다.

작성과 수정은 모두 GET 요청에서 폼을 보여 주고, POST 요청에서 폼을 검증한 뒤 저장한다. 상세, 수정, 삭제는 모두 URL의 pk로 게시글 하나를 찾는다. 목록은 여러 객체를 가져와 템플릿에 넘긴다.

Django는 이런 반복을 줄이기 위해 클래스 기반 View, 특히 제네릭 View를 제공한다. 제네릭 View는 목록, 상세, 생성, 수정, 삭제처럼 자주 나오는 화면 흐름을 미리 구현해 둔 클래스다. 우리는 그 위에 모델, 템플릿 이름, 성공 후 이동할 주소처럼 달라지는 부분만 지정하면 된다.1

함수 기반 View와 클래스 기반 View의 차이는 "무엇을 직접 쓰느냐"에 있다.

구분함수 기반 View클래스 기반 View
형태하나의 함수하나의 클래스
장점흐름이 눈에 잘 보인다반복되는 CRUD 코드를 줄이기 좋다
단점비슷한 코드가 늘어나기 쉽다처음에는 내부 동작이 숨겨져 보인다
적합한 상황짧고 특수한 처리목록, 상세, 작성, 수정, 삭제 같은 표준 화면

둘 중 하나만 써야 하는 것은 아니다. 한 프로젝트 안에서도 단순한 검색 API는 함수 기반 View로 만들고, 게시판 CRUD는 클래스 기반 View로 만들 수 있다. 중요한 기준은 코드가 더 읽기 쉬워지는가이다.

클래스 기반 View로 코드 정리하기AI 생성 이미지

클래스 기반 View는 URL에서 as_view()로 연결한다

함수 기반 View는 URL에 함수 이름을 바로 넘겼다.

path('', views.post_list, name='post_list')

클래스 기반 View는 클래스 자체를 바로 넘기지 않고 as_view()를 호출한다.

path('', views.PostListView.as_view(), name='post_list')

as_view()는 클래스를 Django가 호출할 수 있는 View 형태로 바꿔 준다. 요청이 들어오면 클래스 기반 View는 요청 방식에 따라 get(), post() 같은 메서드로 처리를 나눈다. 예를 들어 목록 페이지에 접속하는 GET 요청은 내부적으로 get() 흐름을 탄다. 작성 폼을 제출하는 POST 요청은 post() 흐름을 탄다.2

하지만 처음부터 get()과 post()를 직접 오버라이드할 필요는 없다. ListView, DetailView, CreateView, UpdateView, DeleteView에는 이미 기본 흐름이 들어 있다. 우리는 필요한 설정만 추가하면서 시작하면 된다.

게시판 URL을 클래스 기반 View로 바꾸면 다음과 같다.

# boards/urls.py
from django.urls import path
from . import views

app_name = 'boards'

urlpatterns = [
    path('', views.PostListView.as_view(), name='post_list'),
    path('posts/new/', views.PostCreateView.as_view(), name='post_create'),
    path('posts/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
    path('posts/<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_update'),
    path('posts/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]

URL 이름은 이전 장과 그대로 유지했다. 템플릿에서 이미 {% url 'boards:post_detail' post.pk %}처럼 URL 이름을 사용하고 있다면, View 구현 방식을 바꿔도 템플릿 링크는 거의 그대로 둘 수 있다.

ListView로 목록 화면 만들기

먼저 목록 View를 바꿔 보자. 함수 기반 View에서는 직접 QuerySet을 만들고 render()에 넘겼다.

def post_list(request):
    posts = Post.objects.filter(is_published=True).order_by('-created_at')
    return render(request, 'boards/post_list.html', {'posts': posts})

ListView를 사용하면 이 흐름을 클래스로 표현할 수 있다.

# boards/views.py
from django.views.generic import ListView
from .models import Post


class PostListView(ListView):
    model = Post
    template_name = 'boards/post_list.html'
    context_object_name = 'posts'

    def get_queryset(self):
        return Post.objects.filter(is_published=True).order_by('-created_at')

model = Post는 이 View가 Post 모델의 목록을 다룬다는 뜻이다. template_name은 사용할 템플릿 파일을 직접 지정한다. 지정하지 않아도 Django의 기본 규칙이 있지만, 처음에는 명시하는 편이 이해하기 쉽다.

context_object_name = 'posts'는 템플릿에서 사용할 변수 이름을 정한다. 이 설정이 없으면 템플릿에서 object_list 같은 기본 이름을 사용해야 한다. 이전 장에서 만든 템플릿은 posts를 반복하고 있으므로 이 이름을 맞춰 주면 템플릿을 고치지 않아도 된다.

get_queryset()은 목록에 표시할 데이터를 정하는 메서드다. 기본값은 대략 Post.objects.all()이라고 생각하면 된다. 우리는 공개된 글만 최신순으로 보여 주고 싶으므로 이 메서드를 오버라이드했다. 오버라이드는 부모 클래스의 기본 동작을 자식 클래스에서 필요한 방식으로 다시 정의하는 것이다.

DetailView로 상세 화면 만들기

상세 화면도 비슷하다. 함수 기반 View에서는 get_object_or_404()로 게시글 하나를 직접 가져왔다.

def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk, is_published=True)
    return render(request, 'boards/post_detail.html', {'post': post})

DetailView는 URL의 pk 값을 사용해 객체 하나를 찾는 기본 흐름을 이미 알고 있다.

# boards/views.py
from django.views.generic import DetailView
from .models import Post


class PostDetailView(DetailView):
    model = Post
    template_name = 'boards/post_detail.html'
    context_object_name = 'post'

    def get_queryset(self):
        return Post.objects.filter(is_published=True)

여기서도 context_object_name = 'post'를 지정했다. 기존 상세 템플릿이 {{ post.title }}, {{ post.content }}처럼 작성되어 있기 때문이다.

get_queryset()을 사용해 공개된 글만 조회 대상으로 제한했다. 이렇게 하면 비공개 글의 pk를 주소에 직접 입력해도 상세 화면에 표시되지 않는다. 객체 하나를 가져오는 View에서도 조회 범위를 먼저 좁히는 습관이 중요하다.

CreateView로 작성 흐름 줄이기

작성 View는 클래스 기반 View의 장점이 가장 잘 보이는 부분이다. 함수 기반 View에서는 다음 흐름을 직접 작성했다.

  1. GET 요청이면 빈 폼을 보여 준다.

  2. POST 요청이면 request.POST로 폼을 만든다.

  3. form.is_valid()로 검증한다.

  4. 저장 후 상세 페이지로 이동한다.

CreateView는 이 과정을 기본으로 제공한다.

# boards/views.py
from django.urls import reverse
from django.views.generic import CreateView
from .models import Post
from .forms import PostForm


class PostCreateView(CreateView):
    model = Post
    form_class = PostForm
    template_name = 'boards/post_form.html'

    def get_success_url(self):
        return reverse('boards:post_detail', kwargs={'pk': self.object.pk})

form_class = PostForm은 이 View에서 사용할 폼 클래스를 지정한다. CreateView는 GET 요청에서는 빈 PostForm을 만들고, POST 요청에서는 제출된 데이터로 PostForm을 만든 뒤 검증한다. 검증에 성공하면 객체를 저장한다.

저장 후 어디로 이동할지는 get_success_url()에서 정했다. self.object는 저장된 객체를 가리킨다. 새 글을 저장한 뒤 그 글의 상세 페이지로 이동하려면 self.object.pk가 필요하다.

목록으로만 이동하면 충분한 경우에는 success_url을 더 간단히 사용할 수도 있다.

from django.urls import reverse_lazy

success_url = reverse_lazy('boards:post_list')

reverse_lazy()는 URL 계산을 나중으로 미루는 함수다. 클래스 속성처럼 파일을 불러오는 시점에 평가될 수 있는 곳에서는 reverse()보다 reverse_lazy()가 안전하다.

UpdateView와 DeleteView로 수정·삭제 정리하기

수정은 작성과 거의 같다. 차이는 새 객체를 만드는 것이 아니라 URL의 pk에 해당하는 기존 객체를 가져와 폼에 채운다는 점이다. UpdateView는 이 흐름을 기본으로 처리한다.

# boards/views.py
from django.urls import reverse
from django.views.generic import UpdateView
from .models import Post
from .forms import PostForm


class PostUpdateView(UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'boards/post_form.html'
    context_object_name = 'post'

    def get_success_url(self):
        return reverse('boards:post_detail', kwargs={'pk': self.object.pk})

함수 기반 View에서 직접 넘겼던 instance=post가 코드에서 사라졌다. 사라진 것이 아니라 UpdateView 내부의 기본 흐름으로 들어간 것이다. URL의 pk로 객체를 찾고, 그 객체를 기준으로 폼을 만들고, 검증에 성공하면 같은 객체를 수정한다.

삭제는 DeleteView를 사용한다.

# boards/views.py
from django.urls import reverse_lazy
from django.views.generic import DeleteView
from .models import Post


class PostDeleteView(DeleteView):
    model = Post
    template_name = 'boards/post_confirm_delete.html'
    context_object_name = 'post'
    success_url = reverse_lazy('boards:post_list')

DeleteView는 GET 요청에서 확인 화면을 보여 주고, POST 요청에서 삭제를 수행한다. 따라서 앞 장에서 강조한 원칙, 즉 링크 클릭만으로 삭제하지 않고 확인 폼의 POST 요청으로 삭제한다는 흐름을 유지할 수 있다.

기존 삭제 템플릿도 그대로 사용할 수 있다.

<form method="post">
  {% csrf_token %}
  <button type="submit">삭제</button>
</form>

클래스 기반 View를 사용해도 CSRF 토큰이 자동으로 템플릿에 생기는 것은 아니다. 데이터를 바꾸는 POST 폼에는 여전히 {% csrf_token %}을 넣어야 한다.

기본 동작을 바꾸는 세 가지 지점

클래스 기반 View를 익힐 때 모든 메서드를 외울 필요는 없다. 처음에는 자주 쓰는 확장 지점 세 가지를 기억하면 충분하다.

첫째, get_queryset()은 조회 대상을 바꿀 때 사용한다.

class PostListView(ListView):
    model = Post
    template_name = 'boards/post_list.html'
    context_object_name = 'posts'

    def get_queryset(self):
        queryset = Post.objects.filter(is_published=True)
        keyword = self.request.GET.get('q')

        if keyword:
            queryset = queryset.filter(title__icontains=keyword)

        return queryset.order_by('-created_at')

self.request는 현재 요청 객체다. 함수 기반 View의 request와 같은 역할을 한다. 위 코드는 주소에 ?q=django 같은 검색어가 들어오면 제목에 해당 단어가 포함된 글만 보여 준다.

둘째, get_context_data()는 템플릿에 추가 데이터를 넘길 때 사용한다.

class PostListView(ListView):
    model = Post
    template_name = 'boards/post_list.html'
    context_object_name = 'posts'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['page_title'] = '게시글 목록'
        context['post_count'] = self.get_queryset().count()
        return context

중요한 부분은 super().get_context_data(**kwargs)를 먼저 호출하는 것이다. 부모 클래스가 이미 만든 기본 context를 가져온 뒤 필요한 값을 추가해야 한다. 처음부터 빈 딕셔너리를 만들면 posts 같은 기본 데이터가 빠질 수 있다.

셋째, form_valid()는 폼 검증이 끝난 뒤 저장 직전에 추가 처리를 할 때 사용한다.

class PostCreateView(CreateView):
    model = Post
    form_class = PostForm
    template_name = 'boards/post_form.html'

    def form_valid(self, form):
        form.instance.is_published = True
        return super().form_valid(form)

form_valid()는 폼이 유효할 때 호출된다. 사용자가 직접 입력하지 못하게 하고 싶은 값을 서버에서 채울 때 유용하다. 다음 장에서 사용자 인증을 다룬 뒤에는 form.instance.author = self.request.user처럼 현재 로그인한 사용자를 작성자로 저장하는 방식도 자주 사용한다.

짧아진 코드보다 중요한 것

클래스 기반 View의 목표는 단순히 줄 수를 줄이는 것이 아니다. 더 중요한 것은 반복되는 웹 화면의 흐름을 Django가 제공하는 이름으로 정리하는 것이다.

  • 여러 객체를 보여 주면 ListView

  • 객체 하나를 보여 주면 DetailView

  • 새 객체를 만들면 CreateView

  • 기존 객체를 고치면 UpdateView

  • 객체를 삭제하면 DeleteView

이 이름만 봐도 View의 목적이 드러난다. 반대로 처리 흐름이 표준 CRUD에서 많이 벗어나고 조건 분기가 복잡하다면 함수 기반 View가 더 읽기 쉬울 수 있다. 클래스 기반 View는 무조건 더 좋은 방식이 아니라, Django가 이미 알고 있는 패턴을 활용하는 방식이다.

이제 게시판 코드는 더 짧고 일정한 구조를 갖게 되었다. 다음 단계에서는 이 게시판에 사용자를 연결해야 한다. 누가 글을 작성했는지 저장하고, 작성자만 수정하거나 삭제할 수 있게 만들려면 인증과 권한 관리가 필요하다.

참고

1Class-Based & Generic Views — Django Tutorial

2Django Class-Based Views Guide

책 목록

Django 활용법: 웹 개발 기본기부터 배포와 운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기