본문 바로가기

졸업 프로젝트

Django 기초 + 유저 기능 만들기

# 개발 전 git 관리

 

졸업 프로젝트 기능과 디자인을 구체화했으니 기능을 만든다.

먼저, 기능을 구별해서 만들기 위해 branch를 분리했다. 기능이 완성될 때마다 merge를 해준다.

원래 다른 사람들이 체크하고 충돌안나는지 확인하고 복잡하지만 1인 개발인 나는 그런거 신경쓰지 않는다!

 

branch는 기능별/개발자별 등 개발 스타일대로 분리하기

작업할 branch 업데이트 완료!

commit하고 push할 때 따로 만들어줘도 된다. 근데 그냥 지금 옮김.

 


# 장고란?

Django는 가장 많이 사용되는 Python 기반 웹 프레임워크 중 하나이다.

MTV 패턴으로 제작하고, Model과 DB의 연동이 쉽다.

또 관리자 페이지나 여러 편리한 로직을 따로 제공하여 처음 접하기 쉬움. 

게다가 ORM을 제공한다. 쉽고 Model을 짜면 Django가 알아서 DB를 짜준다고 보면된다.

물론 그만큼 무겁고 활용도가 떨어질 수 있다..

 

MTV와 MVC의 차이가 궁금하면 아래 참고

https://tibetsandfox.tistory.com/16

 

장고(Django) - MVC패턴과 MTV패턴

MVC패턴과 MTV패턴 웹 개발을 공부하시는 분이라면 MVC패턴에 대해 한 번쯤은 들어보셨을 겁니다. MVC(Model-View-Controller)는 *디자인 패턴중의 하나로 프로젝트의 구성 요소를 Model(모델), View(뷰), Contro

tibetsandfox.tistory.com

 

 


# User App 만들기 

 

저번에 Django 프로젝트를 만들었다. 프로젝트는 작은 기능들(App)이 이루어져서 만들어진다.

오늘 프로젝트 기능 중 하나인 User 기능을 만들 것이다.

간단하게 정리함

팀원들과 상의를 통해 얻은 위 표를 통해 ERD를 작성함.

제약 조건은 피그마를 바탕으로 개발하면서 걸겠음.

 

 

먼저 필요한 라이브러리를 설치한다. 이전에 설치한 것에 추가로 설치.

# git bash
pip install djangorestframework djangorestframework-simplejwt

djangorestframework, 줄여서 DRF는 Django에서 Restful API를 구축할 수 있도록 도와주는 라이브러리이다.

다른 프레임워크와 소통하기 위해서는, 웹이 JSON 형식으로 CRUD 데이터를 주고받아야 한다.

이를 위해 라이브러리를 설치하고 Serializer가 직렬화를 수행하여 전송 가능한 형식(JSON)으로 보낸다.

djangorestframework-simplejwt는 유저가 토큰으로 정보를 주고받기 위해 설치!

 

먼저 유저관련 기능을 수행할 accounts앱을 만든다.

python manage.py startapp [앱 이름]으로 만들 수 있다.

완성되면 이런 폴더가 생성된다.

settings.py의 INSTALLED_APPS에 만든 앱과 라이브러리를 작성한다.

또, 곧 만들 User 모델을 사용하기 위해 AUTH_USER_MODEL을 설정한다.

AUTH_USER_MODEL = 'accounts.User' # 커스텀한 유저 모델 사용

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework',
    'rest_framework_simplejwt',

    "accounts",
]

 

 


# MODEL 만들기

 

이제 ERD를 바탕으로 model을 만든다. 

Django는 기본 유저모델 기능이 아주 잘 지원된다. 따라서 기본 유저모델을 바탕으로 확장시킬 것이다.

아래에 user관련 설명을 정리해 두었다.

CEOS Readme 정리 - 4주차:DRF2 : Simple JWT & Permission (tistory.com)

 

CEOS Readme 정리 - 4주차:DRF2 : Simple JWT & Permission

Readme 파일에만 정리했는데 나중에 볼려고 블로그에도 정리함 4주차 CEOS 17기 백엔드 스터디 Q1. 로그인 인증 Session ID + Cookie 먼저, 쿠키는 서버와 클라이언트가 연결되면 자동으로 생성되고, 유저

jain5379.tistory.com

 

 

from django.db import models
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.core.validators import MinValueValidator, MaxValueValidator
import uuid


# Create your models here.


