HTML을 돌려주는 View와 JSON을 돌려주는 API
지금까지 만든 게시판은 브라우저가 HTML 페이지를 요청하는 구조였다. 사용자가 /posts/에 접속하면 View가 게시글을 조회하고, Template에 데이터를 전달하고, 최종 HTML을 응답했다. 글 작성도 HTML 폼을 보여 준 뒤 POST 요청을 받고, 저장 후 다른 페이지로 redirect했다.
API는 응답의 중심이 다르다. API를 호출하는 쪽은 꼭 브라우저 화면이 아니다. React 같은 프론트엔드, 모바일 앱, 다른 서버일 수 있다. 이 클라이언트들은 완성된 HTML보다 가공하기 쉬운 데이터가 필요하다. 그래서 API View는 보통 JSON 형식의 요청을 받고, JSON으로 해석할 수 있는 응답을 돌려준다.
전통적인 Django View의 흐름은 대략 이렇다.
return render(request, 'boards/post_detail.html', {'post': post})API View의 흐름은 다음에 가깝다.
return Response(serializer.data)두 코드 모두 요청을 처리하고 응답을 만든다는 점은 같다. 차이는 응답의 형식이다. HTML View는 Template을 거쳐 화면용 HTML을 만든다. API View는 Serializer를 거쳐 Python 객체를 JSON으로 표현할 수 있는 데이터로 바꾸고, DRF가 그 데이터를 적절한 응답 형식으로 렌더링한다. Django REST Framework, 줄여서 DRF는 이 과정을 쉽게 만들기 위한 Django 확장 도구다.
설치는 간단하다.
pip install djangorestframework그리고 settings.py에 앱을 등록한다.
INSTALLED_APPS = [
# ...
'rest_framework',
]토큰 인증까지 사용할 예정이라면 뒤에서 사용할 앱도 함께 등록한다.
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework.authtoken',
]rest_framework.authtoken을 추가했다면 토큰 저장 테이블을 만들기 위해 마이그레이션을 실행해야 한다.
python manage.py migrate
AI 생성 이미지
Serializer는 API의 Form이다
Django의 HTML 폼에서 Form과 ModelForm이 입력값 검증과 저장을 맡았다면, API에서는 Serializer가 비슷한 역할을 한다. Serializer는 모델 객체나 QuerySet 같은 복잡한 데이터를 JSON으로 바꾸기 쉬운 Python 기본 데이터로 변환하고, 반대로 요청으로 들어온 데이터를 검증한 뒤 모델 객체로 저장할 수 있게 해 준다.1
게시글 모델이 다음과 같이 있다고 가정하자.
# boards/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='posts',
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title이 모델을 API로 노출하려면 serializers.py를 만든다.
# boards/serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
author_username = serializers.ReadOnlyField(source='author.username')
class Meta:
model = Post
fields = [
'id',
'title',
'content',
'author',
'author_username',
'created_at',
'updated_at',
]
read_only_fields = [
'id',
'author',
'created_at',
'updated_at',
]
def validate_title(self, value):
if len(value.strip()) < 2:
raise serializers.ValidationError('제목은 두 글자 이상이어야 합니다.')
return valueModelSerializer는 ModelForm과 닮았다. model과 fields를 지정하면 모델 필드를 기반으로 Serializer 필드를 만든다. read_only_fields에 들어간 필드는 클라이언트가 직접 보낼 수 없다. 예를 들어 author는 요청 데이터에서 받지 않는다. 글 작성자는 서버가 request.user로 정해야 한다. 클라이언트가 임의로 다른 사용자의 id를 보내 작성자를 바꾸면 안 되기 때문이다.
author_username은 저장용 필드가 아니라 응답에 보여 주기 위한 읽기 전용 필드다. 실제 데이터베이스에는 author 외래키가 저장되지만, API 응답에서 작성자의 숫자 id만 보이면 불편할 수 있다. 그래서 author.username 값을 함께 내려준다.
Serializer를 사용하는 기본 형태는 두 가지다.
# 객체를 응답 데이터로 바꿀 때
serializer = PostSerializer(post)
serializer.data
# 요청 데이터를 검증하고 저장할 때
serializer = PostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
post = serializer.save(author=request.user)여러 게시글을 한 번에 응답하려면 many=True를 붙인다.
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)APIView로 요청과 응답 흐름 보기
먼저 DRF의 APIView로 목록 조회와 생성을 만들어 보자. APIView는 Django의 클래스 기반 View와 비슷하지만, request.POST 대신 request.data를 사용하고, HttpResponse나 render() 대신 Response를 사용한다.
# boards/api_views.py
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
class PostListCreateAPIView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
posts = Post.objects.select_related('author').order_by('-created_at')
serializer = PostSerializer(posts, many=True)
return Response(serializer.data)
def post(self, request):
serializer = PostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)get()은 게시글 목록을 JSON 응답으로 돌려준다. post()는 JSON 요청 데이터를 검증한 뒤 새 게시글을 만든다. is_valid(raise_exception=True)는 검증 실패 시 자동으로 400 Bad Request 응답을 만든다. HTML 폼에서 form.errors를 템플릿에 보여 주던 것과 달리, API에서는 오류 내용도 JSON으로 응답한다.
URL은 일반 View와 똑같이 연결한다.
# boards/api_urls.py
from django.urls import path
from .api_views import PostListCreateAPIView
urlpatterns = [
path('posts/', PostListCreateAPIView.as_view(), name='api-post-list'),
]프로젝트 URL에서 API 경로를 분리해 두면 HTML 화면과 API를 함께 운영하기 쉽다.
# config/urls.py
from django.urls import include, path
urlpatterns = [
path('api/', include('boards.api_urls')),
]이제 GET /api/posts/는 게시글 목록을 돌려주고, POST /api/posts/는 새 게시글을 만든다. 여기까지는 View가 요청 방식을 직접 나누어 처리한다는 점에서 이해하기 쉽다.
ViewSet과 Router로 CRUD 엔드포인트 만들기
게시판 API에는 목록, 생성, 상세, 수정, 삭제가 모두 필요하다. APIView만으로도 만들 수 있지만, 비슷한 코드가 반복된다. DRF의 ViewSet은 관련된 API 동작을 하나의 클래스에 모으는 방식이다. 특히 ModelViewSet은 목록, 생성, 상세, 전체 수정, 부분 수정, 삭제에 해당하는 기본 동작을 제공한다.2
boards/api_views.py를 다음처럼 바꿀 수 있다.
# boards/api_views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related('author').order_by('-created_at')
serializer_class = PostSerializer
permission_classes = [IsAuthenticated]
def perform_create(self, serializer):
serializer.save(author=self.request.user)코드는 훨씬 짧지만 기능은 많다. ModelViewSet은 내부적으로 다음 동작을 처리한다.
| 동작 | HTTP 메서드 | URL 예시 | 의미 |
|---|---|---|---|
| 목록 | GET | /api/posts/ | 게시글 목록 조회 |
| 생성 | POST | /api/posts/ | 게시글 생성 |
| 상세 | GET | /api/posts/1/ | 1번 게시글 조회 |
| 전체 수정 | PUT | /api/posts/1/ | 1번 게시글 전체 수정 |
| 부분 수정 | PATCH | /api/posts/1/ | 1번 게시글 일부 수정 |
| 삭제 | DELETE | /api/posts/1/ | 1번 게시글 삭제 |
perform_create()는 생성 직전에 끼어드는 메서드다. 여기서 author=self.request.user를 넣어 작성자를 현재 로그인 사용자로 저장한다. HTML 게시판에서 form_valid() 안에서 작성자를 지정했던 흐름과 같은 목적이다.
Router를 쓰면 URL도 자동으로 만들 수 있다.
# boards/api_urls.py
from rest_framework.routers import DefaultRouter
from .api_views import PostViewSet
router = DefaultRouter()
router.register('posts', PostViewSet, basename='post')
urlpatterns = router.urlsRouter에 posts를 등록하면 목록 URL과 상세 URL이 자동으로 생긴다. 직접 path('posts/<int:pk>/')를 모두 쓰지 않아도 된다. API가 많아질수록 Router의 장점이 커진다.
권한은 View와 객체 단위로 나누어 생각한다
permission_classes = [IsAuthenticated]는 로그인한 사용자만 API를 호출할 수 있게 한다. DRF의 권한 검사는 먼저 View에 들어오는 요청 자체에 대해 실행된다. 지정한 권한 클래스는 모두 통과해야 하며, 하나라도 실패하면 View 로직은 실행되지 않는다.3
하지만 로그인한 사용자라면 누구나 모든 글을 수정하거나 삭제해도 된다는 뜻은 아니다. 게시판에서는 보통 작성자만 자신의 글을 수정하거나 삭제할 수 있어야 한다. 이럴 때는 객체 단위 권한을 추가한다.
# boards/permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsAuthorOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.author == request.userSAFE_METHODS는 GET, HEAD, OPTIONS처럼 데이터를 바꾸지 않는 요청을 의미한다. 조회는 허용하고, 수정·삭제는 작성자에게만 허용한다. 이런 객체 단위 권한은 상세 조회, 수정, 삭제처럼 특정 객체를 가져온 뒤에 판단해야 하는 동작에서 특히 중요하다.
이 권한을 ViewSet에 적용한다.
# boards/api_views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Post
from .permissions import IsAuthorOrReadOnly
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related('author').order_by('-created_at')
serializer_class = PostSerializer
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)이제 모든 API 요청은 인증된 사용자에게만 허용된다. 그중 수정과 삭제는 작성자에게만 허용된다. 인증과 권한은 함께 생각해야 한다. 인증은 사용자가 누구인지 확인하는 일이고, 권한은 그 사용자가 이 행동을 해도 되는지 판단하는 일이다.
만약 게시글 조회는 누구에게나 열어 두고, 작성·수정·삭제만 로그인 사용자에게 제한하고 싶다면 IsAuthenticated 대신 공개 읽기 정책을 따로 적용해야 한다. 중요한 것은 "누가 읽을 수 있는가"와 "누가 바꿀 수 있는가"를 API마다 분명히 정하는 것이다.
세션 인증과 토큰 인증
DRF 인증은 요청이 View에 들어오는 초반에 실행되어 request.user와 request.auth를 설정한다. 인증 자체는 사용자를 식별하는 과정이고, 실제 허용 여부는 권한 클래스가 결정한다.4
같은 Django 사이트 안에서 JavaScript로 API를 호출한다면 세션 인증을 사용할 수 있다. 사용자가 Django 로그인 화면에서 로그인하면 브라우저는 세션 쿠키를 가지고 있고, API 요청도 그 세션을 이용한다.
모바일 앱이나 외부 클라이언트에서는 토큰 인증이 단순하다. 사용자가 아이디와 비밀번호로 토큰을 발급받고, 이후 요청마다 토큰을 HTTP 헤더에 담아 보낸다.
전역 설정은 다음처럼 둘 수 있다.
# config/settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}토큰 발급 URL도 추가한다.
# config/urls.py
from django.urls import include, path
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
path('api/', include('boards.api_urls')),
path('api-token-auth/', obtain_auth_token),
]클라이언트는 다음 주소로 사용자 이름과 비밀번호를 POST 요청으로 보낸다.
POST /api-token-auth/성공하면 응답으로 토큰 값을 받는다. 이후 API를 호출할 때는 헤더에 토큰을 넣는다.
curl -X GET http://127.0.0.1:8000/api/posts/ \
-H 'Authorization: Token 여기에_토큰값'세션 인증을 사용할 때 POST, PUT, PATCH, DELETE 같은 요청은 CSRF 토큰 처리가 필요하다. 반면 토큰 인증은 보통 쿠키 대신 Authorization 헤더를 사용한다. 운영 환경에서 토큰 인증을 쓴다면 반드시 HTTPS를 사용해야 한다. 토큰은 비밀번호처럼 다루어야 한다.
API를 만들 때 확인할 것
API는 화면이 아니라 계약에 가깝다. 클라이언트는 정해진 URL, HTTP 메서드, 요청 형식, 응답 형식을 믿고 기능을 만든다. 그래서 다음 사항을 일관되게 유지하는 것이 중요하다.
목록과 상세 URL을 구분한다.
생성은 POST, 전체 수정은 PUT, 일부 수정은 PATCH, 삭제는 DELETE를 사용한다.
성공과 실패에 맞는 HTTP 상태 코드를 돌려준다.
클라이언트가 보내면 안 되는 필드는
read_only_fields로 막는다.작성자 같은 중요한 값은
request.user로 서버에서 정한다.인증과 권한을 ViewSet에 명시한다.
관련 객체를 함께 응답한다면
select_related()와prefetch_related()를 고려한다.
Django는 HTML 웹사이트를 만드는 데서 끝나지 않는다. 같은 Model과 인증 시스템을 바탕으로 JSON API 서버로 확장할 수 있다. Template이 화면을 만든다면, Serializer는 데이터를 API 응답으로 정리한다. ViewSet과 Router는 반복되는 CRUD API를 일정한 구조로 묶어 준다. 이 구조를 이해하면 하나의 Django 프로젝트에서 관리자 페이지, HTML 화면, 모바일 앱용 API를 함께 설계할 수 있다.
참고
1Serializers - Django REST framework
2Viewsets - Django REST framework
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.