<학습 목표>
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)
이제 창이 제대로 뜬다 아래처럼 데이터를 넣어주고
데이터가 잘 저장돼서 화면에 뜨는 걸 확인할 수 있다
데이터베이스에 저장된 글
데이터베이스에도 글이 저장된 것을 확인할 수 있다
현재 모델에는 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 상태코드가 잘 출력되는 것을 확인할 수 있다
Article.objects.get(id=article_id) 사용 시 만날 수 있는 에러페이지
수정(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와 같다 유효성 검사 해주고 저장하고 화면 띄우기
아이디가 3인 게시글을 수정하려고 한다
수정이 완료되고, 업데이트시간 필드도 바뀐 것을 확인할 수 있다
데이터베이스에서도 수정된 것을 확인할 수 있다
지금 보니 DRF 화면상으로는 생성시간, 업데이트시간이 한국시간으로 맞게 나오는데 데이터베이스에는 한국시간으로 나오지 않는다
데이터베이스에도 한국시간으로 설정해주려면 settings.py에서 TIME_ZONE 아래 아래에 있는
USE_TZ = True
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을 불러와서 삭제 메서드를 쓰면 된다
DELETE 버튼을 누른 직후
글 목록
아이디가 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를 눌러준다
n번 게시글 가져오기
특정 게시글 보는 API도 위와 똑같이 해주면 된다
3번 게시글이 조회되는 것을 확인할 수 있다
저장 하는 거 잊지 말 것!
생성(POST)
게시글 만들기
환경, API 이름, 주소, 저장, send는 위와 같이 해주면 되고 method는 POST로 해준다
POST에서 잊지 말아야 할 것은 내용물을 Body -> raw 에서 작성하는데 JSON을 꼭 선택해준다(이미지에 체크하는 거 깜빡함...raw 옆에 쭉 보면 파란 글씨로 된 부분 JSON으로 바꿔주면 되는 거임 처음엔 JSON으로 안 되어있을 수 있음)
내용물 작성 후 send 눌러주면 아래처럼 게시글 작성된 것을 확인할 수 있다
저장도 잊지 않고 해준다
수정(PUT)
n번 게시글 수정하기
method PUT으로 하는 것 잊지 말고
게시글 생성과 마찬가지로 Body -> raw에 작성하는 거 잊지 말고
환경 local인지 잘 확인하고 저장도 잊지말고!
삭제(DELETE)
n번 게시글 삭제하기
method DELETE로 하고 삭제하고 싶은 게시글 아이디를 주소에 넣어 send한다
화면엔 뜨는 게 없지만 상태코드가 204 No Content인 것을 보아 삭제가 잘 됐다는 걸 확인할 수 있다
- swagger 적용해보기
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'),
...
]
http://127.0.0.1:8000/swagger/
근데 이 상태에서는 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로 바꿔보기
# 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)
# 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 형태로 변환한다
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
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를 이용해 게시글 제목들을 담는다 <- 데이터상으로만 추가된 것
프론트에 보이게 하려면 맨 마지막 줄을 추가한다
글 제목들이 프론트에 찍힌 것을 확인할 수 있음
이렇게 백엔드에 있는 데이터를 프론트로 불러옴