[udemy] Build REST APIs with Django REST Framework and Python 학습
* 이미 알고있는 지식 외, 필요한 부분만 요약 정리함
1. Validation
- serializers에서의 validation은 3가지 방법이 있다.
1) 하나의 필드를 대상으로 한 내장 함수
- validate_***
class MovieSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField()
def validate_name(self, value):
if len(value) < 2:
raise serializers.ValidationError("Name is too short!")
else:
return value
2) 전체 필드를 대상으로 한 내장 함수
- validate
class MovieSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField()
description = serializers.CharField()
def validate(self, data):
id = data["id"]
name = data["name"]
description = data["description"]
if data["name"] == data["description"]:
raise serializers.ValidationError(
"Title and Description should be different!"
)
else:
return data
3) 필드 자체에 validators 속성 추가
def name_length(value):
if len(value) < 2:
raise serializers.ValidationError("Name is too short!")
class MovieSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(validators=[name_length])
* 필드 자체에 validators 혹은 에러 메시지를 넣고자 한다면, form이나 serializers의 필드보다 model에 넣는게 적합하다.
* 모델 필드에 에러 메시지 넣는 방법
- field_name = models.Field(error_messages = {"key": "message"})
class GeeksModel(Model):
geeks_field = models.CharField(
max_length = 200,
unique = True
)
- reference :
https://docs.djangoproject.com/en/4.2/ref/validators/
2. SerializerMethodField
- 읽기 전용 필드로 모델에 벗는 필드를 추가하고 싶거나, 모델에 있는 값을 변형, 특정 함수 수행 후의 결과 값을 새로운 필드의 값으로 넣고 싶을 때 사용한다.
Class UserSerializer(serializers.Serializer):
full_name = JSONField
first_name = serializers.SerializerMethodField('get_first_name')
# first_name = serializers.SerializerMethodField(method_name = 'get_first_name')
def get_first_name(self, obj): # 객체를 인자로 받음
return obj.full_name['first_name'] # 유저 객체의 full_name 속성에서 first_name 추출
- reference :
https://leffept.tistory.com/319
https://velog.io/@oen/SerializerMethodField-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EB%B2%88%EC%97%AD
https://eunjin3786.tistory.com/268
3. Nested relationships
- 다른 곳에 정의된 시리얼라이저를 필드로 불러올 수 있다.
- 필드가 다대다 관계인 경우 필드에 Many=True 플래그를 추가해야 한다.
- 필드 명은 모델에서 정의된 related_name 혹은 외래키 필드와 체이닝을 사용할 수 있다.
- xxx_set 형식의 참조명을 사용할 수 있는지 여부는 확인이 필요함
class WatchListSerializer(serializers.ModelSerializer):
# reviews = ReviewSerializer(many=True, read_only=True)
platform = serializers.CharField(source="platform.name")
class Meta:
model = WatchList
fields = "__all__"
class StreamPlatformSerializer(serializers.ModelSerializer):
watchlist = WatchListSerializer(many=True, read_only=True)
class Meta:
model = StreamPlatform
fields = "__all__"
- serializers.StringRelatedField(many=True)
- 모델의 def __str__(self):에 선언된 필드로 출력된다.
def __str__(self):
return self.title
- serializers.PrimaryKeyRelatedField(many=True, read_only=True)
- pk 숫자로 출력된다.
- serializers.HyperlinkedRelatedField(many=True,read_only=True,view_name="movie-details",)
- 각 컨텐츠와 연관된 하이퍼링크가 출력된다.
- view_name에는 urls에 선언된 해당 url의 name을 입력한다.
- view에서 해당 시리얼라이즈를 호출할 때 context에 request를 넣어줘야 한다.
- views
serializer = StreamPlatformSerializer(
platform, many=True, context={"request": request}
)
- serializers
watchlist = serializers.HyperlinkedRelatedField(
many=True,
read_only=True,
view_name="movie-details",
)
- urls
path("<int:pk>/", WatchDetailAV.as_view(), name="movie-details"),
- 여러개의 Nested relationships field를 사용하고 싶다면 해당 필드의 플래그에 source를 정의한다
- source의 값은 모델에서 정의된 related_name 혹은 외래키 필드와 체이닝을 사용한다.
- reference :
https://www.django-rest-framework.org/api-guide/relations/#nested-relationships
4. HyperLinked Model Serializer
- HyperlinkedModelSerializer 클래스는 하이퍼링크를 사용하여 기본 키가 아닌 관계를 나타내는 점을 제외하면 ModelSerializer 클래스와 유사하다
- serializer
class AccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Account
fields = ['url', 'id', 'account_name', 'users', 'created']
- views
serializer = AccountSerializer(queryset, context={'request': request})
- 기본적으로 자동으로 생성되는 url은 현재 시리얼라이저 class 명을 기반으로 추적된다.
- 예를들어 StreamPlatformSerializer 라는 클래스에서 정의 했다면 detail의 url에 접근할 이름을 자동으로 다음과 같이 만든다.
streamplatform-detail
- 이러한 문제를 해결하기 위해 명시적으로 url name을 선언할 수 있다. name 대신 주소를 직접 입력해도 될것이다.
class Meta:
model = StreamPlatform
fields = "__all__"
extra_kwargs = {
"url": {
"view_name": "steam-detail"
}, # Replace 'custom-model-detail' with your desired view name
}
- reference :
https://www.django-rest-framework.org/api-guide/serializers/#hyperlinkedmodelserializer
5. 테스트를 위한 django 자체 로그인 메뉴 활성화
- 프로젝트 폴더의 최상위 urls.py에 다음 코드를 추가한다.
path('api-auth/', include('rest_framework.urls')),
6. permission
- 전체 퍼미션은 settings.py에서 정의한다.
- REST_FRAMEWORK 변수는 하나만 선언이 가능하며 내부 정의 요소들은 추가가 가능하다.
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
- 함수형은 데코레이터로 정의한다.
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def example_view(request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
- 클래스형은 클래스 변수로 정의한다.
class ExampleView(APIView):
permission_classes = [IsAuthenticated|ReadOnly]
- reference :
https://www.django-rest-framework.org/api-guide/permissions/#permissions
- custom permission
- .has_permission(self, request, view) .has_object_permission(self, request, view, obj) 두 함수를 오버라이딩해서 구현할 수 있다.
- has_permission(request, view)
- APIView 접근시, 체크.
- 거의 모든 Permission 클래스에서 구현되며 로직에 따라 True/False 반환
- has_object_permission(request, view, obj)
- APIView의 get_object 함수를 통해 object 획득 시에 체크.
- 브라우저를 통한 API 접근에서 CREATE/UPDATE Form 노출 시 체크
- DjangoObjectPermissions에서 구현하며 로직에 따라 True/False 반환
- 기본 코드 라인은 다음과 같다.
if request.method in permissions.SAFE_METHODS:
# Check permissions for read-only request
else:
# Check permissions for write request
- 구현 예시는 다음과 같다.
class IsAdminOrReadOnly(permissions.IsAdminUser):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
else:
return bool(request.user and request.user.is_staff)
class IsReviewUserOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
else:
return obj.review_user == request.user or request.user.is_staff
- reference :
https://www.django-rest-framework.org/api-guide/permissions/#isadminuser
https://velog.io/@nameunzz/DRF-permissions
7. ID / Password 기반 인증 (authentication)
- 기본 인증 (BasicAuthentication)
- settings.py에 REST_FRAMEWORK 변수에 정의할 수 있다.
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.BasicAuthentication",
]
}
- 예시 코드는 다음과 같다.
class ExampleView(APIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, format=None):
content = {
'user': str(request.user), # `django.contrib.auth.User` instance.
'auth': str(request.auth), # None
}
return Response(content)
- 기본 인증은 id와 password로 이루어 진다.
- 방법은 headers에 key로 Authorization, value에 Basic id:password로 구성하면 되는데 id:password 부분은 base64로 인코딩 되어야 한다. 다른 툴이나 사이트를 이용해서 테스트 할 수 있다.
8. 기본 토큰 기반 인증 (Token authentication)
- REST_FRAMEWORK 변수의 DEFAULT_AUTHENTICATION_CLASSES 항목을 변경한다.
REST_FRAMEWORK = {
# "DEFAULT_AUTHENTICATION_CLASSES": [
# "rest_framework.authentication.BasicAuthentication",
# ],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
]
}
- INSTALLED_APPS 항목에 app을 추가한다.
INSTALLED_APPS = [
...
'rest_framework.authtoken'
]
- python manage.py migrate 를 실행시킨다.
- postman의 설정을 다음과 같이 정의한다.
- Token 생성 방법
1) ObtainAuthToken 뷰를 통한 획득 및 생성
- 이미 rest_framework 패키지에 만들어진 ObtainAuthToken 클래스를 사용한다.
- URL Pattern에 매핑 필요
- 코드 원형
# rest_framework/authtoken/views.py
class ObtainAuthToken(APIView):
def post(self, request, *args, **kwargs):
# ...
token, created = Token.objects.get_or_create(user=user)
return Response({'token':token.key})
2) Signal을 통한 자동 생성
- created나 updated 둘다 save를 호출하기 때문에 created=False를 기본으로 두고 True일 경우에만 토큰 생성
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch imort receiver
from rest_framework.authtoken.models import Token
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender,instance=None,created=False,**kwargs):
if created:
Token.objects.create(user=instance)
3) Management 명령을 토한 생성
# 생성된 Token을 변경하지는 않음. 필수X
python3 manage.py drf_create_token <username>
# 강제로 재생성
python3 manage.py drf_create_token -r <username>
4) admin 페이지에서 생성
- postman에서 테스트 하기
- 효율적인 save() 오버라이딩 코드 예시
class RegistrationSerializer(serializers.ModelSerializer):
password2 = serializers.CharField(style={"input_type": "password"}, write_only=True)
class Meta:
model = User
fields = ["username", "email", "password", "password2"]
extra_kwargs = {"password": {"write_only": True}}
def save(self):
password = self.validated_data["password"]
password2 = self.validated_data["password2"]
if password != password2:
raise serializers.ValidationError({"error": "P1 and P2 should be same!"})
if User.objects.filter(email=self.validated_data["email"]).exists():
raise serializers.ValidationError({"error": "Email already exists!"})
account = User(
email=self.validated_data["email"], username=self.validated_data["username"]
)
account.set_password(password)
account.save()
# account = User.objects.create_user(
# email=self.validated_data["email"],
# username=self.validated_data["username"],
# password=self.validated_data["password"],
# )
print("account : ", account)
return account
- 일반 code 방법으로 Token 생성 : View
- 생성 예제
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=...)
print(token.key)
- 가져오기 예제
token = Token.objects.get(user=account).key
- reference :
https://www.django-rest-framework.org/api-guide/authentication/
https://donis-note.medium.com/django-rest-framework-token-2618d914f018
https://velog.io/@duo22088/DRF-Token-Authentication
https://velog.io/@joje/Token%EC%9D%B8%EC%A6%9D-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0
9. JWT Authentication
- 여러 패키지들이 있어며 pyjwt로 직접 구현하는 방법도 있다 ( reference 참조)
- simple jwt를 사용한다. (https://django-rest-framework-simplejwt.readthedocs.io/en/latest/)
- 공식 사이트에 가면, 여러 url과 제공되는 기능들을 확인할 수 있다.
- 해당 패키지를 사용하면, DB에 토큰을 저장하지 않고 토큰에 관련한 정보를 담아 전송하여 인증에 사용한다.
- access token, refresh token 두 개의 토큰이 발행되며 각 토큰의 제한시간은 access token은 5분, refresh token은 24시간이다.
- api 설정
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
- postman 테스트
- 결과
- 토큰 사용 방법
- Headers탭을 누르고 Key에 Authorization, Value에 Bearer {access token} 이렇게 입력한다.
- 만료된 access token을 refresh token을 이용해 재발급 받기
- body 탭을 누르고 x-www-form-urlencoded를 선택한다. key에는 refresh, value에는 refresh token을 입력한다.
- 주요 settings 설정
- ACCESS_TOKEN_LIFETIME
- access token이 유효한 기간을 지정하는 datetime.timedelta 객체
- REFRESH_TOKEN_LIFETIME
- refresh token이 유효한 기간을 지정하는 datetime.timedelta 객체
- ROTATE_REFRESH_TOKENS
- True로 설정할 경우, refresh token을 보내면 새로운 access token과 refresh token이 반환된다.
- BLACKLIST_AFTER_ROTATION
- True로 설정될 경우, 기존에 있던 refresh token은 blacklist가된다.
- 코드를 통해 simplejwt의 refresh token과 access token을 추출하는 방법
- 참조 : https://django-rest-framework-simplejwt.readthedocs.io/en/latest/creating_tokens_manually.html
from rest_framework_simplejwt.tokens import RefreshToken
def registration_view(request):
.
refresh = RefreshToken.for_user(account)
data['token'] = {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
return Response(data, status=status.HTTP_201_CREATED)
- reference :
https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
https://velog.io/@duo22088/DRF-JWT-인증
https://gaussian37.github.io/python-rest-JWT-Authorization/
- 토큰 취약점
https://velog.io/@thelm3716/JWTvul
https://hyotwo.tistory.com/162
https://liebe97.tistory.com/20
- pyjwt
https://seongonion.tistory.com/112
https://juneyr.dev/2018-01-28/making-token-pyjwt
https://velog.io/@2cong/bcrypt%EC%99%80-PyJWT
10. Throttling - 호출 횟수 제한
- Throttle 이란 특정 조건 하에 전체, 혹은 개별 API의 최대 호출 회수를 결정하는 클래스이다.
- 기본 코드
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "1/day",
"user": "3/day",
}
}
- 모든 View에 대해 접속 제한이 설정된다. (모든 접근에 대해 total count 합계로 산정한다.)
- anon은 ip 기반, user은 로그인한 사용자 기반이다.
- 전역 설정을 view 개별 설정으로 바꾸는 방법 (모든 접근에 대해 total count 합계로 산정한다.)
- setting 내용을 변경한다.
REST_FRAMEWORK = {
# "DEFAULT_THROTTLE_CLASSES": [
# "rest_framework.throttling.AnonRateThrottle",
# "rest_framework.throttling.UserRateThrottle",
# ],
"DEFAULT_THROTTLE_RATES": {
"anon": "1/day",
"user": "3/day",
}
}
- 각 view에 접속 제한 범위에 대한 변수 값을 정의한다
class ReviewDetail(generics.RetrieveUpdateDestroyAPIView):
.
throttle_classes = [AnonRateThrottle]
- custom throttling 만들기 - 각 view별로 접근 제한을 설정할 수 있다.
- throttling 파일을 생성한다.
from rest_framework.throttling import UserRateThrottle
class ReviewCreateThrottle(UserRateThrottle):
scope = 'review-create'
class ReviewListThrottle(UserRateThrottle):
scope = 'review-list'
- setting 업데이트
"DEFAULT_THROTTLE_RATES": {
"anon": "5/day",
"user": "10/day",
"review-create": "2/day",
"review-list": "100/day",
"review-detail": "100/day",
},
- view에 적용
class ReviewList(generics.ListCreateAPIView):
.
throttle_classes = [ReviewListThrottle]
- 여러개의 throttling 을 선언하면, 설정된 값 중 가장 낮은 제한을 기준으로 설정된다.
throttle_classes = [ReviewListThrottle, AnonRateThrottle]
- ScopedRateThrottle 설정으로 제한하는 방법 - 코드 수를 줄일 수 있다.
1) setting을 이용한 전역 설정
- setting 설정
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.ScopedRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
"anon": "5/day",
"user": "10/day",
"review-create": "2/day",
"review-list": "100/day",
"review-detail": "100/day",
},
}
- view 설정
class ReviewDetail(generics.RetrieveUpdateDestroyAPIView):
.
throttle_scope = "review-detail"
2) 각 class별 설정
- setting에서 DEFAULT_THROTTLE_CLASSES 설정을 하지 않은 상태로 둔다.
- view 설정
throttle_classes = [ScopedRateThrottle]
throttle_scope = "review-detail"
- DEFAULT_THROTTLE_RATES에 선언된 변수를 특정 view에 같이 설정하면 합산되어 count가 계산된다.
- reference :
https://www.django-rest-framework.org/api-guide/throttling/
https://seoyoung2.github.io/django/2020/08/19/Throttling.html
https://www.lostcatbox.com/2020/01/23/DRF10+11/
11. Filtering
- django 코드만을 사용한 serching 기능 구현
class UserReview(generics.ListAPIView):
# queryset = Review.objects.all()
serializer_class = ReviewSerializer
# permission_classes = [IsAuthenticated]
# throttle_classes = [ReviewListThrottle, AnonRateThrottle]
# def get_queryset(self):
# username = self.kwargs['username']
# return Review.objects.filter(review_user__username=username)
def get_queryset(self):
username = self.request.query_params.get('username', None)
return Review.objects.filter(review_user__username=username)
- django-filter를 사용한 기능 구현
- 패키지 정보 : https://django-filter.readthedocs.io/en/stable/
- 패키지 설치 :
pip install django-filter
- setting 설정
INSTALLED_APPS = [
...
'django_filters',
...
]
- setting을 이용한 전역 설정
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}
- view에 개별 설정
from django_filters.rest_framework import DjangoFilterBackend
class UserListView(generics.ListAPIView):
...
filter_backends = [DjangoFilterBackend]
- view에서 필터 정의 방법
- 검색에 사용될 모델 필드명을 정의한다.
- 해당 필터들은 index가 되어 있어야 효율적인 검색이 이루어 진다.
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['category', 'in_stock']
- filterset_fields의 검색 인자
- '^' 검색으로 시작합니다.
- '=' 정확히 일치합니다.
- '@' 전체 텍스트 검색. (현재는 Django의 PostgreSQL 백엔드만 지원됩니다.)
- '$' 정규식 검색
- url에도 직접 검색 유형을 정의할 수 있다. 해당 내용은 공식 문서를 통해 확인 가능하다.
- 검색 결과를 ordering 할 수 있다.
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
filter_backends = [filters.OrderingFilter]
ordering_fields = ['username', 'email']
* 여러 인자 획득
- filtering 을 하는데 필요한 인자들을 request 를 이용해 획득할 수 있다
- self.request.user
- 현재 로그인 중인 유저에 접근할 수 있다
- 로그인이 안 되어 있을 시에는 AnnoymousUser 인스턴스를 획득한다
- self.request.GET
- 요청한 get 인자들을 획득한다
- self.request.query_params
- self.request.GET 와 같은 값을 얻는다
- 보다 더 가독성이 높기 때문에 DRF 에서 지원하고 있다
- self.kwargs
- URL Capture 된 인자를 획득한다
- reference :
https://www.django-rest-framework.org/api-guide/filtering/
12. pagination
- PageNumberPagination은 데이터를 일정 크기의 페이지로 나누고 클라이언트가 특정 페이지를 요청할 수 있게 합니다.
- LimitOffsetPagination은 클라이언트가 반환할 항목 수와 데이터 컬렉션 내에서 시작 지점을 지정할 수 있습니다.
- CursorPagination은 큰 데이터 세트에 대해 더 효율적인 커서 기반의 페이지네이션 시스템을 제공합니다.
- LimitOffsetPagination 설정 방법
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 100
}
- postman에서 호출 방법
- view 개별적으로 pagination 설정, PageNumberPagination, LimitOffsetPagination, CursorPagination 커스텀
- 각 클래스를 상속받아 개별 pagination을 정의한다
- pagination.py 생성
- 각 클래스별 사용되는 클래스 변수의 기능들은 공식 페이지 혹은 다음 사이트 정보를 참고한다
- https://kimdoky.github.io/django/2018/07/19/drf-Pagination/
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination
class WatchListPagination(PageNumberPagination):
page_size = 3
page_query_param = 'p'
page_size_query_param = 'size'
max_page_size = 10
last_page_strings = 'end'
class WatchListLOPagination(LimitOffsetPagination):
default_limit = 5
max_limit = 10
limit_query_param = 'limit'
offset_query_param = 'start'
class WatchListCPagination(CursorPagination):
page_size = 5
ordering = 'created'
cursor_query_param = 'record'
- view 파일에 정의
class WatchListGV(generics.ListAPIView):
queryset = WatchList.objects.all()
serializer_class = WatchListSerializer
pagination_class = WatchListCPagination
- reference :
https://www.django-rest-framework.org/api-guide/pagination/
https://kimdoky.github.io/django/2018/07/19/drf-Pagination/
https://velog.io/@jewon119/TIL00.-DRF-Pagination-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0
https://ssungkang.tistory.com/entry/Django-DRF-Pagination
https://velog.io/@holawan/DRF-Pagination
13. 응답 형식 변경 - renderers
- 하나의 API 인자에 따라 같은 내용을 PDF, XLSX, JSON, HTML 등등 다양한 형태의 응답 포맷을 지원하고 싶을땐 DRF의 Renderer 를 이용할 수 있다.
- 기본 설정
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
]
}
- view에서 설정하는 방법
from django.contrib.auth.models import User
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
class UserCountView(APIView):
"""
A view that returns the count of active users in JSON.
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
user_count = User.objects.filter(active=True).count()
content = {'user_count': user_count}
return Response(content)
- template가 있는 경우
class UserDetail(generics.RetrieveAPIView):
"""
A view that returns a templated HTML representation of a given user.
"""
queryset = User.objects.all()
renderer_classes = [TemplateHTMLRenderer]
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return Response({'user': self.object}, template_name='user_detail.html')
- reference :
https://velog.io/@duo22088/DRF-Renderer-를-통한-다양한-응답
https://hyun-am-coding.tistory.com/entry/8-Renderers