2025.7.22: drf-spectacular (2)

jiyongg·2025년 7월 22일

TIL: Today I Learned

목록 보기
5/30

이번에는 미니 해커톤 프로젝트에 직접 drf-spectacular를 사용해보고자 한다.

1. ⚙️ 설치 및 구성

로컬 설정 수정

지난 번 배포 실습에서는 로컬이든 배포 환경이든 AWS의 DB 서버를 사용했다. 하지만, AWS의 DB 서버는 퍼블릭 액세스를 허용해두면 과금이 되므로, 이번에는 로컬의 sqlite3을 사용하고자 한다.

local_settings.py를 수정한다.

...
from .settings import BASE_DIR

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

DEBUG = True

그리고 개발 환경이므로 DEBUG = True를 추가해 주었다.

설치 및 구성

$ pip install drf-spectacular

이후 settings.py를 수정한다.

  • INSTALLED_APPSdrf_spectacular를 추가한다.
  • REST_FRAMEWORKDEFAULT_SCHEMA_CLASS 설정을 drf_spectacular.openapi.AutoSchema로 변경한다.
  • SPECTACULAR_SETTINGS 설정을 추가한다.
...
INSTALLED_APPS = [
    ...
    'drf_spectacular',
    ...
]

...

REST_FRAMEWORK = {
    ...
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
    ...
}

SPECTACULAR_SETTINGS = {
    'TITLE': 'MovieReview API',
    'DESCRIPTION': '미니 해커톤 2조 MovieReview 사이트의 API입니다.',
    'VERSION': 'dev',
    'SERVE_INCLUDE_SCHEMA': False,
}

urls.py를 수정해서 URL에 스키마를 서비스할 뷰를 연결해준다.

...
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView

...

urlpatterns = [
    ...
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/schema/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger'),
    path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]

2. 🛜 서버 실행

서버를 열어본다.

$ python manage.py runserver

API들이 잘 나타난다.

3. ✅ 데코레이터가 필요한 부분 찾기

하지만 일부 API들은 정보가 제대로 제공되지 않는다.

예를 들어 /api/movies/comment/list/{movie_id}/의 GET 메소드 정보를 보면

이 엔드포인트에 연결된 뷰(CommentList)가 댓글 정보(페이지네이션된 CommentResponseSerializer의 데이터)를 응답으로 돌려줌에도 불구하고 No response body라고 되어 있는 것을 볼 수 있다.

이것은 drf-spectacular가 뷰를 분석할 때, 응답에 대해 필요한 정보를 얻지 못했음을 의미한다.

그리고 터미널을 보면 Warning과 Error가 떠 있는 것을 볼 수 있다.

이런 식으로 일부 뷰나 시리얼라이저에 대해 필요한 정보를 얻지 못해서 정보가 누락될 수 있다. 누락된 정보는 해당 뷰나 시리얼라이저에 데코레이터를 붙여서 제공한다.

그렇다면 어디에 데코레이터를 붙여야 할 지 미리 알 수 있다면 좋지 않을까?

drf-spectacular--fail-on-warn--validate를 이용하면 이 문제를 해결할 수 있다.

$ python manage.py spectacular --color --fail-on-warn --validate

이 명령어를 실행하면, 경고가 하나라도 발생하면 SchemaGenerationError가 일어나 스키마 생성이 실패한다.

이런 식으로 스키마 생성에 발생한 경고와 에러의 개수를 보여준다.

그리고 위로 스크롤을 올리면 경고와 에러가 발생한 부분을 볼 수 있다. 그러면 그 부분에 대해 데코레이터를 이용해 추가적인 정보를 제공하면 된다.

나의 경우 두가지 부분에서 발생했다.

  • MovieListResponseSerializer에서 SerializerMethodField로 지정된 detail_url의 필드 타입을 알아내지 못했다.
  • CommentList 뷰에서 이용되는 시리얼라이저를 알아내지 못했다.

4. 🖌️ 데코레이터로 정보 제공

이제 데코레이터가 필요한 부분을 알았으니, 데코레이터로 정보를 제공하면 된다.

MovieListResponseSerializer

class MovieListResponseSerializer(serializers.HyperlinkedModelSerializer):
    '''
    메인 화면에서 보이는 여러 영화의 정보
    '''
    detail_url = serializers.SerializerMethodField()

    def get_detail_url(self, obj):
        request = self.context['request']
        # reverse: 현재 상대 경로를 바탕으로 절대 경로 생성 후 반환
        return reverse('movies:movie-detail', kwargs={'movie_id': obj.id}, request=request)

    class Meta:
        model = models.Movie
        fields = ['id', 'title_kor', 'poster_url', 'rate', 'detail_url']

