[TIL / Django] Westagram - 회원가입 엔드포인트

haejun-kim·2020년 8월 4일
8

[Django]

목록 보기
13/20
post-thumbnail

위코드 3주차. Foundation Week에 들어서면서 본격적으로 Django를 시작했다. 프론트앤드 분들이 만들고 계신 인스타그램의 클론인 westagram 의 백엔드 부분을 이제 Django를 통해 API를 만들어서 다음주에 프론트앤드분들이 만든 부분과 붙여보는 시간이 될거기에 굉장히 중요한 시기라고 생각하고 빨리 가기보단 최대한 이해하고 내 손으로 하나하나 코드를 짜보는 시간으로 만들어보려고 한다. ( 말은 이렇게 해도 잘하시는 분들 보면 조바심도 생기고, 부러운 마음도 생기는건 어쩔 수 없나보다 .)

Django 초기 세팅

Guide에 따라서 초기 세팅실시. 초기 세팅부분에서 신경써야 할 부분은 다음과 같다.

  • 사용하지 않는 부분에 대해서는 주석보단 삭제
  • Align 항상 신경 쓰기
  • ALLOWED_HOSTS = [] 빈값으로 두면 어떠한 아이피도 접근을 못하기 때문에 적절히 표현해줄 것

상기 사항들을 항상 주의해서 프로젝트 초기 셋팅 신경쓰도록 하자.

User App 생성

프로젝트 생성 및 세팅을 했으니 실제 프로젝트의 기능을 담당할 앱을 생성해주어야 한다. 회원 가입 앱에서 필수적으로 구현해야 할 사항으로 다음과 같은 미션이 주어졌다.

  • 인스타그램에 회원가입 할 때에는 전화번호, 사용자 이름 또는 이메일이 필수로 필요합니다.
  • 인스타그램에 회원가입 할 때에는 비밀번호도 필수로 필요합니다.
  • 이메일이나 패스워드 키가 전달되지 않았을 시, {"message": "KEY_ERROR"}, status code 400 을 반환합니다.
  • 회원가입시 이메일을 사용할 경우, 이메일에는 @와 .이 필수로 포함되어야 합니다. 해당 조건이 만족되지 않을 시 적절한 에러를 반환해주세요. 이 과정을 email validation이라고 합니다.
  • 회원가입시 비밀번호는 8자리 이상이어야만 합니다. 해당 조건이 만족되지 않을 시, 적절한 에러를 반환해주세요. 이 과정을 password validation이라고 합니다.
  • 회원가입시 서로 다른 사람이 같은 전화번호나 사용자 이름, 이메일을 사용하지 않으므로 기존에 존재하는 자료와 중복되어서는 안됩니다. 적절한 에러를 반환해주세요.
  • 회원가입이 성공하면 {"message": "SUCCESS"}, status code 200을 반환합니다.
  • [추가 구현 사항]: email validation 또는 password validation 과정에서 정규식을 사용해보세요.

Model

가장 먼저 모델을 생성해주자. 회원가입을 하기 위해서 필요한 정보는 전화번호 or 사용자 이름 or email 로 주어졌다. 이 중에서 나는 이메일을 선택했는데, 다음 미션에서 이메일의 정규식과 관련된 미션이 있었기에 정규식을 사용해보고 싶어서 이메일을 선택했다. 그래서 다음과 같이 모델링을 설정했다.

from django.db import models

from .validation import validate_email, validate_password

class User(models.Model): 
    email          = models.EmailField(
        max_length = 50,
        validators = [validate_email],
        unique     = True,
    )
    password       = models.CharField(
        max_length = 300,
        validators = [validate_password],
    )

django에서 기본적으로 제공하는 Field인 EmailFieldCharField를 사용했다. 최대 길이는 이메일과 패스워드를 각각 50, 300으로 지정했고, 비밀번호는 다른 사용자와 같을 수 있지만 이메일이 중복되선 안되기에 option값으로 unique=True를 지정해주었다. 다른 옵션으로 지정한 validators는 이메일과 패스워드를 입력했을 때, 조건에 만족한 값을 사용자가 입력해주는지 검사해주는 유효성 검사를 해주는 함수를 입력했다. 이 함수에 대해서는 Validation에서 설명하겠다.

Validation

유효성 검사를 실제로 실시하는 함수가 들어있는 파일이다. 실제로 validation.py로 파이썬 파일을 하나 새로 만들어주었고, 그 안에 유효성 검사에 관련된 함수들을 작성한 뒤 모델에서 import해서 필요한 함수들을 사용했다. validation.py는 다음과 같이 작성했다.

import re

from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _

def validate_email(value):
    email_reg = r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
    regex = re.compile(email_reg)

    if not regex.match(value):
        raise ValidationError(
            _('%(value)s is not a valid number'),
            params = {'value' : value},
        )

