Django | 인스타그램 클론 코딩(1) - 회원가입

Sua·2021년 2월 7일
2

Django

목록 보기
8/23
post-thumbnail
post-custom-banner

🙋‍♀️인스타그램 회원가입 기능 구현

백엔드 관점에서 인스타그램의 회원가입 기능을 구현해보도록 하겠습니다. 기본적인 기능만 구현할 수 있지만 저는 최대한 실제 인스타그램과 유사하게 만들어보고 싶어서 도전해보았습니다.

우선 인스타그램 홈페이지에 들어가서 회원가입이 어떤 식으로 이루어지는지 파악을 해봐야겠죠? 직접 회원가입을 시도해보면서 어떤 형식의 이메일을 넣었을 때는 가능한지 등 여러 조건을 파악해보시길 추천드립니다. 수없이 많은 사이트에 회원가입을 해봤겠지만 백엔드 개발자 입장에서 생각해보면 좀 다를 것입니다.

제가 파악한 조건들을 아래에 설명해놓았습니다.

🔎 회원가입 기능 분석

입력 정보

  1. 입력받을 수 있는 정보는 Mobile Number, Email, Full Name, Username, Password입니다.
  2. Mobile NumberEmail은 둘 중 하나만 입력하면 됩니다. 나머지 정보는 꼭 입력되어야 합니다.

회원가입할 때 받은 데이터는 로그인할 때 어느 데이터를 사용하냐에 따라 조건이 달라질 수 있습니다. 이 내용은 Model을 작성할 때 자세히 다루도록 하겠습니다.

Validation 조건

  1. Mobile Number는 숫자로만 입력합니다. 중간에 - 등이 들어가면 안 됩니다.
  2. email은 이메일 형식에 맞춰서 입력합니다. @, .이 들어가는 식이어야 합니다.
  3. username에는 소문자, 숫자, _, .이 들어갈 수 있는데, 특히 소문자가 하나 이상 들어가야 합니다. (사실 _이나 .으로 끝나면 안 되는 등 세세한 조건이 있지만 여기서 다루지는 않겠습니다. 정규식을 깊게 들어가야 하기 때문입니다.)
  4. password는 8자리 이상이어야 합니다.

🛠 App 생성

(이미 westagram이라는 프로젝트가 생성되어 있는 상황입니다.)
기능 분석이 완료가 되었으니 회원정보를 관리할 수 있는 user라는 앱을 만들도록 하겠습니다.

python manage.py startapp user

settings.pyINSTALLED_APPS에 생성한 앱을 등록합니다.

# westagram/settings.py

INSTALLED_APPS = [
    # 'django.contrib.admin',
    # 'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'user',       ---> 입력!
]    

🖌 Model 작성

# user/models.py

from django.db import models

class User(models.Model):
    email         = models.CharField(max_length=100, unique=True, null=True)
    mobile_number = models.CharField(max_length=100, unique=True, null=True)
    full_name     = models.CharField(max_length=100)
    username      = models.CharField(max_length=100, unique=True)
    password      = models.CharField(max_length=300)
    created_at    = models.DateTimeField(auto_now_add=True)
    updated_at    = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'users'

Q. email, mobile_number, usernameunique=True를 지정한 이유는?
로그인할 때 필요한 정보를 살짝 들여다 봅시다.
email, mobile_number, username 이 세 가지 정보 중 하나를 id로 사용할 수 있습니다. 각각의 값은 중복된 값이 없어야(unique=True이어야) id로서 역할을 할 수 있겠지요.

Q. email, mobile_numbernull=True를 지정한 이유는?
회원가입 시 emailmobile_number 중에 하나의 정보만 받을 수 있습니다. 당연히 각각의 값은 들어오지 않을 수 있으므로 null=True 지정해주었습니다.

Q. passwordmax_length=300을 지정한 이유는?
비밀번호는 이후에 암호화 과정을 거치면서 DB에 저장되는 값이 길어질 수 있습니다. 넉넉히 max_length=300을 지정해주었습니다.

Q.created_at, updated_at 필드를 작성한 이유는?
보통 회원가입을 관리할때에는 시간정보가 중요합니다. created_at에는 처음 생성된 시점, updated_at에는 업데이트된 시점을 기록하는데, 날짜와 시간을 가져오는 필드인 DateTimeField를 이용합니다. auto_now_add = True를 넣어주면 처음 생성(추가)된 시점을 자동으로 기록해주고, auto_now = True를 넣어주면 필드가 업데이트 될 때마다 그 시점을 기록해줍니다.

makemigrations & migrate

Model 작성이 완료되었으면 makemigrations로 migration 파일을 만들어줍니다.
manage.py가 위치한 디렉토리(루트 디렉토리)에서 아래의 명령어를 입력합니다.

python manage.py makemigrations user

makemigrations 뒤에 마이그레이션을 할 앱(user)을 지정해주는게 좋은데, 만약 앱 모델 간에 참조 관계가 있는 경우 순서가 중요할 때가 있기 때문입니다.