여기에서 detail_url의 필드 타입을 알아내지 못헀다. get_detail_url 메소드에 @extend_schema_field를 붙여서 detail_url 필드는 URI 타입임을 알려준다.

...
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes

class MovieListResponseSerializer(serializers.HyperlinkedModelSerializer):
    '''
    메인 화면에서 보이는 여러 영화의 정보
    '''
    detail_url = serializers.SerializerMethodField()
    
    @extend_schema_field(OpenApiTypes.URI)
    def get_detail_url(self, obj):
        request = self.context['request']
        # reverse: 현재 상대 경로를 바탕으로 절대 경로 생성 후 반환
        return reverse('movies:movie-detail', kwargs={'movie_id': obj.id}, request=request)

    class Meta:
        model = models.Movie
        fields = ['id', 'title_kor', 'poster_url', 'rate', 'detail_url']

CommentList의 시리얼라이저

현재 CommentList는 이렇게 작성되어 있다.

class CommentList(generics.GenericAPIView):
    authentication_classes = [JWTAuthentication]
    permission_classes = [IsAuthenticatedOrReadOnly]

    def get(self, request, movie_id):
        try:
            movie = models.Movie.objects.get(id=movie_id)
            comment = models.Comment.objects.filter(movie_id=movie)

            page = self.paginate_queryset(comment)

            if page is not None:
                serializer = serializers.CommentResponseSerializer(page, many=True)
                return self.get_paginated_response(serializer.data)
            
            serializer = serializers.CommentResponseSerializer(comment, many=True)
            return Response(serializer.data)
        except models.Movie.DoesNotExist:
            return Response(status=status.HTTP_404_NOT_FOUND)
        
    def post(self, request, movie_id):
        try:
            movie = models.Movie.objects.get(id=movie_id)
        except models.Movie.DoesNotExist:
            return Response(status=status.HTTP_404_NOT_FOUND)
        
        serializer = serializers.CommentRequestSerializer(data=request.data)
        print(request.user)
        if serializer.is_valid(): # 유효성 검사
            serializer.save(movie_id=movie, user_id=request.user)
            return Response(serializer.data, status = status.HTTP_201_CREATED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  • CommentListget, post에서 각각 다른 시리얼라이저를 사용한다. 그리고 CommentList의 각 메소드는 영화 id(movie_id)를 필요로 한다.
  • GenericAPIView를 상속 받는다. 하지만, serializer_classqueryset은 가지고 있지 않다.
  • ListModelMixinlist과 매우 유사한 코드인데 get_serializer, get_queryset, filter_queryset을 사용하지 않고 직접 시리얼라이저와 쿼리셋을 지정해주고 있다.

여기에서 getpost에 사용되는 시리얼라이저 정보를 제공해야 한다. getpost에 각각 @extend_schema를 사용하는 방법도 있지만, 이번에는 OpenApiViewExtension을 사용해 보고자 한다.

공식 문서에 따르면, schema.py라는 파일을 새로 만들고 거기에 Extension들을 모두 작성한 후 apps.py에서 스키마를 불러오도록 하는 것이 권장된다고 한다.

그래서 schema.py를 만들고, apps.py를 아래와 같이 수정해준다.

from django.apps import AppConfig


class MoviesConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'movies'

    def ready(self):
        import movies.schema

schema.pyOpenApiViewExtension을 상속하는 클래스를 하나 만들어 준다.

from drf_spectacular.utils import extend_schema, extend_schema_view
from drf_spectacular.extensions import OpenApiViewExtension

from . import serializers

class SchemaCommentList(OpenApiViewExtension):
    target_class = '.views.CommentList'
    def view_replacement(self):
        class CommentListReplacement(self.target_class):
            def get_serializer_class(self):
                if self.request.method == 'GET':
                    return serializers.CommentResponseSerializer
                elif self.request.method == 'POST':
                    return serializers.CommentRequestSerializer

        return CommentListReplacement
  • target_class에 원본 뷰를 지정한다.
  • view_replacement 안에 원본 뷰(self.target_class)를 상속한 클래스 CommentListReplacement를 만들고 해당 클래스를 반환한다.
    • get_serializer_class 메소드를 오버라이딩했다. get 메소드는 CommentResponseSerializer를 사용하고 있고, post 메소드는 CommentRequestSerializer를 사용하고 있으므로 HTTP 메소드에 따라 get_serializer_class가 다른 시리얼라이저를 반환하게 한다.

사실 원래 뷰의 get_serializer_class 메소드를 오버라이딩시켜도 되었겠지만, 실습을 위해서 한 번 이렇게 해 보았다.

다시 스키마 생성 명령어를 실행해 데코레이터가 필요한 부분이 더 있는지 확인해본다.

$ python manage.py spectacular --color --fail-on-warn --validate

이번에도 경고가 발생하며 스키마 생성에 실패하였다.

스크롤을 위로 올려보니, 이번에 경고가 발생한 부분은 CommentResponseSerializerget_nickname 부분이었다.

아까 get_detail_url과 같은 방법으로 처리해주면 된다.

class CommentResponseSerializer(serializers.ModelSerializer):
    '''
    1. 영화에 달린 코멘트 목록 열람용 시리얼라이저
    2. 코멘트 작성 시 반환되는 응답용 시리얼라이저
    '''
    nickname = serializers.SerializerMethodField()
    created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M")

    class Meta:
        model = models.Comment
        fields = ['id', 'nickname', 'comment', 'created_at']

    @extend_schema_field(OpenApiTypes.STR)
    def get_nickname(self, obj):
        return obj.user_id.nickname

여기서 주석을 보면, 코멘트 작성 시 응답용 시리얼라이저로 이것을 반환하게 의도했는데, 코멘트 부분을 인수인계 하는 과정에서 꼬인 것 같다...

이제 스키마 생성 명령어를 다시 실행해보면

스키마 생성이 잘 이루어져서 화면에 스키마 문서의 내용이 출력되는 것을 볼 수 있다.

5. ☑️ 확인

다시 서버를 열고 스키마를 볼 수 있는 URL로 들어가 본다.

API가 잘 나타난다.

아까 문제가 있었던 /api/movies/comments/list/{movie_id}/를 열어보니, No response body로 나타났던 응답 부분도 이렇게 잘 나타나고 있는 것을 볼 수 있다.

6. 🎨 추가 커스터마이징

그런데, 무언가 각 메소드에 대한 요약이 있다면 좋지 않을까? 그리고 예시도 추가한다면 좋을 것 같다.

이러한 것들을 해결하기 위해 추가적으로 커스터마이징을 진행해보고자 한다.

요약(summary) 달기

엔드포인트에 대한 요약을 다는 방법은 두가지가 있다.

  • @extend_schema(summary="내용")을 메소드에 달아준다.
  • @extend_schema_view(메소드명=extend_schema(summary="내용"))을 클래스형 뷰에 달아준다.

첫번째 방법은 직접 구현한 메소드일 경우 사용하기 좋고, 두번째 방법은 상속을 통해 부모 메소드를 오버라이딩 없이 사용할 때 사용하기 좋다.

여기서 모든 코드를 다루면 글이 길어지기 때문에, 몇가지 뷰만 살펴보고자 한다.

Concrete Generic View일 때

Concrete Generic View는 CreateAPIView, ListAPIView, RetrieveAPIView처럼 이미 HTTP 메소드 핸들러가 구현되어 있는 뷰들이다.

이러한 뷰에서 상속받은 메소드를 그대로 사용할 때에는, @extend_schema_view를 사용하는 방법이 더 효율적이다.

@extend_schema_view(
    get=extend_schema(summary="모든 영화 조회")
)
class MovieList(generics.ListAPIView):
    '''
    모든 영화 목록을 조회
    '''
    queryset = models.Movie.objects.all()
    serializer_class = serializers.MovieListResponseSerializer
  • ListAPIView에는 get이 구현되어 있다. 이 get에 대한 summary를 추가해준다.
    • ⚠️ ViewSet과 헷갈릴 수 있는데, Concrete Generic View는 get, post 등의 핸들러가 구현되어 있는 것이고, ViewSet은 list, create 등의 action이 구현되어 있고 라우터의 매핑 과정을 통해 action이 적절한 HTTP 메소드에 연결되는 것이다. 그래서 Concrete Generic View에 @extend_schema_view를 달 때에는 키워드형 인자의 이름은 get, post 등이 되어야 한다.

Generic View일 때, 혹은 메소드를 오버라이딩했을 때

Generic View일 경우 get, post 등의 메소드는 직접 구현해야 한다. 또는, 메소드를 오버라이딩했을 때에도 역시 getpost 등의 메소드를 구현한 부분이 있을 것이다. 이럴 때에는 두 가지 방법 모두 사용할 수 있는데, 나는 첫번째 방법을 사용했다.

class MovieSearch(generics.ListAPIView):
    '''
    한국어 제목을 바탕으로 영화 검색. 영화 제목 내 검색어 포함을 기준으로 검색됨
    '''
    queryset = models.Movie.objects.all()
    serializer_class = serializers.MovieListResponseSerializer
    
    @extend_schema(summary="한국어 제목을 바탕으로 영화 검색")
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)
    
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        keyword = request.query_params.get('title', '')
        queryset = queryset.filter(title_kor__icontains=keyword)

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