def validate_password(password):
    if len(password) <= 8:
        raise ValidationError("password is too short.")

먼저 정규표현식을 사용해야하기 때문에 정규표현식에 관련된 reimport해주었다. 그 뒤에 조건에 만족하지 않으면 ValidationError를 발생시켜야하기 때문에 해당 부분을 import해주었다. ugettext_lazy는 아직 잘 이해하지 못했는데, Django 공식문서에서 주어진 예시에 위와 같이 사용해서 나도 공식문서를 보고 사용했다. 이 부분은 추가적으로 공부가 필요한 부분이다.

이메일 정규표현식은 인터넷에 여러가지가 올라와 있지만 여러가지를 실제로 사용해봤을 때, 길긴 했지만 위의 정규 표현식이 가장 정확했다. @ or . 이 없는 경우를 정확히 찾아주었기에 위의 정규 표현식을 사용했다. 정규 표현식을 컴파일해주고, 이메일의 값과 매칭 시켰을 때, 매칭되지 않으면 ValidationError를 발생시킨다. params 부분은 에러가 발생했을 때, 사용자가 입력한 값을 보여주면서 메시지를 띄우기 때문에 어떤 부분에서 잘못됐는지 인식하기 훨씬 수월해진다.

패스워드와 관련된 부분은 제약사항이 8자 이하일 경우 에러 메세지를 출력해줘야했기 때문에 비교적 간단하게 작성했다.

views

import json, traceback

from django.views import View
from django.http import JsonResponse
from django.core.exceptions import ValidationError

from .models import User

class SignupView(View):
    def post(self, request):
        data = json.loads(request.body)

        try: 
            if not data['email'] or not data['password']:
                return JsonResponse({'message' : 'No value entered'}, status = 400)
            signup_user = User(
                email    = data['email'],
                password = data['password'],
            )
            signup_user.full_clean()
            signup_user.password = signup_user.password.encode('utf-8')
            signup_user.password = bcrypt.hashpw(signup_user.password, bcrypt.gensalt())
            signup_user.password = signup_user.password.decode('utf-8')
            signup_user.save()
            return JsonResponse({'message' : 'SUCCESS'}, status = 200)
        except ValidationError as e:
            trace_back = traceback.format_exc()
            print(f"{e} : {trace_back}")
        except KeyError:
            return JsonResponse({'message' : 'KEY_ERROR'}, status = 400)
        
        return JsonResponse({'message' : 'Invalid format or Duplicated Email'}, status = 400)

    # 유저 리스트
    def get(self, request):
        user = User.objects.values()
        return JsonResponse({'user' : list(user)}, status = 200)

views.py는 위와 같이 작성했다. 여기서 가장 어려웠던 부분은 full_clean()과 예외처리에 대한 부분이였다.

