오늘 한 일
로그인
settings.py
INSTALLED_APPS = [
'rest_framework',
'rest_framework_simplejwt',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
}
serializer
- 필수 입력(required=True) / 입력 전용(write_only=True)
serializers.EmailField: 이메일 형식이 맞는지 자동으로 검사
style={'input_type': 'password'}
- Browsable API에서 비밀번호 입력창을 마스킹 처리함
from rest_framework import serializers
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField(write_only=True, required=True)
password = serializers.CharField(
write_only=True,
required=True,
style={"input_type": "password"}
)
service
class UserService:
@staticmethod
def authenticate_user(email, password):
"""
사용자 인증 및 JWT 토큰 생성을 담당하는 메서드입니다.
"""
"""
- 이 함수는 이메일과 비밀번호가 DB와 일치하는지 확인하고, 일치하면 User 객체를 반환함
- 일치하지 않으면 None을 반환함
"""
user = authenticate(email=email, password=password)
"""
- 이렇게 명시적으로 예외를 던지면 View에서 400/401 에러를 적절히 반환 가능
"""
if not user:
raise exceptions.AuthenticationFailed(
"이메일 또는 비밀번호가 일치하지 않습니다."
)
"""
- 비밀번호가 맞아도 관리자가 정지시킨 계정(is_active=False)은 로그인되면 안 됨
"""
if not user.is_active:
raise exceptions.PermissionDenied("해당 계정은 비활성화 상태입니다.")
"""
- RefreshToken.for_user(user)를 호출하면 해당 유저를 위한 Refresh/Access 토큰 쌍이 생성됨
"""
refresh = RefreshToken.for_user(user)
"""
- View로 User 객체와 생성된 토큰 문자열을 딕셔너리 형태로 전달
"""
return {
"user": user,
"access_token": str(refresh.access_token),
"refresh_token": str(refresh),
}
view
- 로그인 엔드포인트는 인증되지 않은 사용자도 접근 가능해야 하므로
AllowAny를 설정
class LoginAPIView(APIView):
permission_classes = [AllowAny]
def post(self, request):
"""
POST 요청을 처리하여 로그인을 수행합니다.
"""
"""
- request.data에는 사용자가 보낸 JSON body(email, password)가 들어있습니다.
- is_valid(raise_exception=True)는 유효성 검사 실패 시 자동으로 400 Bad Request를 반환함
"""
serializer = LoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
"""
- 검증된 데이터(validated_data)에서 email과 password를 꺼내 인증 서비스에 넘김
- 인증 실패 시 Service 내부에서 예외가 발생하므로, 여기까지 코드가 진행되었다면 인증 성공
"""
login_data = UserService.authenticate_user(
email=serializer.validated_data["email"],
password=serializer.validated_data["password"],
)
"""
- 클라이언트가 사용할 수 있도록 토큰과 필요한 유저 정보를 JSON으로 구성
"""
return Response(
{
"message": "로그인에 성공하였습니다.",
"token": {
"access": login_data["access_token"],
"refresh": login_data["refresh_token"],
},
"user": {
"email": login_data["user"].email,
"nickname": login_data["user"].nickname,
},
},
status=status.HTTP_200_OK,
)
회원가입
serializer
User = get_user_model()
class SignupSerializer(serializers.ModelSerializer):
password = serializers.CharField(
write_only=True,
required=True,
style={'input_type': 'password'},
min_length=8
)
class Meta:
model = User
fields = ['email', 'nickname', 'password']
def validate_email(self, value):
"""
이메일 중복 여부를 커스텀하게 검증합니다.
Model의 unique=True가 있지만, 여기서 명시적인 에러 메시지를 주는 것이 UX에 좋습니다.
"""
if User.objects.filter(email=value).exists():
raise serializers.ValidationError("이미 존재하는 이메일입니다.")
return value
service
User = get_user_model()
class SignupService:
@staticmethod
def create_user(validated_data: dict):
"""
유효성 검사가 완료된 데이터를 받아 유저를 생성합니다.
"""
email = validated_data.get('email')
password = validated_data.get('password')
nickname = validated_data.get('nickname')
"""
- create_user 메서드는 UserManager(managers.py)에 정의된 로직을 따름
- 내부적으로 set_password()를 호출하여 비밀번호를 암호화(hashing) 저장
"""
user = User.objects.create_user(
email=email,
nickname=nickname,
password=password
)
return user
view
class SignupAPIView(APIView):
permission_classes = [AllowAny]
def post(self, request):
"""
회원가입 요청을 처리합니다.
"""
serializer = SignupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
"""
- serializer.validated_data에는 검증이 끝난 깨끗한 데이터가 들어있음
"""
user = SignupService.create_user(serializer.validated_data)
return Response(
{
"message": "회원가입이 성공적으로 완료되었습니다.",
"user": {
"id": user.id,
"email": user.email,
"nickname": user.nickname
}
},
status=status.HTTP_201_CREATED
)
문제
무한로딩