queryset을 바꾸기 위해서 list를 오버라이딩해 준 상태이다. 그리고 get이 오버라이딩되어 있는데, 지금 보니 ListAPIViewget 메소드와 완전히 똑같아서 굳이 필요하지 않았을 것 같다. 어쨌든 이 get@extend_schema 데코레이터를 달아서 엔드포인트의 요약을 추가해 주었다.

OpenApiViewExtension과의 사용

아까 CommentList의 스키마를 위해서 SchemaCommentList라는 클래스를 만들어 주었다. 여기에서 getpost 메소드에 대한 summary를 추가해보고자 한다.

class SchemaCommentList(OpenApiViewExtension):
    target_class = CommentList
    def view_replacement(self):
        @extend_schema_view(
            get=extend_schema(summary="해당 영화의 모든 코멘트 조회"),
            post=extend_schema(summary="해당 영화에 코멘트 추가"),
        )
        class CommentListReplacement(self.target_class):
            def get_serializer_class(self):
                if self.request.method == 'GET':
                    return serializers.CommentResponseSerializer
                elif self.request.method == 'POST':
                    return serializers.CommentRequestSerializer

        return CommentListReplacement
  • 이 상황에서 굳이 CommentListReplacement 안에 다시 getpost를 정의하는 것보다는, @extend_schema_view를 사용하면 효율적으로 정보를 추가할 수 있다.