먼저 입력받은 데이터를 json 형식의 파일로 받아서 data라는 변수에 넣어준다. 그 다음의 if문은 이메일이나 패스워드가 빈값으로 들어왔을 때 반환해 줄 에러 메시지와 관련되어 있다. data['email'] 이나 data[password'] 값이 없을 경우(Flase) 입력 된 메세지가 없다는 뜻으로 No value entered 라는 메시지와 함께 400 에러를 반환해준다.

또한 프론트앤드와 백엔드가 JSON 방식으로 통신을 하기 때문에 서로의 KEY값이 같아야 하는데, KEY값이 같지 않을 경우 ( email이라는 키를 잘못 입력했을경우 ) KeyError가 발생하는데 exceptKEY ERROR가 발생했을 경우 에러코드와 적절한 메세지를 반환해주는 코드를 작성했다.

여기서 400 에러는 잘못된 구문 요청이나, 유효하지 않은 메시지 프레이밍 등의 오류를 감지해 요청을 처리할 수 없거나, 처리하지 않는다는 의미의 에러코드이다.

if문을 정상적으로 통과했다는것은 데이터가 빈칸이 아닌 어떤 값이 들어왔음을 의미한다.

여기서 이제 full_clean()을 통해 실제 유효성 검사를 실시한다. 이 부분이 많이 어려웠는데, model에서 생성한 class에 validation이라는 옵션을 주는것은 보통 form에 관련된 경우 사용을 한다. 따라서 지금 나의 상황처럼 form이 아닌 파일에서 유효성 검사를 실제로 실시하지 않는다. 이럴 때 full_clean() 메소드를 사용해주면 form이 아니여도 유효성 검사를 실시해주는 메소드이다. 따라서 유저가 입력한 정보를 받아서 signup_user라는 객체를 생성했고, 해당 객체에서 full_clean()메소드를 사용하여 유효성 검사를 실시했다. 만약 유효성 검사에서 ValidationError가 발생하게 되면 except 구문으로 예외처리에 들어간다. traceback은 호출 순서와 발생한 예외를 확인할 수 있는 역할을 한다. 사용하는 방법에는 두가지 방법이 있는데 다음의 블로그에서 확인했다.

[Python] Exception 처리와 추적 (디버깅)

  1. print_exc() : 화면에 바로 출력
  2. format_exc() : 문자열로 반환하여 그 결과를 로깅 설정과 레벨에 따라 화면 또는 파일 등에 출력할 수 있는 방법 ( 권장되는 방법 )

나는 2번 방법으로 사용했다. ( 권장되는 방법이라길래.. )

이렇게 처리한 예외를 출력해주도록 하였고, 가장 마지막의 return 문에 JsonResponse로 하여 400

의 에러코드와 적절한 에러 메세지를 띄워주도록 구현하였다.

만약 유효성 검사에서 문제가 생기지 않아 정상적으로 try가 작동하였다면 else: 부분으로 넘어가서 받아온 유저 정보를 저장하고, SUCCESS 메시지와 함께 200 코드를 출력해준다.

bcyrpt를 통해서 암호화 한 부분음 다음에서 확인할 수 있다.

비밀번호 암호화 및 token 발행 ( bcrypt, JWT )

westagram/urls

from django.urls import path , include

urlpatterns = [
    path('', include('User.urls')),
]

User 앱의 urls로 연결시켜주었다.

User/urls

from django.urls import path

from .views import SignupView

urlpatterns = [
    path('user', SignupView.as_view()),
]

View에서 생성한 SignupView 클래스를 임포트해주고 뷰로 보이도록 메소드를 설정했다. 그리고 user와 관련된 엔드포인트이기 때문에 user로 경로를 설정해줬다.


추가적으로 공부해야할 사항

ugettext_lazy

공식 문서에 예제로 나와있어서 사용한 케이스. 어떤 기능때문에 사용했는지 정확히 모르기 때문에 추가적인 정리가 필요한 부분.

Translation | Django documentation | Django

full_clean()

현재 알고 있는 내용은 위에서 설명한대로 form이 모델에서 바로 유효성 검사를 실시하고 싶을 때, 사용하는 메소드이다. 이 메소드를 사용해주어야 실제로 유효성 검사를 실시한다. 이 clean()과 관련된 메소드는 세가지가 있었는데 이 세가지를 모두 포함하고 있는 메소드가 full_clean()이였기 때문에 사용했다. 세가지의 내용에 대해서는 다음에서 확인할 수 있다. 나도 추가적인 공부가 필요한 부분이기에 따로 남겨둔다.

Model instance reference | Django documentation | Django


리뷰 후 수정 사항

대부분의 리뷰가 Align과 import 여러개 했을 때는 개행을 시켜주는 것이 훨씬 가독성이 좋다는 피드백을 받았다. 그래서 정렬이나 import 개행이 이루어져 있지 않은 파일들에 대해서는 수정을 했다. 그 외 회원가입 관련한 수정사항은 다음과 같다.

Error Exception 이후에 else구문이 오는 것은 다소 어색합니다. signup_user가 저장되는 것은 제일 위 if 즉, key error가 아닐 경우 바로 진행되어도 되므로 try 문 내부로 넣어주세요 :)

  • 기존 코드
        try: 
            signup_user = User(
                email = data['email'],
                password = data['password'],
            )
            signup_user.full_clean()
        except ValidationError as e:
            trace_back = traceback.format_exc()
            print(f"{e} : {trace_back}")
        else:
            signup_user.save()
            return JsonResponse({'message' : 'SUCCESS'}, status = 200)

회원 가입 시 이메일과 패스워드를 입력하는데 그 과정에서 유효성 검사를 통과하지 못하면 에러 발생, 통과하면 SUCCESS 문구를 출력해주는 구문이다. 멘토님의 수정사항으로는 try 뒤에 else 가 나오는것이 다소 어색하다고 하셨다. 멘토님의 리뷰를 보고 나서 생각하지 못했던 부분이라 바로 수정에 들어갔다.

  • 수정 코드
				try: 
            signup_user = User(
                email    = data['email'],
                password = data['password'],
            )
            signup_user.full_clean()
            signup_user.save()
            return JsonResponse({'message' : 'SUCCESS'}, status = 200)
        except ValidationError as e:
            trace_back = traceback.format_exc()
            print(f"{e} : {trace_back}")

full_clean()구문에서 유효성 검사를 실행하고 정상적이면 바로 저장하고 SUCCESS 메세지를 반환해준다. 만약 full_clean()에서 유효성 검사를 통과하지 못하면 바로 except 로 넘어가기 때문에 else가 필요하지 않았다. 실제로 수정 후 동작을 확인했을때도 정상적으로 동작했다.

당연시 생각했던 부분에 대해 새로운 시각을 길러주시는 것 같다. 🙂

0개의 댓글