❗️ 기본적으로 클래스형으로 작성된 Django 문서를 기반으로 작성하였습니다.
💡 코드 동작을 원하시는 경우:
Django secret key가 환경변수로 지정되어있기 때문에
mac의 경우 터미널에서vi ~/.zshrc
명령어로 z shell에 들어간 후
가장 하단(찾기 편하게)에export HUMANSCAPE_SECRET_KEY="123abc"
를
넣어주시면 Postman 등으로 테스트해 보실 수 있습니다.
(POST 관련 API는 Swagger를 위해 급조한 부분으로 조금 이상할 수 있습니다!)
- Serialize(직렬화)
쿼리셋, 모델 인스턴스 등의 complex type(복잡한 데이터)을 JSON, XML등의
컨텐트 타입으로 쉽게 변환 가능한 python datatype으로 변환시켜줍니다.
Serializer는 우리가 Django에서 사용하는 파이썬 객체나 queryset 같은
복잡한 객체들을 REST API에서 사용할 json 과 같은 형태로 변환해주는
어댑터 역할을 한다.
🥕 출처
- drf-yasg
pip install drf-yasg
pip install rest_framework
- 의존성/호환 가능 Package
# 2021년 11월 16일 기준
Django Rest Framework: 3.10, 3.11, 3.12
Django: 2.2, 3.0, 3.1
Python: 3.6, 3.7, 3.8, 3.9
Model 및 DataBase를 생성해줍니다.
python manage.py makemigration
python manage.py migrate
프로젝트와 관련된 app을 생성하고 API를 작성합니다.
- settings.py(메인 app)
메인 app에 있는 settings.py 파일에 INSTALLED_APPS
부분을 확인합니다.
'django.contrib.auth'
부분이 주석처리 되었거나 삭제 되어있는 경우
추가해 줘야 합니다.
INSTALLED_APPS = [
...
'drf_yasg',
'rest_framework',
'django.contrib.auth'
]
- urls.py
from django.urls import path, include
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="프로젝트 이름(예: humanscape-project)",
default_version='프로젝트 버전(예: 1.1.1)',
description="해당 문서 설명(예: humanscape-project API 문서)",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="이메일"), # 부가정보
license=openapi.License(name="mit"), # 부가정보
),
public=True,
permission_classes=[permissions.AllowAny],
)
urlpatterns = [
path(r'swagger(?P<format>\.json|\.yaml)', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path(r'swagger', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path(r'redoc', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc-v1'),
# 이 아랫 부분은 우리가 사용하는 app들의 URL들을 넣습니다.
path('app이름', include('app이름.urls'))
]
openapi.Info
부분의 내용은 스웨거 창에 표시됩니다.
아직은 API 목록이 보이지 않습니다.
짧은 텀으로 실행되는 모습을 보는 것이 더 즐겁기도 하고
지금까지의 작업이 잘 되었는지 확인하는 것도 중요하니 실행시켜봅니다.
python manage.py runserver
로 서버를 실행시켜야 합니다.
상단의 링크는 localhost:8000으로 설정되었으며 자신에게 맞춰주시면 됩니다.
- views.py(메인app 외 app)
# swagger와 관련 없는 package들은 삭제하였습니다.
# from django.views import View 대신 사용할 패키지
from rest_framework.views import APIView
# 수정: class SearchView(View) -> class SearchView(APIView)
class SearchView(APIView):
def get(self, request):
...
return JsonResponse(result, status=200)
드디어 적용시킨 APP의 이름과 API 목록들이 보입니다!🕺
GET API의 경우 "해당 API"를 클릭 -> "Try it out" 클릭 ->
"Execute"를 클릭하면 리스트가 출력되는 것을 확인할 수 있습니다.
- views.py
# swagger와 관련 없는 package들은 삭제하였습니다.
from rest_framework.views import APIView
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
class TaskDetailView(APIView):
task_id = openapi.Parameter('task_id', openapi.IN_PATH, description='task_id path', required=True, type=openapi.TYPE_NUMBER)
@swagger_auto_schema(tags=['지정한 데이터의 상세 정보를 불러옵니다.'], manual_parameters=[task_id], responses={200: 'Success'})
def get(self, request, task_id):
...
Path Parameter는 API 함수 상단에 관련 내용을 작성 및 적용합니다.
(기타 여러 방법이 있는 것 같으나 개인적으로 이 방법이 가장 깔끔해 보입니다.)
parameter에 대한 설정들을 openapi.Parameter()에 작성하고 변수에 할당합니다.
그리고 적용할 parameter 및 기타 설정들을 @swagger_auto_schema()
데코레이터에 작성하여 해당 API에 붙여줍니다.
사실 아무런 내용을 추가하지 않고 url.py에 주소를
path('/<int:task_id>', TaskDetailView.as_view())
와 같이
적용시키기만 해도 path parameter를 입력할 수 있긴 합니다.
하지만 data type 지정과 같은 작업을 수행할 수 없고 string type으로
입력값이 자동 지정됩니다.
🥕 The swagger_auto_schema decorator
https://drf-yasg.readthedocs.io/en/stable/custom_spec.html#the-swagger-auto-schema-decorator
🥕 module-drf_yasg.openapi
https://drf-yasg.readthedocs.io/en/stable/drf_yasg.html?highlight=IN_QUERY#module-drf_yasg.openapi
위에서 지정한 옵션이 잘 적용되었다면 swagger 화면에는 다음과 같이
입력값의 type이 지정되어있는 등 옵션이 적용되어 있습니다.
Query Parameter와 body는 serializer를 작성해 줍니다.
Query의 경우 Path Parameter처럼 함수 위에 작성할 수 도 있지만
검색과 같이 받을 Query가 많아지면 해당 API가 아래와 같이 장황해집니다.
- views.py
class TaskSearchView(APIView):
offset = openapi.Parameter('offset', openapi.IN_QUERY, description='offset param', required=True, default=0, type=openapi.TYPE_INTEGER)
limit = openapi.Parameter('limit', openapi.IN_QUERY, description='limit param', required=True, default=10, type=openapi.TYPE_INTEGER)
trial_stage = openapi.Parameter('trial_stage', openapi.IN_QUERY, description='trial_stage param', required=False, type=openapi.TYPE_STRING)
department = openapi.Parameter('department', openapi.IN_QUERY, description='department param', required=False, type=openapi.TYPE_STRING)
institute = openapi.Parameter('institute', openapi.IN_QUERY, description='institute param', required=False, type=openapi.TYPE_STRING)
scope = openapi.Parameter('scope', openapi.IN_QUERY, description='scope param', required=False, type=openapi.TYPE_STRING)
title = openapi.Parameter('title', openapi.IN_QUERY, description='title param', required=False, type=openapi.TYPE_STRING)
type = openapi.Parameter('type', openapi.IN_QUERY, description='type param', required=False, type=openapi.TYPE_STRING)
@swagger_auto_schema(tags=['데이터를 검색합니다.'], manual_parameters=[offset, limit, title, department, institute, type, trial_stage, scope], responses={200: "Success"})
def get(self, request):
try:
offset = int(request.GET.get('offset', 0))
limit = int(request.GET.get('limit', 10))
title = request.GET.get('title', None)
department = request.GET.get('department', None)
institute = request.GET.get('institute', None)
type = request.GET.get('type', None)
trial_stage = request.GET.get('trial_stage', None)
scope = request.GET.get('scope', None)
...
# (이 아래 50줄 더!🥲)
딱 봐도 API 몇 개만 더 추가되도 스크롤 내리기 바쁠 것 같습니다.
그!래!서~~ serialier만 모아놓는 파일을 만들 것입니다.
관리하기 편하게 관련 views.py가 있는 app에 serializer.py 파일을 만들고
그 안에 코드들을 작성하시는 것이 좋습니다.
일단 위 API에서 어마어마했던 코드는 다음과 같이 간략해집니다.
- views.py
class TaskSearchView(APIView):
# query_serializer는 해당 serializer에서 설정한 내용을 swagger에서 인풋값으로 받을 수 있게 해줌
@swagger_auto_schema(tags=['데이터를 검색합니다.'], query_serializer=TaskSearchSerializer, responses={200: 'Success'})
def get(self, request):
...
그리고 query parameter와 직접적으로 관련된 코드들은 serializer 파일로
옮겨지고 코드도 변형됩니다.
- serializer.py
from rest_framework import serializers
# query param
class TaskSearchSerializer(serializers.Serializer):
offset = serializers.IntegerField(help_text='기준점 번호', default=0)
limit = serializers.IntegerField(help_text='출력할 row 갯수', default=10)
trial_stage = serializers.CharField(help_text='trial_stage param', required=False)
department = serializers.CharField(help_text='department param', required=False)
institute = serializers.CharField(help_text='institute param', required=False)
scope = serializers.CharField(help_text='scope param', required=False)
title = serializers.CharField(help_text='title param', required=False)
type = serializers.CharField(help_text='type param', required=False)
입력되어야 할 값들의 data type에 맞춰 field type을 지정해 주고
기타 옵션들을 설정해 줍니다.
required=False
를 지정해 주어야 필수 입력값을 요구하지 않습니다.
(오류인지 모르겠지만 offset, limit은 False값이 지정되어 있습니다.)
🥕 about required:
https://www.django-rest-framework.org/api-guide/fields/#required
body 역시 serializer에 작성해주면 됩니다.
API에 다음 코드를 추가합니다.
@swagger_auto_schema(tags=['데이터를 생성합니다.'], request_body=TaskPostSerializer)
- views.py
class TaskView(APIView):
@swagger_auto_schema(tags=['데이터를 생성합니다.'], request_body=TaskPostSerializer)
@transaction.atomic
def post(self, request):
try:
data = json.loads(request.body)
type = Type.objects.create(name = data['type'])
scope = Scope.objects.create(name = data['scope'])
institute = Institute.objects.create(name = data['institute'])
trial_stage = TrialStage.objects.create(name = data['trialStage'])
department = Department.objects.create(name = data['department'])
Task.objects.create(
number = data['number'],
title = data['title'],
duration = data['duration'],
number_of_target = data['number_of_target'],
department_id = department.id,
institute_id = institute.id,
type_id = type.id,
trial_stage_id = trial_stage.id,
scope_id = scope.id
)
...
위 예시코드의 경우 다음과 같이 serializer를 작성하시면 됩니다.
- serializer.py
class TaskPostSerializer(serializers.Serializer):
number = serializers.CharField(help_text='프로젝트 번호')
title = serializers.CharField(help_text='프로젝트 제목')
duration = serializers.CharField(help_text='연구 기간')
number_of_target = serializers.CharField(help_text='연구 번호')
department = serializers.CharField(help_text='의학 부서')
institute = serializers.CharField(help_text='연구 기관')
type = serializers.CharField(help_text='연구 형태')
trialStage = serializers.CharField(help_text='시행단계')
scope = serializers.CharField(help_text='연구 기관 범위')
위 화면처럼 serializer에서 설정한 목록들이 출력됩니다.
그리고 오른쪽 상단 "Try it out" 버튼을 클릭하면 아래와 같이 입력 가능한
인풋창으로 변형되며, 아래쪽 "Execute" 버튼을 누르면 POST가 실행됩니다.
이번에도 역시나 존시나 쉽지 않은 여정이었습니다.
처음에는 아무것도 모르기에 참고하는 자료마다 차이가 있었고
어떤 부분이 제가 적용하려는 프로젝트에 맞는지 몰라서 부딛히며
추려내느라 저의 정신은 여기저기 피멍이 들게 되었습니다.
그리고 drf-yasg와 rest_framework로 지금 제 상황에 꼭 필요한 부분을
추려낸 후 과정은 나름 수월했지만 무지에 의한 함정이었습니다.🥲
적용해볼 프로젝트에는 GET 요청만 있었고 몇가지 코드만 적용해도
Swagger가 호출되고 요청도 잘 불러와졌었는데..에...
결국 저는 POST는 어떻게 적용시키는지 궁금해졌고 또 GET 요청만
성공하고 마무리하는 것도 찝찝했기에 바로 알아보기 시작했습니다.
하.지.만. 아무리 검색해도 기존 GET에서 쓰던 방식으로는 적용되지 않았고
창의력 까지 발휘해서 어떻게든 기존 방법으로 해결해보려 했지만 무리.
사실 참고자료들이 srializer를 사용했었는데 serializer를 잘 모르기에
왠지 더 꼬일 것만 같은 예감이 들어서 피하기 위해 기존 GET방식으로
해결하려 했었던 것도 있습니다.
결론은 그냥 진작 해볼껄 이라는 생각과 잘 돌아가네 호호홓! 이었습니다.
물론 이해는 필요하지만 "이해가 꼭 선행되어야 하는 것은 아닙니다"라는
몇몇 저명하신 프로그래머님들의 말씀이 문득 생각이 듭니다.
일단 부딛히고 몸과 마음에 각인시키는 과정이 계속 되면서
천천히 대상에 대한 이해와 숙달이 함께 될거라는 생각이 듭니다.
우선 아예 시도해보지도 않는다던가 하는 일은 없어지니 더 좋고요.
오늘도 간신히 한걸음 내딛었네요!!
이제는 Nestjs를 배워보려 하는데 요 serializer 개념이 있는 것 같아
시작이 좋은 것 같습니다!^^🕺