dj-rest-auth의 뷰에 적용

내가 작성한 뷰가 아니더라도 OpenApiViewExtension을 사용해서 정보를 수정할 수 있다.

dj-rest-auth의 로그인 뷰에 summary를 추가해보고자 한다. 유저를 담당하는 앱 accounts 폴더에서 schema.py를 만들고, apps.py를 수정해 스키마를 불러오게 한다.

from django.apps import AppConfig


class AccountsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'accounts'

    def ready(self):
        import accounts.schema

schema.py에 새로운 클래스를 만들고, SchemaCommentList와 같은 방법으로 summary를 추가한다.

from drf_spectacular.utils import extend_schema, extend_schema_view
from drf_spectacular.extensions import OpenApiViewExtension

class SchemaLogin(OpenApiViewExtension):
    target_class = 'dj_rest_auth.views.LoginView'

    def view_replacement(self):
        @extend_schema_view(
            post=extend_schema(summary="로그인"),
        )
        class LoginReplacement(self.target_class):
            pass
        return LoginReplacement
  • dj-rest-auth에서 로그인을 담당하는 뷰는 dj_rest_auth.viewsLoginView이고, 해당 뷰에는 post 메소드가 구현되어 있다.
  • LoginView 클래스에 대해 변경할 사항은 없으므로 pass로 그대로 상속받는다.

로그인 엔드포인트에 로그인이라는 summary가 잘 나타나는 것을 볼 수 있다.

이러한 방법으로 dj-rest-auth의 뷰들에 summary를 달아보았다.

/api/dj/logout/

이 URL은 LogoutView와 연결되어 있는데, 이 뷰에는 serializer_class가 지정되어 있지 않다. 그래서인지 OpenApiExtension으로 스키마를 확장시키면 응답 정보가 제대로 표시되지 않는다.

LogoutView가 반환하는 응답의 데이터는 모두 {'detail': '내용'}의 형식이며, LogoutView는 따로 시리얼라이저를 사용하지 않고 직접 데이터를 구성해서 반환하고 있다. 그래서 inline_serializer를 통해 응답 데이터의 구성 정보를 제공할 것이다.