class User(AbstractUser):

    gender_list = (
        ('여', '여'),
        ('남', '남')
    )

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    username = models.CharField(max_length=20, unique=True) 
    nickname = models.CharField(max_length=20)
    email = models.EmailField(unique=True)
    gender = models.CharField(max_length=2, choices=gender_list, default='여')
    age = models.IntegerField(validators=[MinValueValidator(10), MaxValueValidator(80)])

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['password', 'nickname', 'email', 'gender', 'age']

    def __str__(self):
        return f'{self.username}'
  • id : 기본으로 제공하지만, 보안을 위해 id가 아닌 UUIDfield로 생성하였다. 이것을 PK로 사용한다.
  • username : 기본으로 제공하지만, max_length수정을 위해 사용하였다. 아이디로 사용하므로unique=True 필수
  • nickname : 최대 길이 제한
  • email : email형식에 맞아야하므로 Emailfield사용
  • gender : '여' 혹은 '남' 중에서만 고르도록 설정하였다.
  • age : 최소/최대 나이를 설정하였다. 우리 서비스는 정확한 유저 정보가 중요하므로!
  • USERNAME_FIELD는 아이디로 사용할 필드를 선택하고, REQUIRED_FILEDS는 필수 필드를 선택한다.

 

비밀번호는 Django에서 제공하는 것 그대로 사용하므로 따로 건드리지 않았다.

Django에서 강력한 규칙과 암호화를 제공한다.

# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

settings.py에서 내용을 확인할 수 있고, 해당 파일에 들어가 수정도 가능하다.

순서대로 username/id/email과 유사성 확인, 최소 8글자, 흔한 password인지 비교, 숫자로만 이루어진 비밀번호 금지 등의 규칙이다.

 

아무튼 이렇게 원하는 모델을 만들어준 후

python manage.py makemigrations

python manage.py migrate

를 하면 DB에 모델이 올라간다.

 

가끔 django.db.migrations.exceptions.InconsistentMigrationHistory 이런 오류가 뜰 수 있다.

이건 User 모델 커스텀 적용 전 superuser를 만들었다던가~하는 이유로 의존성 문제가 뜨는 것이다.

migrations 파일 삭제 후 DB도 삭제 후 다시 위 명령어를 입력하면 된다.

 

# admin.py

from django.contrib import admin
from .models import User

# Register your models here.

admin.site.register(User)

 

이제 admin.py 파일에 User 모델을 올려서 관리자 페이지에서 확인해본다.

 

관리자 페이지는 superuser로 들어가야 하므로 python manage.py createsuperuser를 입력하여 관리자 계정을 만든다.

이제 runserver 후 admin페이지에 들어가보자.

 

기존의 Django model에 내가 추가한 필드가 잘 추가되었다. 참고로 굵은 글씨가 필수입력요소이다.

 


# Serializer(직렬화)

 

위에서 설명한대로, DRF는 직렬화를 제공한다.

Django는 파이썬 객체의 형태로 저장하므로, views.py의 함수를 통해 정보를 전달받고, 이를 직렬화해 저장해주는 과정이 필요하다.

직렬화를 수행할 serializers.py파일을 accounts.py에 생성한다.

 

먼저 회원가입 함수이다.

from rest_framework import serializers
from .models import User

class SignUpSerializer(serializers.Serializer):
    class Meta:
        model=User
        fields=['id', 'username','password','nickname', 'email', 'gender', 'age']
        extra_kwargs = {"password": {"write_only":True}}

    gender_list = (
        ('남', '남'),
        ('여', '여')
    )

    username = serializers.CharField(max_length=20) # 아이디
    password = serializers.CharField()
    email = serializers.EmailField()
    nickname = serializers.CharField(max_length=20) # 이름
    gender = serializers.ChoiceField(
        choices=gender_list
    )
    age = serializers.IntegerField()

    def create(self, validated_data):

        if User.objects.filter(username=validated_data['username']).exists() or User.objects.filter(email=validated_data['email']).exists():
            raise serializers.ValidationError('username 존재 or email 존재')


        else:
            user = User.objects.create(
                username=validated_data['username'],
                nickname=validated_data['nickname'],
                email=validated_data['email'],
                gender=validated_data['gender'],
                age=validated_data['age']
            )
            user.set_password(validated_data['password'])
            user.save()
            return user

 class Meta: 에는 해당 serializer가 사용할 정보를 제공한다.

model은 User를 사용하고, 직렬화를 수행할 모델의 필드를 적는다.(전부면 위와 같이 적고, 아니라면 ['필드이름1', '필드이름2']와 같이 작성해준다.)

extra_kwargs에는 기타 적용할 특성을 적는다. 비밀번호는 작성만 가능하도록 했다.

