[Django] Sign In 구현하기

유제·2020년 11월 10일
1

Django로 Sign In 구현해보자.

🚨 참고사항
1. function based view 대신 class based view를 사용했습니다.
2. Sign In을 구현하는 방법을 하드코딩하는 양에 따라 임의로 총 3수준으로 나눴습니다. 가장 간편한 방법을 원하시는 분은 마지막 수준을 보시면 됩니다.

제 1수준으로 구현하기

url은 path("login/", views.LoginView.as_view()) 이렇게 연결해주었습니다.
Login을 할 때 login/ 경로로 아래의 두 가지(GET, POST) 요청이 들어옵니다.

  1. GET: 사용자가 login/으로 접속할 때 발생하는 요청
  2. POST: 사용자가 Login Form을 제출할 때 발생하는 요청

그렇기 때문에 LoginView에서 GET, POST 요청을 처리할 두 메소드(get, post)를 override해주었습니다.

// views.py
from django.views.generic import View

class LoginView(View):
    def get(self, request):
        # login/ 경로로 들어왔을 때 사용자에게 어떤 화면을 보여줄 지 정하는 메소드

    def post(self, request):
        # 제출된 form의 정보를 처리하는 역할을 하는 메소드

🚨 function based view를 사용하지 않는 이유
1. 바로 위 LoginView 예시코드와 아래 function based view인 login_view는 같은 동작을 합니다. 두 코드를 비교했을 때 class based view가 더 깔끔하게 느껴졌습니다.

// views.py
def login_view(request):
    if request.method == "GET":
    	# login/ 경로로 들어왔을 때 사용자에게 어떤 화면을 보여줄 지 정하는 메소드
    elif request.method == "POST":
    	# 제출된 form의 정보를 처리하는 역할을 하는 메소드
  1. 후술할 다른 수준의 Sign In 구현 방법들은 class based view를 기반으로 하기 때문입니다.

Form 작성 및 값 검증하기

django-admin startapp ~~ 명령어로 생성한 django app에다가 forms.py 파일을 만들어줍니다.

# forms.py

from django import forms

class LoginForm(forms.Form):
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
# views.py

from django.shortcuts import render
from django.views.generic import View
from . import forms

class LoginView(View):
    def get(self, request):
        form = forms.LoginForm
        return render(request, "login.html", {"form":form})

    def post(self, request):
        form = forms.LoginForm(request.POST)
        return render(request, "login.html", {"form":form})
// login.html

<form method="post">
    {% csrf_token %}
    {{form.as_p}}
    <button>Sign In</button>
</form>

forms.pyviews.py에서 import해서 form 변수를 만들고 login.html을 랜더링합니다. 이제 form에서 email, password를 입력하면 post 메소드에서 request.POST를 통해 email, password값을 받습니다. 이 값을 forms.LoginForm에 넘겨줌으로써 request.POST안의 값이 forms.LoginForm에 채워지게 됩니다.

이제 forms.py에서 넘겨받은 requset.POST안의 값을 검증해주어야합니다.
값을 검증하기 위한 메소드는 clean()입니다.
만약 email, password를 각각 검증하고 싶다면 clean_<fieldname>() 메소드를 사용하면 됩니다. 검증 순서는 상단 필드부터 차례대로 진행 후, 마지막에 clean() 메소드가 실행됩니다.
위의 forms.py로 예를 들면 clean_email() => clean_password() => clean() 순서로 실행됩니다.

clean_<fieldname>()메소드를 통하여 값을 검증할 경우 반드시 알아야할 점은 각각의 메소드들의 리턴값을 누적이 되며, 리턴값이 없을 경우 값이 사라지게 됩니다.

아래 순서대로 예를 들어보겠습니다.

clean_email(self) => clean_password(self) => clean(self)

clean_email(self)의 리턴값은 clean_password(self)에서 self.cleaned_data.get("email")로 접근이 가능합니다.
하지만 clean_email(self)에서는 self.cleaned_data.get("password")로 값을 얻을 수 없습니다. 왜냐하면 clean_email(self)clean_password(self)보다 먼저 실행되었기 때문에 clean_password(self)의 리턴값이 아직 없다고 볼 수 있습니다.