class SchemaLogout(OpenApiViewExtension):
    target_class = 'dj_rest_auth.views.LogoutView'

    def view_replacement(self):
        @extend_schema_view(
            get=extend_schema(summary="로그아웃"),
            post=extend_schema(summary="로그아웃"),
        )
        class LogoutReplacement(self.target_class):
            serializer_class = inline_serializer('RestAuthDetail', {'detail': serializers.CharField()})
        return LogoutReplacement
  • inline_serializer에는 name 인자, fields 인자가 차례대로 오며, fields는 필드명을 키로, 시리얼라이저 필드를 값으로 하는 딕셔너리이다.
/api/dj/token/refresh/

이 URL은 다른 뷰들과 달리 get_refresh_view라는 함수를 호출한 뒤 해당 함수가 반환하는 뷰가 연결되어 있다.

def get_refresh_view():
    """ Returns a Token Refresh CBV without a circular import """
    from rest_framework_simplejwt.settings import api_settings as jwt_settings
    from rest_framework_simplejwt.views import TokenRefreshView

    class RefreshViewWithCookieSupport(TokenRefreshView):
        serializer_class = CookieTokenRefreshSerializer

        def finalize_response(self, request, response, *args, **kwargs):
            if response.status_code == status.HTTP_200_OK and 'access' in response.data:
                set_jwt_access_cookie(response, response.data['access'])
                response.data['access_expiration'] = (timezone.now() + jwt_settings.ACCESS_TOKEN_LIFETIME)
            if response.status_code == status.HTTP_200_OK and 'refresh' in response.data:
                set_jwt_refresh_cookie(response, response.data['refresh'])
                if api_settings.JWT_AUTH_HTTPONLY:
                    del response.data['refresh']
                else:
                    response.data['refresh_expiration'] = (timezone.now() + jwt_settings.REFRESH_TOKEN_LIFETIME)
            return super().finalize_response(request, response, *args, **kwargs)
    return RefreshViewWithCookieSupport

함수 get_refresh_view의 내부에 클래스를 선언하고, 그 클래스를 반환하는 방식이다. 이것 때문에, 바로 뷰가 연결되어 있지 않아서인지 위의 방법이 먹히지 않는다.

일단 이번 포스트에서는 이 부분은 넘어가고, 조금 더 공부하며 방법을 생각해봐야 할 것 같다.

/api 없애기

지금의 스키마 문서를 보면, 모두 /api로 시작한다.

그런데, 이게 API라는걸 모르는 사람이 있을까? 이 거슬리는 /api를 없애보자.

drf-spectacular의 설정에는 SCHEMA_PATH_PREFIX와, SCHEMA_PATH_PREFIX_TRIM이 있다. SCHEMA_PATH_PREFIX를 지정하고 SCHEMA_PATH_PREFIX_TRIMTrue로 설정하면, 각 엔드포인트의 앞쪽에서 SCHEMA_PATH_PREFIX 부분을 제거해준다.

settings.pySPECTACULAR_SETTINGSSCHEMA_PATH_PREFIXSCHEMA_PATH_PREFIX_TRIM을 추가한다.

SPECTACULAR_SETTINGS = {
    'TITLE': 'MovieReview API',
    'DESCRIPTION': '미니 해커톤 2조 MovieReview 사이트의 API입니다.',
    'VERSION': 'dev',
    'SERVE_INCLUDE_SCHEMA': False,
    'SCHEMA_PATH_PREFIX': '/api',
    'SCHEMA_PATH_PREFIX_TRIM': True,
}

이렇게 /api가 제거함과 동시에 dj-rest-auth 관련 기능은 dj, 영화 관련 기능은 movies 태그로 구분되면서 기능의 구분이 더 깔끔해졌다!

태그 수정

그런데, 현재 댓글 관련 엔드포인트를 보면 movies라는 태그로 구분되어 있다. 나는 이 엔드포인트들의 태그를 comments로 바꾸고자 한다.

아까 만들었던 SchemaCommentList의 내부 클래스 CommentListReplacement에 덧붙인 @extend_schema_view에 태그 정보를 추가해준다.

class SchemaCommentList(OpenApiViewExtension):
    target_class = 'movies.views.CommentList'
    def view_replacement(self):
        @extend_schema_view(
            get=extend_schema(summary='해당 영화의 모든 코멘트 조회', tags=['comments']),
            post=extend_schema(summary='해당 영화에 코멘트 추가', tags=['comments']),
        )
        class CommentListReplacement(self.target_class):
            def get_serializer_class(self):
                if self.request.method == 'GET':
                    return serializers.CommentResponseSerializer
                elif self.request.method == 'POST':
                    return serializers.CommentRequestSerializer

        return CommentListReplacement
  • ⭐ 여기에서 CommentList와 연결된 엔드포인트의 태그는 comments만을 가지게 된다. 즉, 데코레이터로 지정한 내용은 덧붙여지는 것이 아니라, 오버라이딩 된다.