[ 🔴 문제: ]
스웨거(Swagger UI) 접속은 되지만, 기능 테스트(Try it out) 시 무한 로딩이 걸리거나
브라우저 콘솔에서 TypeError: Cannot destructure property 'type' of 'i' as it is undefined와 같은
자바스크립트 런타임 에러가 발생하며 작동이 중단되는 현상 발생
[ 🟡 원인: ]
1. 잘못된 설정 값 형식 (String vs Dict)
기존 config/settings.py에서 SWAGGER_UI_SETTINGS를 파이썬의 딕셔너리({})가 아닌 문자열("""...""")로 전달
drf-spectacular 라이브러리는 이 설정을 딕셔너리로 받아 내부적으로 처리하는데,
문자열로 들어오자 이를 올바르게 해석하지 못해 스웨거 프론트엔드 설정이 깨짐
2. 보안 컴포넌트 누락
스웨거 UI는 SECURITY 설정을 만날 때 해당 인증 방식(예: BearerAuth)이 어떻게 작동하는지
components/securitySchemes에서 찾음
이전 코드에는 이 정의가 누락되어 있어, 자바스크립트가 존재하지 않는 객체에서 type 속성을 읽으려다
에러(undefined)를 냄
[ 🔵 해결: ]
1. 딕셔너리 구조 채택
SWAGGER_UI_SETTINGS를 파이썬 딕셔너리 형식으로 수정하여 라이브러리가 각 설정 항목(filter, deepLinking 등)을
정확히 인식하고 자바스크립트 객체로 변환할 수 있게 함
2. 인증 스키마 명시 (APPEND_COMPONENTS)
securitySchemes를 통해 BearerAuth가 http 타입이고 bearer 방식임을 명확히 선언
이를 통해 스웨거 UI가 인증 토큰을 어디에 어떤 방식으로 담아 보내야 하는지 알게 되어 "Try it out" 기능이 정상화됨
authorizations에 2개

[ 🔴 문제: ]
Swagger UI 상단의 'Authorize' 버튼을 눌렀을 때, 동일한 JWT 인증 기능을 수행하는
'BearerAuth'와 'jwtAuth'가 중복해서 나타나는 현상.
이로 인해 사용자가 어떤 곳에 토큰을 넣어야 할지 혼란을 느낌
[ 🟡 원인: ]
두 설정의 '충돌'이 원인
1. 자동 감지: drf-spectacular는 REST_FRAMEWORK 설정의 DEFAULT_AUTHENTICATION_CLASSES를
보고 "JWT를 쓰시네요? 제가 'jwtAuth'라는 이름을 알아서 만들어 드릴게요"라며 자동 생성
2. 수동 설정: SPECTACULAR_SETTINGS의 APPEND_COMPONENTS 항목에 직접 'BearerAuth'라는 이름으로 보안 설정을 정의
결과적으로 라이브러리가 만든 것과 본인이 만든 것이 합쳐져 두 개가 출력된 것
[ 🔵 해결: ]
SPECTACULAR_SETTINGS에 "AUTHENTICATION_WHITELIST": [] 설정을 추가하여 해결
이 옵션은 라이브러리가 프로젝트의 인증 클래스를 스스로 분석해서 자동으로 보안 스키마를 생성하는 것을 '차단'함
자동 생성이 막히면서 'jwtAuth'는 사라지고, 수동으로 정의한 'BearerAuth'만 명세서에 남게 되어 깔끔하게 하나로 통합된 것