clean_<fieldname>()메소드들을 통해서 누적된 값들은 clean()에서 추가적인 작업을 할 수도 있습니다. 최종적으로 clean() 메소드의 리턴값은 LoginView에서 form.cleaned_data로 접근할 수 있습니다.

clean_<fieldname>()을 override하지 않는 경우엔 바로 clean(self)에서 self.cleaned_data로 모든 필드의 값에 접근할 수 있습니다.

이제 clean_<fieldname>()을 작성해봅시다.

# forms.py

from django import forms
from . import models

class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    
    def clean_email(self):
    	# clean_<fieldname>()에서는 self.cleaned_data.get(<fieldname>)값에 접근할 수 있습니다.
    	email = self.cleaned_data.get("email")
        try:
            models.User.objects.get(username=email)
            return email
        except models.User.DoesNotExist:
            raise forms.ValidationError("User does not exist.")
    
    def clean_password(self):
        email = self.cleaned_data.get("email")
        password = self.cleaned_data.get("password")
        try:
            user = models.User.objects.get(username=email)
            if user.check_password(password):
                return password
            else:
                raise forms.ValidationError("Password is wrong.")
        except models.User.DoesNotExist:
            pass

clean_email에서는 email 값을 가지고 와서, User 중 넘겨준 email과 동일한 email을 가지고 있는 user를 찾고, user가 존재하지 않는 경우 forms.ValidationError가 발생하도록 했습니다.
만약 forms.ValidationError가 발생하면, 사용자에겐 email 필드에 User does not exist.라는 텍스트가 보여집니다.

clean_email를 통과하면 clean_password 메소드가 실행됩니다.
User 중 넘겨준 email과 동일한 email을 가지고 있는 user를 찾고, django에서 기본적으로 제공하는 check_password로 password를 확인합니다. password가 맞으면 password를 반환하고, 틀릴 경우 forms.ValidationError가 발생합니다. clean_password에서는 models.User.DoesNotExist 에러가 발생할 때는 처리하지 않습니다. 왜냐하면 이미 clean_email에서 같은 에러를 처리하기 때문입니다.

clean_emailclean_password의 코드를 비교해보면 중복된 부분이 많다는 게 보이실 겁니다. 이를 clean_<fieldname>대신 clean메소드를 이용해서 합쳐주는 작업을 해보겠습니다.

# forms.py

from django import forms
from . import models

class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    
    def clean(self):
        email = self.cleaned_data.get("email")
        password = self.cleaned_data.get("password")
        try:
            user = models.User.objects.get(username=email)
            if user.check_password(password):
                return self.cleaned_data
            else:
                raise forms.ValidationError("Password is wrong.")
        except models.User.DoesNotExist:
            raise forms.ValidationError("User does not exist.")

clean메소드로 통합한 후에 발생하는 forms.ValidationError는 field와의 관계가 끊어집니다. clean_email에서 발생하는 에러는 email 필드에 에러가 뜨고, clean_password에서 발생하는 에러는 password 필드에 뜹니다. 하지만 clean메소드에서는 어느 field에 어떤 에러가 발생하는 지를 알 수 없습니다. 그렇기 때문에 에러를 특정 field에 뜨도록 하고 싶다면 추가적인 작업이 필요합니다.

기존의 에러는 아래와 같은 코드로 발생시켰습니다.
raise forms.ValidationError("Password is wrong.")

하지만 특정 field랑 연결하기 위해서는 add_error라는 메소드를 사용해야합니다.
self.add_error("password", forms.ValidationError("Password is wrong."))

add_error의 첫 번째 인자에는 fieldname이 들어가고, 두 번째 인자에는 에러가 들어가도록 작성하면 됩니다.

