장고를 처음 접해보면서, 인스타그램의 백엔드를 매우 기초적인 수준으로 클론 해보고 있습니다.
이제 앱을 생성해보겠습니다. 장고에서는 서비스에 필요한 각 기능들을 각각의 앱들로 나눠서 관리하는데요, 이번에는 로그인을 담당하는 앱을 만들고 내부 코드를 작성해보겠습니다.
회원가입/로그인을 관리하는 account
라는 앱을 만들어보겠습니다.
➜ django-admin startapp <앱 이름> # 명령어 구조
➜ django-admin startapp account # 명령어 실제 작성
➜ tree westa_posting
westa_posting
├── account
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── manage.py
└── westa_posting
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
account
라는 앱이 생겼습니다. 앱을 만들고 나면 내부 파일들을 채워나가야하는데, 앱 등록 -> 모델 작성 -> 뷰 작성 -> 경로 등록의 순서로 진행하겠습니다.
생성한 앱은 settings.py
에 등록을 해줘야 django가 인식을 하기 시작합니다. settings.py
에 들어가 INSTALLED_APPS
에 account
를 추가합니다. 모든 수정을 한 후 저장하는 것을 잊지 마세요!
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'account', # account 앱 등록
]
모델로 사용할 Account
클래스를 만듭니다.(참고로 클래스명은 맨 앞은 대문자이면서, 단수로 만듭니다) 커스터마이징 하지 않는 이상 모든 클래스는 django에 포함된 models.Model
을 상속받습니다. 따라서 Account
에도 상속 받아줍니다.
from django.db import models
class Account(models.Model):
email = models.CharField(max_length = 200)
password = models.CharField(max_length = 500)
created_at = models.DateTimeField(auto_now_add = True)
updated_at = models.DateTimeField(auto_now = True)
class Meta:
db_table = "accounts"
모델 내부에는 향후 DB(데이터베이스)에 쌓아줄 데이터의 기준을 만들어줍니다. 회원가입 및 로그인 과정에서는 이메일과 패스워드만 받을 예정이므로, 각각의 필드를 만들어줍니다. 그리고, 기본적으로 시점에 대한 기록을 남기기 위해 처음 계정이 생성된 시점과 업데이트 되는 시점을 필드로 만듭니다.
그리고 각 필드에는 특정 설정값을 인자로 줄 수 있습니다. CharField
의 경우에는 max_length
로 최대값을 설정해 줄 수 있습니다. 해당 필드가 비어있어도 괜찮다면 blank=true
값을 추가할 수 있지만, 이메일과 패스워드는 필수이므로 추가하지 않습니다.
DateTimeField
는 날짜와 시간을 가져오는 필드인데, auto_now_add = True
를 넣어주면 처음 생성(추가)된 시점을 자동으로 기록해주고, auto_now = True
를 넣어주면 필드가 업데이트 될 때마다 그 시점을 기록해줍니다.
마지막으로 내부에 Meta
클래스를 추가해 db_table
을 선언해줍니다. 이 필드값은 DB에 account
데이터가 어떤 이름으로 테이블에 기록될지 정의해줍니다. 설정하지 않으면 'account_Account' 같은 이상한 값으로 기록되므로 원하는 값으로 바꿔줍시다.
모델을 다 작성해줬으면 migration과 migrate를 해줘야합니다. 장고는 migrations 테이블을 두어 마이그레이션 적용 여부를 추적하고, migrate 할 때 문제가 있는지 미리 확인해줍니다. (migrate 과정은 model 작성, 수정시에만 적용됩니다.)
먼저 마이그레이션을 해줍시다. makemigrations
뒤에 마이그레이션을 할 앱을 지정해주는게 좋은데, 만약 앱 모델 간에 참조 관계가 있는 경우 순서가 중요할 때가 있기 때문이라고 합니다.
➜ python manage.py makemigrations <앱 이름> # 명령어 구성, 앱 이름 생략 시 프로젝트 전체에서 마이그레이션 진행
➜ python manage.py makemigrations account # 실제 명령어 작성
Migrations for 'account':
account/migrations/0001_initial.py # 0001 이름의 마이그레이션 파일 생성
- Create model Account
➜ tree westa_posting/account
westa_posting/account
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-38.pyc
│ ├── admin.cpython-38.pyc
│ └── models.cpython-38.pyc
├── admin.py
├── apps.py
├── migrations # 요렇게 마이그레이션 디렉토리가 생기면서, 0001 이라는 파일이 생성됩니다.
│ ├── 0001_initial.py
│ ├── __init__.py
│ └── __pycache__
│ └── __init__.cpython-38.pyc
├── models.py
├── tests.py
└── views.py
그 다음으로 DB에 모델을 적용해주기 위해 migrate
를 진행합니다. 아래와 같이 출력 되면 성공!
➜ python manage.py migrate <앱 이름> <마이그레이션 번호> # 명령어 구성, 앱 이름, 마이그레이션 번호를 안쓰면 전체 프로젝트 대상 마이그레이트 진행
➜ python manage.py migrate account 0001 # 실제 명령어 작성
Operations to perform:
Target specific migration: 0001_initial, from account
Running migrations:
Applying account.0001_initial... OK
뷰는 작성된 모델을 바탕으로, 들어오는 데이터를 어떻게 처리할지에 대한 논리를 맡고 있습니다.
첫 부분에는 우리가 사용할 모듈들을 불러와야하는데요. 먼저 우리는 http를 통해 받은 요청 정보(json)를 파이썬이 읽을 수 있는 형태로 변환해줄 수 있는 모듈인 json을 임포트 해오고, json 형태로 응답(response) 해줄 수 있는 JsonResponse 모듈을 불러옵니다. 그리고 뷰에서 처리할 정보가 쌓이는 기준이 되는 Account class를 모델에서 불러옵니다.
import json
from django.views import View
from django.http import JsonResponse
from .models import Account
참고로 json.loads는 json 형태의 데이터를 파이썬이 읽을 수 있는 형태로 디코딩 해주는 역할을 합니다.
json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') ['foo', {'bar': ['baz', None, 1.0, 2]}]
이제 회원가입을 담당하는 SignUpView
를 만들어보겠습니다. 클래스의 명칭은 실제 적용될 기능의 내용이 들어가면 더 직관적이 되어 좋습니다. 회원가입 클래스에서는 post
요청이 들어오면 회원가입을 처리해주는 로직을 만들건데요, 기초기 때문에 이전에 가입했는지 여부 등은 따지지 않고, 요청이 들어오면 바로 가입이 완료되는 형태로 작성했습니다.
보통
get
,post
요청이 들어오는데,get
은 순전히 데이터만을 요청할 때,post
는 어떠한 데이터를 입력, 수정을 할 때 라고 생각하면 됩니다. 예를 들어, 로그인도 로그인 완료라는 데이터를 요청해서get
일 것 같지만, 일단 데이터를 입력해서 데이터가 맞는지 확인 받기 때문에post
메서드를 사용합니다.
요청(request)을 통해 들어온 json 데이터는 json.loads
에 의해 파이썬이 읽을 수 있는 형태로 디코딩 됩니다. 이 데이터에서 우리가 필요한 데이터를 뽑아 우리가 만들었던 Account 모델의 DB에 저장합니다. 아직 형태는 익숙지 않은데, 모델명( 필드명 = 입력할 데이터).save()
이 구조를 기억하세요!
저장이 완료되면, 요청을 한쪽으로 json 형태의 응답을 보내줍니다. 우리는 모든 데이터가 정상적으로 들어온다고 가정하므로, JsonResponse의 인자로는 회원가입 완료 메시지와 요청이 성공했음을 나타나는 status 200 코드를 리턴해줍니다.
JsonResponse는 HttpResponse의 서브클래스로, Json으로 인코드된 응답을 하도록 도와줍니다. Content-Type header는 application/json으로 디폴트 시켜주고, 첫 번째 파라미터는 딕셔너리 형태의 인스턴스가 와야합니다.
class SignUpView(View):
def post(self, request):
data = json.loads(request.body)
Account(
email = data['email'],
password = data['password']
).save() # 받아온 데이터를 DB에 저장시켜줌
return JsonResponse({'message':'회원가입 완료'},status=200)
이번에는 로그인 뷰 차례 입니다. 로그인 뷰의 논리는 이렇게 짜보려고 합니다. 기본적으로 로그인 기능은 사용자가 입력한 이메일(아이디)와 패스워드가 DB에 있는 값과 일치하면 로그인 완료 페이지를 리턴해주는 방식입니다. 그러므로 사용자가 입력한 이메일이 먼저 현재까지 저장된 DB에 있는지 확인하고, 값이 있다면 해당 이메일과 매칭된 패스워드가 일치한지 확인합니다. 이때 일치한다면 로그인 성공 메시지를, 틀리면 비밀번호가 틀렸다는 메시지를 뿌려주고, 전체 확인 과정에서 일치하는 데이터가 아예 없다면 등록되지 않았다는 메시지를 리턴해줍니다.
이를 위해 현재까지 Account
로 저장된 객체들 중 filter
메서드를 활용해 사용자가 입력한 이메일이 DB에 있는지 확인합니다. 여기서 True 값이 확인되면, 비밀번호를 확인하고 위에서 나열한 논리대로 작성을 진행합니다.
처음 코드를 작성할 때는, filter
와 exists
메서드에 대해 알지 못해 for 문 형태로 작성을 했습니다. 그런데 이게 데이터가 적을때는 크게 상관 없지만, 나중에 방대한 양의 데이터를 다룰 때에는 일단 데이터 전체를 훑어야하는 for 문은 효율상 좋지 않습니다. 그래서 해당 값이 있는지만 확인하면 빠져나오게 되는 filter
메서드를 사용해 조금 더 나은 효율성을 추구했습니다.
class SignInView(View):
def post(self, request):
data = json.loads(request.body)
if Account.objects.filter(email = data['email']).exists() :
user = Account.objects.get(email = data['email'])
if user.password == data['password'] :
return JsonResponse({'message':f'{user.email}님 로그인 성공!'}, status=200)
else :
return JsonResponse({'message':'비밀번호가 틀렸어요'},status = 200)
return JsonResponse({'message':'등록되지 않은 이메일 입니다.'},status=200)
이제 경로를 작성해보겠습니다. 이 프로젝트의 경로는 어디서 설정할까요? 이 부분은 우리가 처음에 수정했던 settings.py
를 보면 알 수 있습니다. 아래를 보면 ROOT_URLCONF
에 westa_posting.urls
가 명시되어있는걸 보아 저기서 최초 경로를 관리하고 있습니다.
# westa_posting/settings.py
ROOT_URLCONF = 'westa_posting.urls'
urls.py
에 들어와보면 최상단에 path
가 임포트 되어있을 것이고, include
를 따로 추가해줬습니다. include
는 앞에 인자로 있는 경로로 들어오면, 그때부터의 경로는 include
안에 있는 곳에서 관리한다는 뜻입니다. 여기서는 /sign
을 치고 들어오면, 그 뒤의 url은 account.urls
에서 찾아 처리하라는 의미이죠. 참고로 기본 url은 마지막에 "/"를 자동으로 붙여주기때문에, 또 적으면 안됩니다. 또 적게 되면 http://http://127.0.0.1:8000//sign
이런식으로 슬래시가 두번 적용되버리니 주의하시기 바랍니다.
# westa_posting/urls.py
from django.urls import path, include
urlpatterns = [
path('sign', include('account.urls')),
]
이번에는 include
를 타고 들어온 account.urls
를 작성해보겠습니다.
여기서는 다른 곳으로 넘어갈 url 경로를 관리하지 않기 때문에 처음에 include
는 임포트하지 않았습니다. 그리고 여기서 우리가 아까 작성한 views
가 나타납니다. 아시다시피 view
는 들어온 데이터를 처리하는 논리(요청 객체를 받고 응답 객체를 반환하는 내장 함수)인데요, 현재 경로로 들어온 데이터 요청을 우리가 만든 SignUpView
, SignInView
클래스를 통해 처리합니다.
뷰 클래스는 내장 함수를 반환하는 as_view() 클래스 메서드를 제공하고(클래스 기반 뷰, Class Based View), 모든 클래스 기반 뷰는 이 클래스를 직간접적으로 상속받아 사용합니다.
as_view()
는 클래스의 인스턴스를 생성하고, 이 인스턴스의 dispatch() 메소드를 호출합니다. dispatch() 메서드에서는 뷰가 받은 요청을 검사해서 HTTP 요청 메서드(GET, POST 등)를 알아낸 다음, 인스턴스 내에 해당 이름을 갖는 메서드로 요청을 전달합니다. 만약 뷰 안에 요청으로 온 메서드가 정의되어있지 않으면 HttpResponseNotAllowed
예외가 발생합니다.
방금 만든 우리의 뷰를 가지고 대입해보면, http://127.0.0.1:8000/sign/up email=email password=password
을 통해 요청이 들어오면, as_view()
가 SignUpView
의 인스턴스를 생성하고, dispatch()
메서드를 통해 들어온 요청 메서드가 뷰 안에서 정의한 POST
인지 확인합니다. 이 경우 POST
가 있기 때문에 정상적으로 정의된 SignUpView
의 논리에 따라 응답이 처리되서 반환됩니다.
SignUpView.as_view() -> SignUpView(class) -> SignUpView(instance) -> GET or POST 확인 -> 메서드 매칭
from django.urls import path
from .views import SignUpView, SignInView
urlpatterns = [
path('/up', SignUpView.as_view()),
path('/in', SignInView.as_view()),
]
이제 기초적인 회원가입/로그인 앱이 완성되었습니다. 잘 작동하는지 테스트를 해보겠습니다. 테스트는 httpie
를 활용해 진행했습니다.
먼저 서버를 시작합니다. 명령어는 manage.py
가 위치한 곳에서 입력해야 작동합니다. 아래와 같이 나타나면 성공입니다.
February 07, 2020 - 05:00:45
Django version 3.0.3, using settings 'westa_posting.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
서버가 실행되면 현 창에서는 명령어를 입력할 수 없는데요, 테스트는 다른 쉘 창을 통해서 진행하면 됩니다.
➜ http -v http://127.0.0.1:8000/sign/up 필드=입력데이터 필드=입력데이터 # 명령어 구성
# 회원가입
➜ http -v http://127.0.0.1:8000/sign/up email=aaa@aaa.com password=1234
POST /sign/up HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 44
Content-Type: application/json
Host: 127.0.0.1:8000
User-Agent: HTTPie/2.0.0
{
"email": "aaa@aaa.com",
"password": "1234"
}
HTTP/1.1 200 OK
Content-Length: 52
Content-Type: application/json
Date: Fri, 07 Feb 2020 05:04:47 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"message": "회원가입 완료"
}
# 로그인
# 데이터 잘 입력 시
➜ http -v http://127.0.0.1:8000/sign/in email=aaa@aaa.com password=1234
{
"message": "aaa@aaa.com님 로그인 성공!"
}
# 이메일 틀릴 시
{
"message": "등록되지 않은 이메일 입니다."
}
# 비밀번호 틀릴 시
{
"message": "비밀번호가 틀렸어요"
}
끝!