Django REST Framework 뽀개기 1

marco x brown·2020년 3월 22일
6

글을 작성하지는 않았지만 Django REST Framework(이하 DRF) 공식 문서의 튜토리얼을 따라서 간단한 ModelSerializer를 사용하여 API를 구축해보았다.
단순 JSON 데이터를 입력, 출력하는 것을 넘어 이번에는 DRF의 핵심 기능을 파헤쳐보고자 한다.

Request 객체

DRF는 HttpRequestRequest 객체로 확장하여 더 유연한 요청 파싱을 제공한다. 핵심 기능은 requst.POST와 비슷하지만 웹 API에 더 유용한 request.data 속성이다.

request.POST
request.data

Response 객체

마찬가지로 Response 객체도 제공한다. 이 객체는 TemplateResponse 객체의 일종으로, 렌더링되지 않은 컨텐츠를 가져오고 컨텐츠 협상(?)을 통해 클라이언트에게 반환할 올바른 컨텐츠 유형을 결정한다.

return Response(data)

상태 코드

view에서 숫자 HTTP 상태 코드를 사용한다고 해서 항상 읽을 수 있는 것은 아니다. 그리고 잘못된 에러 코드가 잘못 되었는지 쉽게 알 수도 없다. DRF는 status 모듈의 HTTP_400_BAD_REQUEST와 같이 각 상태 코드에 대해 더욱 명시적인 식별자를 제공한다.

Wrapping API views

DRF는 API view를 작성하는 데 사용할 두 wrapper를 제공한다.

  1. FBV에서 사용되는 @api_view 데코레이터
  2. CBV에서 사용되는 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가 올바른 컨텐츠 유형으로 렌더링하도록 허용한다.

URL에 포맷 접미사 추가

이제 더 이상 하나의 컨텐츠 유형에만 국한되지 않는다는 이점을 살리기 위해 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 페이지같다.

Browsability

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/

profile
개발자로 크는 중

0개의 댓글