이렇게 값을 검증하는 방법을 끝마쳤습니다. 그런데, 검증을 끝마친 값에 접근하려면 어떻게 해야할까요? 앞서 지나가듯이 말씀드려서 기억을 못하실 수도 있습니다. 아래 post 메소드처럼 form.cleaned_data로 접근하시면 됩니다.

문제는 값을 검증하면서 에러가 발생하는 경우가 있을 수 있습니다. clean_email에서 에러가 발생하면 clean_password는 실행되지 않아 검증이 중단될 수도 있고, clean메소드에서는 에러가 발생하면 리턴값이 없습니다. 검증이 제대로 됐을 때와 제대로 되지 않았을 때를 구분하기 위해서 django에서는 is_valid()라는 메소드를 제공합니다. 검증이 제대로 되었다면 True를 반환하고, 제대로 되지않았다면 False를 반환합니다. 아래 코드는 검증이 제대로 되었을 때 form.cleaned_data를 출력합니다.

# views.py

from django.shortcuts import render
from django.views.generic import View
from . import forms

class LoginView(View):
    def get(self, request):
        form = forms.LoginForm
        return render(request, "core/login.html", {"form":form})

    def post(self, request):
        form = forms.LoginForm(request.POST)
        if form.is_valid():
            print(form.cleaned_data)
        return render(request, "core/login.html", {"form":form})

Login 구현하기

이제 진짜 로그인을 구현하겠습니다. django에서 제공하는 함수를 이용하면 굉장히 간단합니다.

# views.py

from django.urls import reverse
from django.shortcuts import render, redirect
from django.views.generic import View
from django.contrib.auth import authenticate, login
from . import forms

class LoginView(View):
    def get(self, request):
        form = forms.LoginForm
        return render(request, "core/login.html", {"form":form})

    def post(self, request):
        form = forms.LoginForm(request.POST)
        if form.is_valid():
            email = form.cleaned_data.get("email")
            password = form.cleaned_data.get("password")
            user = authenticate(request, username=email, password=password)
            if user:
                login(request, user)
                return redirect(reverse("core:home"))

끝입니다. authenticate에 request, username(email), password에 해당하는 값을 넣으면 알아서 그에 맞는 user를 찾아줍니다. 그리고 그 user를 request와 함께 login 함수에 넣으면 됩니다. login 후에는 home으로 redirect를 시켜주었습니다.

값을 검증하지 않고 authenticate함수가 반환한 user의 값을 확인하면 되지않을까라는 궁금증이 생기신 분들도 있을겁니다. username과 password가 없거나 일치하지않으면 authenticate 함수는 아무것도 반환하지 않으니까요.
만약 username이 없다는 에러 텍스트와 username과 password 정보가 일치하지 않는다는 에러 텍스트를 구분하지 않으실 분들은 그냥 authenticate함수를 사용하시면 될 것 같습니다. 저는 두 에러 텍스트를 구분하고 싶어서 authenticate함수를 사용하기 전에 값을 검증한 것입니다.

Logout 구현하기

# views.py

from django.contrib.auth import logout

...

def logout_view(request):
    logout(request)
    return redirect(reverse("core:home"))

logout은 정말 쉽습니다. logout함수에 request를 넘겨주고 다른 페이지로 redirect해주면 끝입니다.

제 2수준으로 구현하기

django에서 제공하는 FormView를 확장하면 조금 더 간단하게 Sign In 구현이 가능합니다.

template_name, form_class, success_url을 설정해줍니다.

  • template_name : 사용자 화면에 보여줄 템플릿의 이름
  • form_class : forms.py에서 만든 form
  • success_url : 로그인을 성공했을 때, 리다이렉트될 url
  • form_valid : form 검증이 제대로 되었을 때, success_url로 사용자를 리다이렉트시키는 메소드
# views.py

from django.urls import reverse_lazy
from django.views.generic import FormView
from django.contrib.auth import authenticate, login

