<학습 목표>
- DRF로 프로젝트 세팅을 할 수 있다
- 시리얼라이저의 의미와 역할을 이해한다
- 시리얼라이저를 활용해서 CRUD를 할 수 있다
- 포스트맨으로 DRF 개발을 테스팅 할 수 있다
- 프로젝트에 Swagger를 적용할 수 있다
- 클래스형 뷰를 작성할 수 있다
- fetch api를 써서 프론트엔드에서 DRF의 데이터를 가져와서 나타낼 수 있다
<학습 내용>
- DRF로 프로젝트 세팅
- DRF를 사용하면 템플릿을 쓰지 않기 때문에 render를 import 할 필요가 없음
- 대신 Response를 import 해줌
- 데코레이터란?
- 함수 내부를 수정하지 않고 어떤 기능을 추가하고 싶을 때 사용함
- 데코레이터가 어떤 동작을 하는지 알 수 있는 기본 구조
def wrapper_function(func):
def decorated_function():
print("함수 이전에 실행")
func()
print("함수 이후에 실행")
return decorated_function
def basic_function():
print("실행하고자 하는 함수")
new_function = wrapper_function(basic_function)
new_function()
# 출력화면
# 함수 이전에 실행
# 실행하고자 하는 함수
# 함수 이후에 실행
- 위 코드에 데코레이터를 사용한다면
def wrapper_function(func):
def decorated_function():
print("함수 이전에 실행")
func()
print("함수 이후에 실행")
return decorated_function
@wrapper_function
def basic_function():
print("실행하고자 하는 함수")
basic_function()
# 출력화면
# 함수 이전에 실행
# 실행하고자 하는 함수
# 함수 이후에 실행
- @함수명 을 수정하지 않고 기능을 추가하고 싶은 함수(basic_function) 위에 붙이고 그 함수만 실행해도 wrapper_function이 실행됨
- 시리얼라이저란?
- 장고 프로젝트에서 내가 만든 모델에서 뽑은 Queryset(모델 인스턴스)을 JSON 타입으로 바꾸는 것
<실습>
- 모델 시리얼라이저 활용해보기
- url과 view가 이미 연결되어 있고, model이 생성되어 있다는 전제 하에 진행
조회(GET) - index
- articles 앱에 serializers.py 파일을 생성함
# articles/serializers.py
from rest_framework import serializers
from articles.models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = "__all__"
- Article 모델을 모델로 하는 ArticleSerializer 클래스를 생성함
- 필드는 Article 모델 필드 전부를 가져옴
# articles/views.py
from rest_framework.response import Response
from rest_framework.decorators import api_view
from articles.models import Article
from articles.serializers import ArticleSerializer
# Create your views here.
@api_view(['GET'])
def index(request):
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)
- 필요한 것들을 import 해줌
- articles 라는 변수에 Article 모델의 데이터 쿼리셋이 담겨있음
- serializer 라는 변수에 그냥 ArticleSerializer(articles)만 넣으면 시리얼라이저가 쿼리셋에서 title 필드를 찾지 못한다는(?) 에러가 뜸
- ArticleSerializer에 매개변수로 many=True를 추가해줘야 시리얼라이저가 쿼리셋을 리스트 형태로 가져옴
- 또한 시리얼라이저는 딕셔너리 형태로 데이터 외에도 다른 게 담겨있어서(맞는 말인가...? 아무튼) 그냥 return Response(serializer) 하면 아래와 같은 에러가 뜸
- rerurn Response(serializer.data)와 같이 data까지 접근해주면 에러가 해결됨
생성(POST) - index
- 시리얼라이저를 이용해 글 작성해보기
# articles/serializers.py
@api_view(['GET', 'POST'])
def index(request):
if request.method == 'GET':
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = ArticleSerializer(request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
print(serializer.errors)
return Response(serializer.errors)
- 먼저 강의를 쭉 보고 기억을 되살려 코드를 작성했다
- 맨 위 @api_view에 'POST'를 추가하고, 위에서 썼던 GET 요청일 때 실행하는 코드 바깥에 if문을 추가했다
- POST 요청일 경우 serializer 변수에 ArticleSerializer를 불러와서 매개변수로는 request.data, 즉 사용자가 직접 적은 내용을 넣어준다
- 시리얼라이저를 저장하기 전에는 is_valid()를 사용해 데이터가 유효한 값인지 검증을 해야 한다
- 데이터가 유효하다면(문제가 없다면) 시리얼라이저를 저장하고 그 데이터를 화면에 띄운다
- 데이터에 문제가 있다면 에러코드를 출력한다
- 단, return 값에 에러를 출력하는 것은 실무에서는 사용하면 안 된다 클라이언트에 에러를 다 보여주는 것은 보안상 좋지 않다고 한다
- 개발 단계에서는 편의성을 위해 넣어놔도 되지만 실제 배포할 때는 프론트에 에러가 그대로 출력되지 않도록 주의해야 한다
- 위와 같이 코드를 작성하고 실행하니 아래와 같은 에러가 떴다
- 강의에서도 같은 에러가 떴었는데 ㅋㅋㅋ까먹고 똑같이 썼다
- is_valid()에 매개변수로 data=request.data를 넣어줘야 한다 그래야 데이터를 가져와서 검증을 할 거 아닌가!
if serializer.is_valid(data=request.data):
- 이렇게 수정 후 저장하니
- 또 에러가 뜬다 ㅋㅋㅋ data=request.data 위치가 잘못된 것 같다 근데 어디에 둬야 하는지 기억이 안 나서 강의를 다시 돌려봤다
- 첫 번째 에러에 시리얼라이저 인스턴스라고 써있었는데....바보같이 is_vaild에 넣다니
- 아래처럼 시리얼라이저에 넣어줘야 한다
serializer = ArticleSerializer(data=request.data)
- 이제 창이 제대로 뜬다 아래처럼 데이터를 넣어주고
- POST를 눌러주면
- 데이터가 잘 저장돼서 화면에 뜨는 걸 확인할 수 있다
- 데이터베이스에도 글이 저장된 것을 확인할 수 있다
- 현재 모델에는 content가 빈칸을 허용할 수 없게끔 되어 있어서 일부러 에러를 유발시켜보면
- 화면이 이렇게 뜨는데 위에 HTTP 상태코드가 200 OK로 나온다
- 현재 상태에 맞는 코드가 뜨게끔 바꿔줘야 한다
from rest_framework import status
...
# 게시글 작성에 성공한 경우
return Response(serializer.data, status=status.HTTP_201_CREATED)
...
# 실패한 경우
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- status를 import하고 리턴값에 위와 같이 추가해줬다 status. 까지 하면 HTTP 상태코드들이 쭉 뜬다 상황에 맞는 걸 골라서 쓰면 된다
조회(GET) - article_view
- 게시글 상세페이지를 조회한다
- 먼저 url을 추가해준다
# articles/urls.py
...
urlpatterns = [
...
path('<int:article_id>/', views.articleDetailAPI, name='article_view'),
]
- 주소창에 http://127.0.0.1:8000/articles/1/ 을 치면 게시글 아이디가 1인 게시글이 보일 수 있게끔 url을 설정했다
- 다음은 article_view 함수를 만든다 article_view 함수에서 상세페이지 조회, 게시글 수정 및 삭제를 구현할 예정이다
# articles/views.py
...
from rest_framework.generics import get_object_or_404
...
@api_view(['GET', 'PUT', 'DELETE'])
def article_view(request, article_id):
if request.method == 'GET':
article = get_object_or_404(Article, id=article_id)
serializer = ArticleSerializer(article)
return Response(serializer.data)
- 먼저 조회, article 변수에 원래 하던대로 Article.objects.get(id=article_id)로 쓰면 http://127.0.0.1:8000/articles/1215/ 이런 식으로 아직 생성되지 않은 게시글 아이디로 접근했을 때 에러페이지가 나온다
- 이를 방지하기 위해 get_object_or_404()를 써준다
- 게시글 상세페이지가 잘 나오는 것을 확인할 수 있다
- 존재하지 않는 게시글 아이디로 접근했을 때 에러 메시지와 함께 404 상태코드가 잘 출력되는 것을 확인할 수 있다
수정(PUT) - article_view
- request.method가 PUT일 때 실행할 코드를 작성한다
# articles/view.py
...
@api_view(['GET', 'PUT', 'DELETE'])
def article_view(request, article_id):
...
elif request.method == 'PUT':
article = get_object_or_404(Article, id=article_id)
serializer = ArticleSerializer(article, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
- PUT은 GET과 POST의 짬뽕이다
- 먼저 article 변수에 해당 게시글 데이터를 가져오고 serializer 변수로 쿼리셋 데이터를 JSON 데이터로 변환시켜준다
- 단, 기존 게시글 내용이 프론트에 뜨게끔 ArticleSerializer 매개변수로 article 변수를 추가한다
- 나머지는 POST와 같다 유효성 검사 해주고 저장하고 화면 띄우기
- 지금 보니 DRF 화면상으로는 생성시간, 업데이트시간이 한국시간으로 맞게 나오는데 데이터베이스에는 한국시간으로 나오지 않는다
- 데이터베이스에도 한국시간으로 설정해주려면 settings.py에서 TIME_ZONE 아래 아래에 있는
USE_TZ = True
- 이 코드를 False로 바꿔주면 된다
USE_TZ = False
- 이미 생성된 게시글의 시간은 바뀌지 않고 게시글을 새로 작성해보면 데이터베이스에도 한국시간을 기준으로 데이터가 생성되는 것을 확인할 수 있다
삭제(DELETE) - article_view
- 삭제는 쉽다 시리얼라이저도 필요없다
# articles/view.py
...
@api_view(['GET', 'PUT', 'DELETE'])
def article_view(request, article_id):
...
elif request.method == 'DELETE':
article = get_object_or_404(Article, id=article_id)
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
- 그냥 article을 불러와서 삭제 메서드를 쓰면 된다
- 아이디가 1인 게시글이 삭제된 것을 확인할 수 있다
- 포스트맨으로 API 확인해보기
환경설정
- 현재 앞에 붙는 기본 주소(http://127.0.0.1:8000)를 변수에 담아서 관리하는 게 API를 추가할 때나 나중에 배포했을 때 편함
- 어떤 사유로 8000번 포트가 아니라 8001번 포트를 쓰게 됐을 때 이 기능을 쓰지 않으면 API 주소를 일일이 다 바꿔야 하는데 이 기능을 쓰면 아래에서 이니셜 밸류와 커런트밸류를 바꿔주면 끝난다
- Environments에 가서 + 버튼을 눌러 local과 deploy를 생성한다
- local은 배포 전 작업환경에서 쓸 거고 deploy는 나중에 배포하고 새로운 도메인이 생기면 쓸 예정
- local과 deploy 변수명을 host로 일치시켜주면 나중에 환경만 local에서 deploy로 바꾸면 된다
- 이니셜밸류와 커런트밸류에 현재 주소를 넣어준다
- 오른쪽 위에 환경을 local로 두는 것을 잊지 말아야 한다
- 주소창에 http://127.0.0.1:8000을 지우고 {{host}}를 써주면 끝
- 다른 API 주소창에도 http://127.0.0.1:8000을 {{host}}로 변경해준다
조회(GET)
- 먼저 오른쪽 위에 환경이 local로 되어 있는지 확인하고 API 이름을 작성하고, method를 선택한다
- 주소창에 알맞은 주소를 입력하고 send를 눌러서 정보가 맞게 들어오는지 확인한다
- 잊지않고 save를 눌러준다
- 특정 게시글 보는 API도 위와 똑같이 해주면 된다
- 3번 게시글이 조회되는 것을 확인할 수 있다
- 저장 하는 거 잊지 말 것!
생성(POST)
- 환경, API 이름, 주소, 저장, send는 위와 같이 해주면 되고 method는 POST로 해준다
- POST에서 잊지 말아야 할 것은 내용물을 Body -> raw 에서 작성하는데 JSON을 꼭 선택해준다(이미지에 체크하는 거 깜빡함...raw 옆에 쭉 보면 파란 글씨로 된 부분 JSON으로 바꿔주면 되는 거임 처음엔 JSON으로 안 되어있을 수 있음)
- 내용물 작성 후 send 눌러주면 아래처럼 게시글 작성된 것을 확인할 수 있다
- 저장도 잊지 않고 해준다
수정(PUT)
- method PUT으로 하는 것 잊지 말고
- 게시글 생성과 마찬가지로 Body -> raw에 작성하는 거 잊지 말고
- 환경 local인지 잘 확인하고 저장도 잊지말고!
삭제(DELETE)
- method DELETE로 하고 삭제하고 싶은 게시글 아이디를 주소에 넣어 send한다
- 화면엔 뜨는 게 없지만 상태코드가 204 No Content인 것을 보아 삭제가 잘 됐다는 걸 확인할 수 있다
- swagger 적용해보기
- https://drf-yasg.readthedocs.io/en/stable/readme.html
- 위 사이트에서 적용 방법을 참고하면 된다
pip install drf-yasg
- swagger를 쓸 수 있는 패키지를 설치하고
pip freeze > requirements.txt
- 새로운 패키지 설치 시 위 명령어 잊지 말 것!
# settings.py
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'articles',
'drf_yasg', # 추가!
'rest_framework',
]
...
- 공식 사이트에 'django.contrib.staticfiles' 도 추가하라고 나와있는데 이미 추가가 되어 있어서 'drf_yasg'만 추가함
# drf_prac/urls.py
...
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
...
schema_view = get_schema_view(
openapi.Info(
title="Snippets API",
default_version='v1',
description="Test description",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@snippets.local"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=[permissions.AllowAny],
)
urlpatterns = [
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
...
]
- 위치에 맞게 개별적으로 추가하기!
- 근데 이 상태에서는 post나 put의 경우 내용을 입력할 수 없게 되어 있다
- 데코레이터를 씌워야 한다
# articles/views.py
...
from drf_yasg.utils import swagger_auto_schema
...
- swagger_auto_schema를 import 해준다
# articles/views.py
...
class ArticleList(APIView):
...
@swagger_auto_schema(request_body=ArticleSerializer)
def post(self, request, format=None):
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
print(serializer.errors)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ArticleDetail(APIView):
...
@swagger_auto_schema(request_body=ArticleSerializer)
def put(self, request, article_id, format=None):
article = get_object_or_404(Article, id=article_id)
serializer = ArticleSerializer(article, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
...
- 위 코드와 같이 입력이 필요한 메서드 위에 @swagger_auto_schema(request_body=ArticleSerializer)를 붙여준다
- 다시 http://127.0.0.1:8000/swagger/ 로 가서 보면
- 이렇게 입력해야 하는 값들이 있고 Try it out을 누르면
- 게시글 내용을 채울 수 있게 바뀐다 내용을 채우고 Execute를 누르면
- 클래스형 view로 바꿔보기
- https://www.django-rest-framework.org/tutorial/3-class-based-views/
- 위 사이트에서 예시를 원래 함수 아래 붙여넣고 비교하며 바꿔본다
# articles/views.py
...
from rest_framework.views import APIView
...
@api_view(['GET', 'POST'])
def articleAPI(request):
if request.method == 'GET':
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
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)
else:
print(serializer.errors)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class SnippetList(APIView):
"""
List all snippets, or create a new snippet.
"""
def get(self, request, format=None):
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = SnippetSerializer(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)
- get과 post인 경우 실행할 코드를 그대로 갖다 붙이면 되겠다
# articles/views.py
...
class ArticleList(APIView):
def get(self, request, format=None):
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
print(serializer.errors)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- urls.py도 수정해준다
# articles/urls.py
...
urlpatterns = [
path('', views.ArticleList.as_view(), name='index'),
...
]
- 클래스형뷰를 넣을 때는 뒤에 .as_view()를 붙여줘야 한다
- 포스트맨으로 테스트 했을 때 잘 돌아가는 것을 확인할 수 있다
- articleDetailAPI 함수도 똑같이 바꿔준다
# articles/views.py
...
class ArticleDetail(APIView):
def get(self, request, article_id, format=None):
article = get_object_or_404(Article, id=article_id)
serializer = ArticleSerializer(article)
return Response(serializer.data)
def put(self, request, article_id, format=None):
article = get_object_or_404(Article, id=article_id)
serializer = ArticleSerializer(article, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, article_id, format=None):
article = get_object_or_404(Article, id=article_id)
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
# articles/urls.py
...
urlpatterns = [
...
path('<int:article_id>/', views.ArticleDetail.as_view(), name='article_view'),
]
- 간단..!
- CBV가 FBV에 비해 뭐가 좋은지는 다음 시간에...
<예제>
- 프론트엔드 만들어보기
폴더 및 index 파일 생성
- drf_prac_front 라는 이름의 폴더를 생성하고 그 안에 index.html과 index.js 파일을 생성한다
- index.html에서 ! + Tab 을 해서 기본 환경을 펼쳐놓고(?) <head> 태그 안에
<!-- index.html -->
...
<head>
...
<script src="index.js"></script>
...
</head>
...
- 위 문장을 넣어서 html과 js 파일을 연결해준다
index.js 코드 짜기
- 먼저 아래 코드를 작성한다
// index.js
window.onload = async function loadArticles() {
const response = await fetch('http://127.0.0.1:8000/articles/', { method: 'GET' })
response_json = await response.json()
console.log(response_json)
}
- window.onload는 처음 페이지 접속해서 로딩하면 나올 것을 담아준다
- async function은 잘 모르지만 쨌든 백엔드에 있는 Articles 앱을 불러온다
- const는 변하지 않는 값을 선언할 때 쓰는 js 용어? 문법?이다
- 상수 response에 저 주소로 가서 GET 요청일 때의 값을 받아서 담아준다
- 받아온 response를 json 형태로 변환한다
- 그대로 실행하면 위와 같은 에러가 뜬다
- 같은 주소끼리 통신해야 하는데 주소가 달라서 보안상의 에러가 뜬다
- CORS 공식 문서: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- 지금 빨간 네모박스처럼 Cross-Origin_Opener_Policy가 same-origin으로 되어 있어서 안 되는 것
- 다른 주소로 통신할 수 있게 허용해줘야 함
- 장고 CORS 참고 사이트 : https://github.com/adamchainz/django-cors-headers
- 백엔드로 가서 작업창으로 가서
python -m pip install django-cors-headers
- django-cors-headers를 설치해준다
- settings.py의 INSTALLED_APPS와 MIDDLEWARE에 아래와 같이 추가해준다
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
...
]
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware', # 이걸
'django.middleware.common.CommonMiddleware', # 이 문장 위에 추가해준다 이 문장은 원래 있음
... # 이 순서가 중요함!
]
# settings.py 맨 아래에
CORS_ALLOW_ALL_ORIGINS = True # 이것도 추가해줌
- 이제 index.html 에서 바디 태그 안에 articles 라는 id를 가진 div를 만들어준다
<!-- index.html -->
...
<body>
...
<div id="articles"></div>
...
</body>
- 다시 index.js로 가서
// index.js
window.onload = async function loadArticles() {
...
const articles = document.getElementById("articles")
response_json.forEach(element => {
console.log(element.title)
})
}
- articles 라는 상수에 html에서 "articles"라는 id를 가진 요소를 담음
- response_json을 하나씩 for문으로 돌리면서 요소의 타이틀만 찍음
// index.js
window.onload = async function loadArticles() {
...
const articles = document.getElementById("articles")
response_json.forEach(element => {
console.log(element.title)
const newArticle = document.createElement("div")
newArticle.innerText = element.title
articles.appendChild(newArticle)
})
}
- newArticle이라는 상수에 index.html에 div를 만들어 담는다
- newArticle에 innerText를 이용해 게시글 제목들을 담는다 <- 데이터상으로만 추가된 것
- 프론트에 보이게 하려면 맨 마지막 줄을 추가한다
- 이렇게 백엔드에 있는 데이터를 프론트로 불러옴