참고로 validate는 유효한 입력(객체)인지 확인하기 위해 적은 것이다.

 

filter()로 username 혹은 email이 존재하는지 확인하고, 존재하지 않는다면 create() 함수로 유저를 생성한다.

set_password()를 이용하여 비밀번호를 암호화하고, save()를 통해 저장된 객체를 직렬화하여 DB에 저장한다!

 

다음은 로그인 함수

class LoginSerializer(serializers.ModelSerializer):
    
    class Meta:
        model = User
        fields=['id', 'username','password','nickname', 'email', 'gender', 'age']
        extra_kwargs = {"password": {"write_only":True}}


    def validate(self, data):
        username = data.get("username", None)
        password = data.get("password", None)

        if User.objects.filter(username=username).exists():
            user = User.objects.get(username=username)
            if not user.check_password(password):
                raise serializers.ValidationError('잘못된 비밀번호입니다.')
            else:
                return data
        else:
            raise serializers.ValidationError('존재하지 않는 사용자입니다.')

로그인은 인증이 중요하다.

username(아이디)와 password만 입력받는다.

username이 존재한다면 check_password() 함수를 이용해 비밀번호가 맞는지 확인한다.

만약 존재하지 않거나 password가 맞지 않으면 error를 출력한다.

 


# View 만들기

이제 views.py에 실제로 회원가입/로그인을 수행할 View를 만든다.

from django.contrib.auth import authenticate
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

from .serializers import *
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.status import *
from rest_framework.permissions import AllowAny


# Create your views here.

class SignupView(APIView):
    permission_classes = [AllowAny]
    def post(self, request, format=None):
        serializer = SignUpSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response({'message': '회원가입 성공', 'data': serializer.data}, status=HTTP_200_OK)
        return Response({'message': '회원가입 실패', 'data': serializer.errors}, status=HTTP_400_BAD_REQUEST)

    def get(self, request):
        users = User.objects.all()
        serializer = SignUpSerializer(users, many=True)
        return Response({'message': '유저 목록 조회 성공', 'data': serializer.data}, status=HTTP_200_OK)

 

라이브러리는 APIView를 사용했다. 

APIView는 Viewset, GenericAPIView 등 다양한 라이브러리의 기본이 된다.

기본이되므로 적을 것은 많지만, 함수를 수정하고 여러 기능을 추가하기 쉽다. 프로젝트가 약간 크므로 이걸 사용함.

회원가입은 누구나 가능하므로 permission_classes는 [AllowAny]를 사용하였다.

회원가입 기능은 post method를 사용하므로 post 함수에서 정의했다.

is_valid()를 이용하면 간단하게 serializer가 유효한지 확인할 수 있다 유효하면 저장 후 회원가입 성공, 유효하지 않으면 error 반환!

여기에 유저 목록 조회를 위한 get 메소드도 추가했다. 있으면 편리함

 

로그인과 로그아웃 API 만들기

#로그인 함수
class LoginView(APIView):
    permission_classes = [AllowAny]
    def get(self, request):
        user = get_object_or_404(User, id=request.user.id)
        serializer = LoginSerializer(user)
        return Response({'message': '현재 로그인된 유저 정보 조회 성공', 'data': serializer.data}, status=HTTP_200_OK)

    def post(self, request):
        user = authenticate(
            username=request.data.get("username"), password=request.data.get("password")
        )
        if user is not None:
            serializer = LoginSerializer(user)
            token = TokenObtainPairSerializer.get_token(user)
            refresh_token = str(token)
            access_token = str(token.access_token)
            res = Response(
                {
                    'message': '로그인 성공',
                    "user": serializer.data,
                    "token": {
                        "access": access_token,
                        "refresh": refresh_token,
                    },
                },
                status=status.HTTP_200_OK,
            )
            res.set_cookie("access", access_token, httponly=True)
            res.set_cookie("refresh", refresh_token, httponly=True)
            return res
        else:
            return Response({'message': '로그인 실패'}, status=status.HTTP_400_BAD_REQUEST)

#로그아웃 함수
class LogoutView(APIView):

    def post(self, request):
        response = Response({
            "message": "로그아웃 성공"
        }, status=status.HTTP_202_ACCEPTED)
        response.delete_cookie('refresh')
        response.delete_cookie('access')

        return response

get_object_or_404() 메서드를 활용하면 객체가 존재하는지 확인하고 error를 반환하도록 간단하게 만들 수 있다.

먼저 get 메소드는 현재 로그인된 유저 정보 반환!

 

다음은 중요한 post 메소드!

