[DRF] ViewSet과 Router

Django REST framework의 ViewSet과 Router에 대해 공부한다.

Posted by Seoyoung Lee on August 10, 2020 · 6 mins read

ViewSet


ViewSet은 2개의 뷰를 만들어주는 보다 확장된 형태의 CBV(Class Based View)이다.

모든 CBV는 .as_view({'http_method': '처리할 멤버함수'})를 호출하여 해당 http_method를 지원하는 뷰 함수를 생성한다. 1개의 뷰 함수를 생성하므로, 하나의 URL만을 처리할 수 있다.

DRF에서는 ReadOnlyModelViewSetModelViewSet의 2가지 뷰셋을 지원한다. 아래에서는 ModelViewSet에 대한 예시를 살펴본다.

우선 app을 만들고 migrate후, 모델과 이에 대한 Serializer를 만든다.

# models.py

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    is_public = models.BooleanField(default=False)
    create_at = models.DateTimeField(auto_now_add=True)
    update_at = models.DateTimeField(auto_now=True)


# Serializers.py

from rest_framework.serializers import ModelSerializer
from .models import Post
    
class PostSerializer(ModelSerializer):
    class Meta:
        model = Post
        fields = '__all__'

다음으로 ViewSet을 정의한다.

# views.py

from rest_framework.viewsets import ModelViewSet
from .models import Post
from .serializers import PostSerializer

class PostViewSet(ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer  


# REST API 규격에 맞춘 URL 매핑

# List Route
post_list = PostViewSet.as_view({
    'get': 'list',
    'post': 'create',
})

# Detail Route
post_detail = PostViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy',
})

이렇게 만들어진 뷰 함수는 다른 FBV와 동일하게 URLConf에 매핑할 수 있다.

# urls.py

from django.urls import path, include
from . import views

urlpatterns = [
    path('post/', views.post_list),
    path('post/<int:pk>/', views.post_detail),
]


Router


기존에는 as_view()를 통해 각 request method마다 대응되는 함수를 연결시켜 주었다면 router는 단지 PostViewSetRouter에 등록하기만 하면, 이를 알아서 연결해준다. 디폴트 매핑은 위와 같이 list routedetail route가 있다.

Router에는 뷰셋만 등록할 수 있으며(뷰는 불가능) 하나의 Router에 다수의 뷰셋을 등록할 수 있다.

위의 urls.py를 아래와 같이 Router를 사용하도록 수정했다.

# urls.py

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r'post',views.PostViewSet)
# /post/ 주소에 대해 URL Reverse 이름은 post-list이 등록
# /post/10/ 류의 주소에 대해 URL Reverse 이름은 post-detail이 등록

urlpatterns = [
    path('',include(router.urls)),
]

Router를 사용하면 더 이상 as_view()를 통해 단일 뷰를 뽑아낼 필요가 없다.

# views.py

from rest_framework.viewsets import ModelViewSet
from .models import Post
from .serializers import PostSerializer

class PostViewSet(ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer  


ViewSet에 API추가하기


디폴트 매핑의 경우, list route 에 대해서 2개, detail route 에 대해서 4개에 대해서 매핑을 해주게 된다. 여기서 추가적인 API를 만들어 매핑하는 방법이 있다.

ViewSet의 멤버함수로 추가 API를 구현한 후, decorator를 붙여주면 된다. 이때 URL은 Router가 알아서 결정한다.

@action decorator

from rest_framework.decorators import action

위와 같이 action decorator를 import해주고 첫번째 인자로 detail, 두번째 인자로 methods를 지정한다.

detail이 True인 경우 pk값을 지정해주는 detail route에 등록이 되고, False인 경우에는 목록단위로 적용되는 list route에 등록이 된다. 이 때 detail의 값에 따라 Router가 URL을 결정하는 방법이 다르다.

methods인자에는 request method를 지정해줄 수 있다. 디폴트 값은 get이다.

아래 코드는 is_public값이 True인 레코드만 필터링하는 public_list와 특정 레코드의 is_public을 True로 수정하는 set_public을 정의했다.

# views.py

from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer

class PostViewSet(ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer   

    # url : post/public_list/
    @action(detail=False)
    def public_list(self, request):
        qs = self.queryset.filter(is_public=True)
        serializer = self.get_serializer(qs, many=True)
        return Response(serializer.data)
    
    # url : post/{pk}/set_public/
    @action(detail=True, methods=['patch'])
    def set_public(self, request, pk):
        instance = self.get_object()
        instance.is_public = True
        instance.save()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)