migration 파일이 생성이 되었으면 migrate 명령어로 변경된 사항을 실제 DB에 적용할 수 있습니다.
하지만, python manage.py sqlmigrate user 0001 명령어로 실제로 어떤 sql문이 적용되는지 확인하는 습관을 가지는 게 좋습니다.

확인 했으면 migrate로 실제 DB에 변경 사항을 적용해봅시다.

python manage.py migrate user

💣 View 작성

제가 작성한 회원가입 뷰를 보여드리기 전에 어떤 방식으로 작성했는지 설명드리겠습니다. 크게 다섯 부분으로 나눌 수 있습니다.

Part1. try, except 쓰지 않고 KeyError 처리하기

data = json.loads(request.body)

회원가입에 필요한 정보는 Json 형식으로 request의 body에 담겨서 옵니다. 그걸 json.loads()로 장고에서 사용할 수 있도록 파이썬 딕셔너리 형식으로 바꾸어 data라는 변수에 담습니다.

하지만 항상 우리에게 필요한 모든 정보가 전달되지 않을 수도 있습니다.

email = data['email']

예를 들어 data['email']로 이메일 정보를 가져오려고, email이 request body에 없다면?! 이 코드는KeyError를 뿜어내고 로직은 중단됩니다.

따라서 KeyError를 처리하기 위해서는