class LoginView(FormView):
    template_name = "core/login.html"
    form_class = forms.LoginForm
    success_url = reverse_lazy("core:home")

    def form_valid(self, form):
        email = form.cleaned_data.get("email")
        password = form.cleaned_data.get("password")
        user = authenticate(self.request, username=email, password=password)
        if user:
            login(self.request, user)
        return super().form_valid(form)

super().form_valid(form)은 왜 있는걸까?

앞서 form_valid는 form 검증이 제대로 되었을 때, success_url로 사용자를 리다이렉트시키는 메소드라고 했습니다.
근데 지금 우리는 form_valid를 override했기 때문에 원래의 기능은 상실한 상태입니다. 원래의 기능을 사용하고 싶다면 super().form_valid(form)을 넣어주어야합니다.

super()는 상속받은 클래스에 접근할 수 있도록 해주는 함수입니다. 지금 상황에서는 super()FormView에 접근합니다. 그리고 FormView에 있는 오리지널 form_valid를 실행합니다. 그래서 super().form_valid(form)가 된 것 입니다.

제 3수준으로 구현하기

django에서 제공하는 LoginView를 확장하면 FormView보다 더 간단하게 Sign In 구현이 가능합니다. 시간이 부족하고 간단한 로그인 기능만 있으면 되는 분들에겐 추천합니다.

개인적으로는 너무 간단하기도 하고, 사용가능한 attributes가 헷갈릴 정도로 너무 많아서 FormView 혹은 View로 로그인을 구현하는 게 좋은 것 같습니다.

# views.py

from django.contrib.auth.views import LoginView

class LoginView(LoginView):
    template_name = "core/login.html"
# settings.py

LOGIN_REDIRECT_URL = "/"

코드의 양 비교하기

공통 코드

login.html

<form method="post">
    {% csrf_token %}
    {{form.as_p}}
    <button>Sign In</button>
</form>

View, FormView 공통 코드

forms.py

from django import forms
from . import models

class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    
    def clean(self):
        email = self.cleaned_data.get("email")
        password = self.cleaned_data.get("password")
        try:
            user = models.User.objects.get(username=email)
            if user.check_password(password):
                return self.cleaned_data
            else:
                self.add_error("password", forms.ValidationError("Password is wrong."))
        except models.User.DoesNotExist:
            self.add_error("email", forms.ValidationError("User does not exist."))

View

views.py

from django.urls import reverse
from django.shortcuts import render, redirect
from django.views.generic import View
from django.contrib.auth import authenticate, login
from . import forms

class LoginView(View):
    def get(self, request):
        form = forms.LoginForm
        return render(request, "core/login.html", {"form":form})

    def post(self, request):
        form = forms.LoginForm(request.POST)
        if form.is_valid():
            email = form.cleaned_data.get("email")
            password = form.cleaned_data.get("password")
            user = authenticate(request, username=email, password=password)
            if user:
                login(request, user)
                return redirect(reverse("core:home"))

FormView

views.py

from django.urls import reverse_lazy
from django.views.generic import FormView
from django.contrib.auth import authenticate, login

class LoginView(FormView):
    template_name = "core/login.html"
    form_class = forms.LoginForm
    success_url = reverse_lazy("core:home")

    def form_valid(self, form):
        email = form.cleaned_data.get("email")
        password = form.cleaned_data.get("password")
        user = authenticate(self.request, username=email, password=password)
        if user:
            login(self.request, user)
        return super().form_valid(form)

LoginView

views.py

from django.contrib.auth.views import LoginView

class LoginView(LoginView):
    template_name = "core/login.html"

settings.py

...

LOGIN_REDIRECT_URL = "/"

개인적인 평가

하드코딩을 하는 양이 많아도 제 1수준이 제일 직관적인 것 같습니다.

✅ 호감도
제 1수준 >= 제 2수준 > 제 3수준

조금 더 복잡해지면 어떨 진 모르겠지만, 단순히 email, password만으로 로그인을 구현하는 경우에는 제 1수준이 괜찮은 것 같습니다.
코드의 양이 획기적으로 줄어든다고 호감이 생기는 건 아닌 것 같네요.

0개의 댓글