먼저 authenticate() 함수를 이용하여 username과 password가 유효한지 인증한다.

유효하다면 TokenobtainPairSerializer()를 이용하여 refresh_token과 access_token을 발급한다.

발급받은 token은 user 정보와 함께 return 한다.

또한 login할 때 set_cookie로 cookie에 토큰을 저장한다. (이 이후는 프론트가 잘 처리하더라...)

 

로그아웃은 다른건 없고

cookie에 있는 token만 삭제해준다.

 


# URL 만들기

이제 URL을 만들면 API가 잘 작동하는지 확인 가능!

# dislodged_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
]


# accounts/urls.py
from django.urls import path
from .views import *
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

app_name = 'accounts'

urlpatterns = [
    path('signup/', SignupView.as_view()),
    path('login/', LoginView.as_view()),
    path('logout/', LogoutView.as_view()),

    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('refresh/token/', TokenRefreshView.as_view(), name='token_refresh'),

]

원하는 url에 view를 연결한다.

나는 앱별로 url을 관리할 것이므로, settings.py가 있는 기본 프로젝트 폴더에 사용할 앱의 url을 명시한 후 앱에 urls.py 파일을 만들고 url을 작성해준다.

클래스형 뷰를 사용할 때는 as_view()를 써준다.

 

Simplejwt에 내장된 api를 사용할 수 있다.

TokenObtainPairView는 refersh_token을 바탕으로 새로운 access token을 발행할 때 사용

TokenRefreshView는 새로운 refresh token을 발행할 때 사용한다.

 


# 확인

runserver 후 확인하기

Django는 참 친절하다. DEBUD=True하면 볼 수 있는 화면

Signup API가 잘 된다. 

내가 만든 GET과 POST가 가능하다.

하지만 웹브라우저에서는 작성이 불편하므로 

따로 API 플랫폼을 이용하여 시험할 것이다!

 

postman 사용

일단 error도 알맞게 뜬다.

잘되는고만
같은 data로 post할때

 

 

로그인 도전

내가 만든 쿠키~

token확인!

 

이제 token을 가지고 놀아보자.

 


# token 설정하기!!

토큰에는 유저 정보가 담겨있다!!

무엇이든지 유저관련 정보를 다루기 위해서는 Token에 access token 혹은 refresh token을 넣어줘야 한다.

필요한 라이브러리도 깔고, token 발급 함수도 만들었지만 가장 중요한 설정을 해주어야 한다.

# settings.py

from datetime import timedelta

CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True # 쿠키 허용

#인증 방식
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES' : [
         'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ]
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': False,
    'UPDATE_LAST_LOGIN': False,

    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    'JWK_URL': None,
    'LEEWAY': 0,

    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',

    'JTI_CLAIM': 'jti',

    'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
    'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

default 인증방식은 JWT인증방식을 사용하고,

기본적으로 내 프로젝트는 rest_framework.permissions.IsAuthenticatedOrReadOnly가 된다.

즉, 인증된 사용자가 아니면 READ만 가능하도록!  근데 권한은 나중에 따로 만들어서 설정할거라...필요없긴 함.

아래 설정은 LIFETIME만 주목하면 된다. 그냥 token의 유효시간이라고 보면 됨.

 

설정 후 다시 runserver 후 로그인 후 token 복사하기

복사 후 login URL에 GET 메소드 요청을 보낸다. 인증 방식을 Bearer Token으로 선택 후 access token에 로그인 한 유저의 token을 입력하면 된다.

와아~

새 access token도 알맞게 발급된다.

Authorization에 token 넣고 아이디랑 비밀번호 입력해주면된다.

 

 

로그아웃도 잘된다.

cookie에 token이 삭제 된것을 확인할 수 있다.

 


# 회고

 

유저 기능 끝~

사실 token blacklist나 permission이나...약간 수정해야할 코드들이 많다.

근데 프론트에서 빠르게 넘겨달라고 했고, 어짜피 수정해야할 것 같아서 최대한 빠르고 간단하게 작성했다.

API 명세서도...저거 말고 따로 작성해야 하고ㅜㅜㅜ

 

노력의 흔적

오랜만에 하니까 Django 환경변수가 anaconda랑 충돌이 나면서 에러가 발생하기도 하고, 좀 까먹은 것도 있어서 시작에서 애를 먹었다.

하다보면 익숙해지겠지 머

 

진짜 진짜 끝!!

error 잡고 회의하면서 코드 좀 수정하고 API명세서 작성까지 한 후에 merge할 예정

힘들구만..error 생기면 코드도 다시 수정해야하구...

힘내야지