새로고침하여 확인해보면

이처럼 댓글 관련 엔드포인트는 comments 태그로 잘 분류된 것을 볼 수 있다.

문서에 서버 정보 추가, 엔드포인트 순서 변경

지금 스키마 문서에는 서버 정보가 없다. 이러면 어느 서버에 요청을 보내야 하는지 알 수 없다. 그리고 엔드포인트가 현재 dj-rest-auth의 엔드포인트들부터 나타나고 있는데, 프로젝트에 등록된 URL 패턴 순서대로 나오게 바꾸고 싶다.

두 작업 모두 drf-spectacular의 설정을 통해서 할 수 있다.

서버 정보는 SERVERS에 서버 정보가 담긴 딕셔너리로 이루어진 리스트를 지정해서 추가할 수 있고, SORT_OPERATIONSFalse로 지정하면 URL 패턴을 추가한 순서대로 엔드포인트가 나타나게 된다.

SPECTACULAR_SETTINGS = {
    'TITLE': 'MovieReview API',
    'DESCRIPTION': '미니 해커톤 2조 MovieReview 사이트의 API입니다.',
    'VERSION': 'dev',
    'SERVERS': [{'url': 'http://localhost:8000/api'}],
    'SERVE_INCLUDE_SCHEMA': False,
    'SCHEMA_PATH_PREFIX': '/api',
    'SCHEMA_PATH_PREFIX_TRIM': True,
    'SORT_OPERATIONS': False,
}
  • 서버 정보가 담긴 딕셔너리는 이 문서를 참고해서 구성하면 된다. 문서 내의 키워드를 키로 하고, 해당 키워드에 해당하는 값을 값으로 한다.
  • SORT_OPERATIONS는 기본적으로 True 상태이다. 켜져 있을 경우 전처리 훅의 작업 이후에 정렬 작업이 적용된다.

현재 urls.py는 다음과 같다.

urlpatterns = [
    path('api/admin/', admin.site.urls),
    path('api/accounts/', include('accounts.urls')),
    path('api/movies/', include('movies.urls')),
    path('api/dj/', include('dj_rest_auth.urls')),
    path('api/dj/registration/', include('dj_rest_auth.registration.urls')),
    
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/schema/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger'),
    path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]

그리고 movies 앱의 urls.py는 다음과 같다.

from django.contrib import admin
from django.urls import path

from . import views

app_name = 'movies'
urlpatterns = [
    path('', views.MovieListofTopTen.as_view(), name='movie-main'),
    path('list/', views.MovieList.as_view(), name='movie-list'),
    path('list/<int:movie_id>/', views.MovieDetail.as_view(), name='movie-detail'),
    path('search/', views.MovieSearch.as_view(), name='movie-search'),
    path('comment/create/<int:movie_id>/', views.CommentList.as_view(),  name='comment-list'),
    path('comment/list/<int:movie_id>/', views.CommentList.as_view(), name='comment-create'),
]

그리고 이 화면을 보면, 다음을 알 수 있다.

  • 두 엔드포인트의 태그가 다르고, 두 엔드포인트가 각 태그의 맨 첫번째 엔드포인트라면, 둘 중 먼저 등록된 엔드포인트의 태그가 앞에 나타난다.
  • 같은 태그 내에서는 엔드포인트들이 등록한 순서대로 나타나고 있음을 볼 수 있다.

7. 🔚 결론

아직 커스터마이징 작업은 끝나지 않았다.

일단 오늘은 여기까지 하고, 다음에 커스터마이징을 조금 더 진행해서 요청/응답 예시나, 매개변수의 값 예시 등을 추가해주는 작업을 하여 drf-spectacular의 실습을 마무리해보고자 한다.

사실 미리 1편을 쓰며 예시 부분을 좀 알아봤는데, 쉽게 될 것 같은 느낌은 아니긴 했다. 그래도 일단 시도는 해보는 게 좋지 않을까?

profile
그냥 쓰고 싶은 것 쓰는 개발(?) 블로그

0개의 댓글