글을 작성하지는 않았지만 Django REST Framework(이하 DRF)
공식 문서의 튜토리얼을 따라서 간단한 ModelSerializer를 사용하여 API를 구축해보았다.
단순 JSON 데이터를 입력, 출력하는 것을 넘어 이번에는 DRF의 핵심 기능을 파헤쳐보고자 한다.
DRF는 HttpRequest
를 Request
객체로 확장하여 더 유연한 요청 파싱을 제공한다. 핵심 기능은 requst.POST
와 비슷하지만 웹 API에 더 유용한 request.data
속성이다.
request.POST
request.data
마찬가지로 Response
객체도 제공한다. 이 객체는 TemplateResponse
객체의 일종으로, 렌더링되지 않은 컨텐츠를 가져오고 컨텐츠 협상(?)을 통해 클라이언트에게 반환할 올바른 컨텐츠 유형을 결정한다.
return Response(data)
view에서 숫자 HTTP 상태 코드를 사용한다고 해서 항상 읽을 수 있는 것은 아니다. 그리고 잘못된 에러 코드가 잘못 되었는지 쉽게 알 수도 없다. DRF는 status
모듈의 HTTP_400_BAD_REQUEST
와 같이 각 상태 코드에 대해 더욱 명시적인 식별자를 제공한다.
DRF는 API view를 작성하는 데 사용할 두 wrapper를 제공한다.
@api_view
데코레이터APIView
클래스아래 코드는 기존의 views.py이다.
# views.py
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@csrf_exempt
def snippet_list(request):
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return JsonResponse(serializer.data, safe=False)
elif request.method == 'POST':
data = JSONParser().parse(request)
serializer = SnippetSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)
@csrf_exempt
def snippet_detail(request, pk):
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return HttpResponse(status=404)
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return JsonResponse(serializer.data)
elif request.method == 'POST':
data = JSONParser().parse(request)
print(f'JSONparsed data: {data}')
serializer = SnippetSerializer(snippet, data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data)
return JsonResponse(serializer.errors, status=400)
elif request.method == 'DELETE':
snippet.delete()
return HttpResponse(status=204)
데코레이터를 사용해서 리팩토링 해보자.
# views.py
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@api_view(['GET', 'POST'])
def snippet_list(request):
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = SnippetSerializer(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 snippet_detail(request, pk):
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return Response(serializer.data)
elif request.method == 'POST':
serializer = SnippetSerializer(snippet, 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':
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
가장 눈에 띄는 변화는 2개다.
JsonResponse
, HttpResponse
객체가 모두 Response
로 대체되었고, 각 Response 객체에 넘기는 status
인자에 단순 숫자 상태 코드가 아닌 status
객체의 식별자로 대체된 것이다.
아 또 있다.
별도의 컨텐츠 유형을 명시적으로 지정하지 않은 것이다. request.data
객체가 들어오는 json 요청을 처리할 수 있지만 다른 포맷의 요청도 처리할 수 있다. 마찬가지로 데이터를 포함한 Response 객체를 반환하지만 DRF가 올바른 컨텐츠 유형으로 렌더링하도록 허용한다.
이제 더 이상 하나의 컨텐츠 유형에만 국한되지 않는다는 이점을 살리기 위해 API 엔드포인트에 포맷 접미사를 추가하자. 지정된 포맷을 명시적으로 참조하는 URL이 제공되며, http://example.co/api/items/4.json
과 같은 URL을 핸들링할 수 있다.
각 views에 format
키워드 인자를 추가하면 된다.
def snippet_list(request, format=None):
...
def snippet_detail(request, pk=pk, format=None):
...
snippets/urls.py
를 조금 수정해서, 기존 URL에 format_suffix_patterns
를 추가하자.
# snippets/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
path('snippets/', views.snippet_list),
path('snippets/<int:pk>/', views.snippet_detail),
]
urlpatterns = format_suffix_patterns(urlpatterns)
필수는 아니지만 format_suffix_patterns
를 통해 특정 포맷을 간단하고 명확하게 참조할 수 있다.
API를 테스트해보자.
http http://127.0.0.1:8000/snippets/
HTTP/1.1 200 OK
Allow: OPTIONS, POST, GET
Content-Length: 529
Content-Type: application/json
Date: Sun, 22 Mar 2020 08:26:21 GMT
Server: WSGIServer/0.2 CPython/3.7.4
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
[
{
"code": "foo = \"bar\"\n",
"id": 1,
"language": "python",
"linenos": false,
"style": "friendly",
"title": ""
},
...
크게 다르지 않다.
Accept
헤더를 사용하거나 포맷 접미사를 붙여서 응답 데이터의 포맷을 바꿀 수 있다.
http http://127.0.0.1:8000/snippets/ Accept:application/json
http http://127.0.0.1:8000/snippets/ Accept:text/html
http http://127.0.0.1:8000/snippets.json
http http://127.0.0.1:8000/snippets.api
# JSON 형태
[
{
"code": "foo = \"bar\"\n",
"id": 1,
"language": "python",
"linenos": false,
"style": "friendly",
"title": ""
},
{
"code": "print(\"hello, world\")\n",
"id": 2,
"language": "python",
"linenos": false,
"style": "friendly",
"title": ""
}
]
브라우저에서 API를 확인할 수 있는데 UI가 별도로 있는 줄은 몰랐다. Django Admin 페이지같다.
API는 클라이언트 요청에 따라 응답 데이터의 content type을 정하기 때문에 웹 브라우저에서 요청하면 HTML 포맷의 데이터를 반환할 것이다.
web-browsable
한 API를 사용한다는 건 굉장히 사용성면에서 유리하고 API를 더 쉽게 만들고 사용할 수 있게 한다. 또한 다른 개발자들이 내가 만든 API를 파악하거나 사용하는 데에 진입장벽을 낮추기도 한다.
https://github.com/yvvyoon/learn-python/tree/master/django-rest-framework
https://www.django-rest-framework.org/tutorial/2-requests-and-responses/