REST프레임워크는 API 뷰를 만드는데 사용할 수 있는 2가지 wrapper를 제공한다.
wrapper 에서는 뷰에서 Request 인스턴스를 수신하고, 해당 메소드를 인자로 전달해서 해당 메소드에 맞는 로직이 실행되도록 도와준다. 즉, @api_view는 클래스형 뷰의 as_view()처럼 여러 가지 유형의 메소드를 함수형에서도 처리해 줄 수 있도록 도와준다고 보면 될 듯 하다.
엔드포인트는 pk 정보가 필요 없는 List, Create를 수행하는 뷰와 pk 정보가 필요한 Detail, Update, Delete를 수행하는 뷰, 2가지로 작성할 수 있다.
[api/urls.py]
from django.urls import path
from news.api.views import (article_list_create_api_view,
article_detail_api_view)
urlpatterns = [
path("articles/", article_list_create_api_view, name='article-list'),
path("articles/<int:pk>", article_detail_api_view, name='article-detail')
]
뉴스 아티클의 리스트를 뿌려주는 기능과 리스트를 추가하는 기능은 하나의 뷰에서 처리할 수 있도록 아래와 같이 코드를 짤 수 있다.
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from news.models import Article
from news.api.serializers import ArticleSerializer
@api_view(["GET", "POST"])
def article_list_create_api_view(request):
if request.method == "GET":
articles = Article.objects.filter(active=True)
serializer = ArticleSerializer(articles, many=True)
# many => queryset에 대응. many 없으면 instance 1개가 올 것으로 기대하고 있어 에러 발생함.
return Response(serializer.data)
elif request.method == "POST":
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Article을 생성하는 POST 메소드가 처리되는 로직을 debug로 살펴보면 아래와 같다.
아래 내용을 통해 request.data, request.query_params, serializer에는 각각 어떤 값들이 담겨줘서 처리하게 되는지 이해할 수 있다.
> /Users/seungholee/udemy_drf/newsapi/news/api/views.py(79)article_list_create_api_view()
-> serializer = ArticleSerializer(data=request.data)
(Pdb) n
> /Users/seungholee/udemy_drf/newsapi/news/api/views.py(80)article_list_create_api_view()
-> if serializer.is_valid():
(Pdb) request
<rest_framework.request.Request object at 0x10fa3a150>
(Pdb) request.data
{'time_since_publication': '1 month, 2 weeks', 'author': 'Joey', 'title': 'Trade force', 'description': "it's tough", 'body': 'hahahah', 'location': 'Seoul', 'publication_date': '2020-07-09T08:31:45Z', 'active': True}
(Pdb) request.query_params
<QueryDict: {}>
(Pdb) request.parsers
[<rest_framework.parsers.JSONParser object at 0x10fa148d0>, <rest_framework.parsers.FormParser object at 0x10fa3a450>, <rest_framework.parsers.MultiPartParser object at 0x10fa3a190>]
(Pdb) n
> /Users/seungholee/udemy_drf/newsapi/news/api/views.py(81)article_list_create_api_view()
-> serializer.save()
(Pdb) serializer
ArticleSerializer(data={'time_since_publication': '1 month, 2 weeks', 'author': 'Joey', 'title': 'Trade force', 'description': "it's tough", 'body': 'hahahah', 'location': 'Seoul', 'publication_date': '2020-07-09T08:31:45Z', 'active': True}):
id = IntegerField(label='ID', read_only=True)
time_since_publication = SerializerMethodField()
author = StringRelatedField()
title = CharField(max_length=120)
description = CharField(max_length=200)
body = CharField(style={'base_template': 'textarea.html'})
location = CharField(max_length=120)
publication_date = DateTimeField()
active = BooleanField(required=False)
created_at = DateTimeField(read_only=True)
updated_at = DateTimeField(read_only=True)
3-1) Request
DRF에서는 HTTP 요청 객체로서 HttpRequest 객체를 확장한 Request 객체를 사용한다. Request는 HttpRequest 객체보다 요청 내용을 유연하게 파싱할 수 있도록 돕는다. 자주 사용하게 될 Request의 속성은 아래와 같다.
[request.POST와 request.data의 차이점]
request.POST # 폼 데이터만 처리할 수 있고, POST 메소드에서만 동작한다.
request.data # 임의의 데이터를 처리할 수 있고, POST, PUT, PATCH 메소드에서 동작한다.
추가적인 설명
DRF는 HttpRequest를 Request 객체로 확장하여 더 유연한 요청 파싱을 제공한다. 핵심 기능은 requst.POST와 비슷하지만 웹 API에 더 유용한 request.data 속성이다.request.POST request.data
2-2) Response
렌더링되지 않은 내용을 읽어서 클라이언트가 요청한 콘텐트 타입에 맞는 형식으로 자동 렌더링 해 준다. Pure Django에서는 전달할 데이터에 따라 HttpResponse, JsonResponse를 개발자가 직접 지정을 해 주어야 하지만, DRF의 Response를 이용하면 알아서 렌더링을 해 주기 때문에 편하게 쓸 수 있다.
# Signature
Response(data, status=None, template_name=None, headers=None, content_type=None)
# use case
return Response(data, status.HTTP_201_CREATED)
추가적인 설명
Response
객체는 TemplateResponse 객체의 일종으로, 렌더링되지 않은 컨텐츠를 가져오고 컨텐츠 협상(?)을 통해 클라이언트에게 반환할 올바른 컨텐츠 유형을 결정한다.return Response(data)
2-3) status
REST Framework에서는 status 모듈 안에 각각의 상태정보를 속성으로 담고 있다. 따라서 status.[상태 속성값]을 호출하면 그에 맞는 상태 값이 전달된다.
status.HTTP_200_OK
stauts.HTTP_400_BAD_REQUEST
뉴스 아티클의 디테일 정보 가져오기, 수정하기, 삭제하기를 수행하는 뷰를 만들어 보자.
@api_view(["GET", "PUT", "DELETE"])
def article_detail_api_view(request, pk):
try:
article = Article.objects.get(pk=pk)
except Article.DoesNotExist:
return Response({"error" : {
"code" : 404,
"message" : "Article not found"
}}, status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = ArticleSerializer(article)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = ArticleSerializer(article, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
=> 이렇게 했다가 오류가 나왔다, 그 이유는 try: 문중 get을 할 때 get에 옵션을 걸지 않아서 serialize 할 때 many=True로 걸어야됀다고 하는 에러였다.
https://itinerant.tistory.com/164 ==> 2번의 Attribute 에러
고친 views.py
API는 클라이언트 요청에 따라 응답 데이터의 content type을 정하기 때문에 웹 브라우저에서 요청하면 HTML 포맷의 데이터를 반환할 것이다.
web-browsable한 API를 사용한다는 건 굉장히 사용성면에서 유리하고 API를 더 쉽게 만들고 사용할 수 있게 한다. 또한 다른 개발자들이 내가 만든 API를 파악하거나 사용하는 데에 진입장벽을 낮추기도 한다.
이번 시간에는 본격적으로 APIView 에 대해서 알아보도록 합시다.
APIView
와 api_view
는 각각 CBV와 FBV 에 대응되는 내용입니다.
두 가지 모두 뷰에 여러가지 기본 설정을 부여하게 됩니다. 이는 아래와 같고 상황에 맞춰서 이를 커스튬하여 사용하게 됩니다.
요청에 따라 적절한 직렬화/비직렬화 선택
요청 내역에서 API 버전 정보를 탐지할 클래스 지정
우선 APIView
부터 자세히 알아보도록 합시다.
위에서 말했듯이 이는 CBV 중 하나이기 때문에 하나의 URL 에 대해서만 처리를 할 수 있습니다.
/post/ 에 대한 CBV
/post/<int:pk>/ 에 대한 CBV
요청 method 에 맞게 맴버함수를 정의하면 해당 method 로 request가 들어올 때 호출되게 됩니다.
def get(self, request):
pass
def post(self, request):
pass
def put(self, request):
pass
def delete(self, request):
pass
각 method 가 호출되면 위에서 봤던 설정에 맞춰 처리가 이루어집니다.
실습
실습을 위해 간단한 Post 모델을 정의하였습니다.
# models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
create_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
이에 대한 Seializer 입니다.
# serializers.py
from rest_framework.serializers import ModelSerializer
from .models import Post
class PostSerializer(ModelSerializer):
class Meta:
model = Post
fields = '__all__'
views 에서 이들을 불러와 처리를 해주게 됩니다.
# views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
# 포스팅 목록 및 새 포스팅 작성
class PostListAPIView(APIView):
def get(self, request):
serializer = PostSerializer(Post.objects.all(), many=True)
def post(self, request):
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
from django.shortcuts import get_object_or_404
# 포스팅 내용, 수정, 삭제
class PostDetailAPIView(APIView):
def get_object(self, pk):
return get_object_or_404(Post, pk=pk)
def get(self, request, pk, format=None):
post = self.get_object(pk)
serializer = PostSerializer(post)
return Response(serializer.data)
def put(self, request, pk):
post = self.get_object(pk)
serializer = PostSerializer(post, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
post = self.get_object(pk)
post.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
각 클래스에 대해 as_view 로 라우팅을 해줍니다.
from django.urls import path, include
from . import views
urlpatterns = [
# FBV
path('post/', views.PostListAPIView.as_view()),
path('post/<int:pk>/',views.PostDetailAPIView.as_view()),
]
api_view는 FBV 에 대해서 사용하는 장식자 입니다.
장식자에 대한 설명은 위의 링크를 참고하시면 됩니다.
실습
api_view
의 첫번째 인자로 해당 함수에서 가능한 request method 를 리스트로 지정해줍니다.
# views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
from rest_framework.decorators import api_view
@api_view(['GET','POST'])
def post_list(request):
if request.method == 'GET':
qs = Post.objects.all()
serializer = PostSerializer(qs, many=True)
return Response(serializer.data)
else:
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
@api_view(['GET','PUT','DELETE'])
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == 'GET':
serializer = PostSerializer(post)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = PostSerializer(post, data=reqeust.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
post.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
# urls.py
from django.urls import path, include
from . import views
urlpatterns = [
# FBV
path('cbv/post/', views.post_list),
path('cbv/post/<int:pk>/',views.post_detail),
]