try:
    email = data['email]
except KeyError:
    return JsonResponse({'message':'KEY_ERROR'}, status=400)

이런 식으로 try, except를 사용하는 방법이 있을 수 있겠으나 저는 다른 방법을 사용하였습니다.

딕셔너리 get 메소드

바로 딕셔너리의 get 메소드를 이용하는 방법입니다.

email = data.get('email', None)

get메소드는 딕셔너리.get(키, 기본값) 형식으로 딕셔너리 안에 찾으려는 Key 값이 없을 경우 기본값을 대신 가져옵니다. 위 예시에서는 dataemail이라는 키가 없으면 에러 대신 None을 가져옵니다.

if문

이제 에러 발생은 막았으니, KeyErorr가 발생했다는 것을 프론트 쪽에 알려야겠지요? if문을 사용합니다.

if not email:
    return JsonResponse({'message':'KEY_ERROR'}, status=400)

email이 없으면 None을 반환하는데, 파이썬에서 NoneFalse입니다. if는 조건식이 True일 때 아래 코드를 실행하므로, emailnot을 붙여 True로 만들어 줍니다.

딕셔너리의 get 메소드와 if not을 이용해 KeyError를 처리합니다!

Part2. JsonDecodeError

저는 JsonDecodeError 받아야 할 정보들이 있는데, 아무런 정보도 전달되지 않았을 때 발생하는 에러라고 정의하였습니다. json.loads()로 Json 형식으로 받은 정보를 decode하는데, decode할 값이 없으니 에러가 발생하는 것입니다.

예를 들어 httpie를 이용하여

http -v POST 127.0.0.1:8000/user/signup 

이렇게 보내면 JsonDecodeError가 발생합니다.

받을 정보가 있는 POST에서 발생하므로, GET에서는 따로 처리를 안 해도 괜찮습니다.

Part3. Validation

정규표현식

정규표현식을 사용하여 Validation 조건을 작성하였습니다. 장고에서는 EmailValidator 등 내장된 Validator가 있습니다만, 장고 이외에 어떤 툴을 사용하더라도 사용할 수 있는 정규표현식을 익히는 것이 중요합니다.

(제가 정규식을 익힌 사이트는 파이썬 코딩 도장연오의 파이썬이라는 곳입니다.)

email_pattern         = re.compile('^[^@]+@[^@]+\.[^@]+$')
mobile_number_pattern = re.compile('^[0-9]{1,15}$')
username_pattern      = re.compile('^(?=.*[a-z])[a-z0-9_.]+$')

상수

비밀번호는 8자리 이상이기만 하면 되므로 상수를 사용하였습니다. 상수는 코드 중간에 선언하는 것보다 함수 바깥 혹은 임포트 아래에 선언해주는 것이 좋습니다.

from user.models import User


PASSWORD_MINIMUM_LENGTH = 8

class SingUpView(View):

if문

이제 if문으로 프론트 쪽에 Validation Error가 발생했다는 것을 알려야 합니다. 사실 정규표현식 등으로 직접 만든 Validator의 경우 실제로 ValidationError가 발생하지는 않습니다. 장고에 내장된 EmailValidator 등을 사용할 때 나는 에러이죠. 그럼에도 불구하고 우리는 입력된 값이 우리가 만든 Validation 조건에 맞지 않다는 것을 알려주어야 합니다.

if email:
	if not re.match(email_pattern, email):
		return JsonResponse({'message':'EMAIL_VALIDATION_ERROR'}, status=400)
        
if len(data['password']) < PASSWORD_MINIMUM_LENGTH:
	return JsonResponse({'message':'PASSWORD_VALIDATION_ERROR'}, status=400)

자주 쓰이는 Validation이라면 함수로 따로 만들어서 사용할 수도 있습니다.

Part4. 이미 존재하는 user일 경우

위에서 언급했듯이 로그인을 할 때 idemail, mobile_number, username 중에 하나를 받을 수 있습니다. 따라서 이미 DB에 존재하는 값을 중복해서 받을 수 없습니다.

filter(), exists()

중복 값 체크는 보통 filter()exists()를 이용합니다.

if User.objects.filter(email = data.get('email', 1)).exists():
	return JsonResponse({'message':'ALREADY_EXISTS'}, status=409)

만약 전달된 email 정보가 있다면, 동일한 값을 filter()로 DB에서 찾아내는데, 만약 그 값이 존재한다면 exists()True를 반환합니다.

만약 전달된 email 정보가 없다면, email = data.get('email', 1)에 의해 email1이 됩니다. None이 아니라 1인지 의문이 들 것입니다.

그 이유는 바로 email에는 null값이 존재할 수 있기 때문입니다! 만약 emailNone이라면 기존 DB에 등록되어 있던 null이 같은 값이라고 취급되어 중복값이 존재하는 것으로 처리됩니다. email에 아무런 값이 전달되지 않았는데 이미 존재하는 정보라고 처리되면 안 되겠죠? 그러니 None 대신 임의의 값 1을 넣어주었습니다.


Q 객체

자 여기까지 email이 이미 존재하는지 확인하는 로직을 작성해보았습니다. 그럼 mobile_number, username의 경우도 새로운 if문으로 이어서 작성하면 되겠죠?
물론~ 그렇게 해도 되겠지만 Q 객체를 사용하면 코드의 길이를 확 줄일 수 있습니다! 한 번 사용해보도록 합시다.

if User.objects.filter(
	Q(email         = data.get('email', 1)) |
	Q(mobile_number = data.get('mobile_number', 1)) |
	Q(username      = data['username'])
	).exists():
		return JsonResponse({'message':'ALREADY_EXISTS'}, status=409)

Q객체를 통해 OR 조건을 사용했습니다. 세 가지 정보 중 하나만 들어오기 때문에 OR 조건입니다.

Part5. 회원정보 생성하기(회원가입 완료!)

User.objects.create(
email         = email,
mobile_number = mobile_number,
full_name     = full_name,
username      = username,
password      = password
)
return JsonResponse({'message':'SUCCESS'}, status=201)

이제 실제로 회원정보를 생성할 차례입니다! create() 메소드로 회원정보를 생성하였고, 모든 검사를 마쳤기 때문에 각각의 필드에는 최초 변수에 담은 데이터로 연결하면 됩니다.

👀 회원가입(SignupView) 전체 코드

# user/views.py

import json
import re
from json.decoder import JSONDecodeError

from django.http      import JsonResponse
from django.views     import View
from django.db.models import Q

from user.models import User


PASSWORD_MINIMUM_LENGTH = 8

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

            email         = data.get('email', None)
            mobile_number = data.get('mobile_number', None)
            full_name     = data.get('full_name', None)
            username      = data.get('username', None)
            password      = data.get('password', None)

            email_pattern         = re.compile('[^@]+@[^@]+\.[^@]+')
            mobile_number_pattern = re.compile('^[0-9]{1,15}$')
            username_pattern      = re.compile('^(?=.*[a-z])[a-z0-9_.]+$')

            if not (
                (email or mobile_number)
                and full_name 
                and username
                and password
            ):
                return JsonResponse({'message':'KEY_ERROR'}, status=400)
            
            if email:
                if not re.match(email_pattern, email):
                    return JsonResponse({'message':'EMAIL_VALIDATION_ERROR'}, status=400)

            if mobile_number:
                if not re.match(mobile_number_pattern, mobile_number):
                    return JsonResponse({'message':'MOBILE_NUMBER_VALIDATION_ERROR'}, status=400)

            if not re.match(username_pattern, username):
                return JsonResponse({'message':'USERNAME_VALIDATION_ERROR'}, status=400)

            if len(data['password']) < PASSWORD_MINIMUM_LENGTH:
                return JsonResponse({'message':'PASSWORD_VALIDATION_ERROR'}, status=400)

            if User.objects.filter(
                Q(email         = data.get('email', 1)) |
                Q(mobile_number = data.get('mobile_number', 1)) |
                Q(username      = data['username'])
            ).exists():
                return JsonResponse({'message':'ALREADY_EXISTS'}, status=409)
            
            User.objects.create(
                email         = email,
                mobile_number = mobile_number,
                full_name     = full_name,
                username      = username,
                password      = password
            )
            return JsonResponse({'message':'SUCCESS'}, status=201)
        
        except JSONDecodeError:
            return JsonResponse({'message':'JSON_DECODE_ERROR'}, status=400)

📬 url 경로 지정하기

# westagram/urls.py

from django.urls import path, include

urlpatterns = [
    path('user', include('user.urls')),
]
# user/urls.py

from django.urls import path
from .views      import SingUpView

urlpatterns = [
    path('/signup', SingUpView.as_view()),
]

다음 시간에는 인스타그램 로그인 기능을 구현하도록 하겠습니다.

profile
Leave your comfort zone
post-custom-banner

0개의 댓글