[강의 공부] 13. 로그인/로그아웃 구현하기

단간단간·2024년 4월 23일
0
post-thumbnail

⎷ 먼저..

보통은 아래 회원가입 뷰(=AccountCreateView) 처럼, django에서 지원하는 클래스 뷰를 적절히 상속받아서 필요에 맞는 클래스 뷰를 만들고

# accountapp > views.py

from django.contrib.auth.forms import UserCreationForm
from django.views.generic import CreateView
from django.urls import reverse_lazy


# 회원가입 뷰
class AccountCreateView(CreateView):
    model = User
    form_class = UserCreationForm
    success_url = reverse_lazy('accountapp:hello_world')
    template_name = 'accountapp/create.html'

urls.py에서 url과 뷰의 매핑을 정의할 때 아래와 같이 설정해주는 걸 볼 수 있다.

참고로 path의 두 번째 인자에는 뷰 함수를 받거나, 뷰 클래스.as_view()를 받는다. as_view() 메서드는 해당 클래스를 django의 url 라우터가 이해하고 사용할 수 있는 호출 가능한 뷰 함수로 변환한다고 한다.

# accountapp > urls.py

from django.urls import path
from accountapp.views import AccountCreateView

app_name = "accountapp"

urlpatterns = [
    path('create/', AccountCreateView.as_view(), name='create'),
]

그런데 위 처럼 django 기본 뷰 클래스를 상속받아 새로운 클래스를 만들지 않고, django에서 만든 뷰 클래스 자체를 그대로 사용하는 경우도 있다. 아래 뷰 클래스를 사용하는 실습을 했다.

  • LoginView : 로그인 할 때 사용
  • LogoutView : 로그아웃 할 때 사용

⎷ 로그인/로그아웃 기능 구현 (LoginView, LogoutView 클래스)

LoginView를 그대로 사용할 경우, 따로 클래스 뷰를 생성해줄 필요가 없다.
하지만 urls.py에서 뷰와 url 매핑 작업은 필요하다.

# accountapp > urls.py

from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path

from accountapp.views import hello_world, AccountCreateView

app_name = "accountapp"

urlpatterns = [
    path('hello_world/', hello_world, name='hello_world'),
    path('create/', AccountCreateView.as_view(), name='create'),
    
    # 여기가 새로 추가된 부분 !!!!!!!!!!!!!!!!!!!
    path('login/', LoginView.as_view(template_name='accountapp/login.html'), name='login'),
    path('logout/', LogoutView.as_view(), name='logout'),
]
  • path('login/', LoginView.as_view(template_name='accountapp/login.html'), name='login'):
    로그인의 경우엔 로그인 페이지가 따로 필요하기 때문에 login.html 라는 템플릿을 만들 예정이다.
  • path('logout/', LogoutView.as_view(), name='logout'):
    로그아웃은 기능만 필요할 뿐, 여기서는 따로 페이지가 필요하지 않아서 템플릿을 만들지 않는다.

간단한 로그인 페이지

<!-- login.html -->

{% extends "base.html" %}

{% block content %}
    <div style="text-align: center">
        <div>
            <h4>Login</h4>
        </div>
        <div>
            <form action="" method="post">
                {% csrf_token %}
                {{ form }}
                <input type="submit" class="btn btn-primary">
            </form>
        </div>
    </div>
{% endblock %}

그런데 여기서 로그인을 하면 아래와 같이 별도로 정의하지도 않았는데 ~/profile url로 접근하려고 하는 것을 볼 수 있다. 존재하지 않는 url로 접근하려니 404 not found 에러가 발생했다.

원인

  • LoginView, LogoutView는 로그인 혹은 로그아웃을 수행하고 난 뒤 가야할 경로를 아래와 같은 우선순위로 얻는다고 한다.
    • 1) next 인자
    • 2) LOGIN_REDIRECT_URL, LOGOUT_REDIRECT_URL (settings.py 파일 내 정의)
    • 3) Default
  • 설명하면,next 인자를 받은 경우 로그인 혹은 로그아웃이 수행되었을 때, 해당 next 경로로 redirect 한다. 만약 next 인자를 받지 못한 경우엔 settings.py에 정의된 LOGIN_REDIRECT_URL(로그인의 경우), LOGOUT_REDIRECT_URL(로그아웃의 경우)에 정의된 경로로 redirect 한다. 이마저도 없다면 Default로 설정된 값으로 redirect 되는데, 여기서는 nextLOGIN_REDIRECT_URL도 설정된 값이 없어서 /profile이라는 Default 경로로 redirect를 하게 된 것이다.

해결

  • 엉뚱한 경로로 가지 않도록 하기 위해 next인자 또는 LOGIN_REDIRECT_URL, LOGOUT_REDIRECT_URL 를 설정하면 된다.

문제를 해결하기 위해, header.html의 nav4 문자를 로그인/로그아웃 기능으로 만들어본다.

< 변경 전 (header.html) >

    <div class="pragmatic_header">

        <div>
            <h1 class="pragmatic_logo">Pragmatic</h1>
        </div>

        <div>
            <span>nav1</span>
            <span>nav2</span>
            <span>nav3</span>
            <span>nav4</span>
        </div>

    </div>

< 변경 후 (header.html) >

    <div class="pragmatic_header">

        <div>
            <h1 class="pragmatic_logo">Pragmatic</h1>
        </div>

        <div>
            <span>nav1</span>
            <span>nav2</span>
            <span>nav3</span>
            {% if not user.is_authenticated %}
            <a href="{% url 'accountapp:login' %}?next={{ request.path }}">
                <span>login</span>
            </a>
            {% else %}
            <a href="{% url 'accountapp:logout' %}?next={{ request.path }}">
                <span>login</span>
            </a>
            {% endif %}
        </div>

    </div>
  • 로그아웃 상태라면 아래 이미지처럼 login 버튼을 활성화 시키고, 로그인 상태라면 logout 버튼을 활성화 시키기 위해 위처럼 구현했다.
  • ?next={{ request.path }}:
    next인자로 현재 보여지고 있는 페이지의 경로를 전달한다. LoginView는 next인자로 특정 경로를 받았으므로 로그인이 성공한다면 해당 next 값으로 redirect 시켜준다.

next 인자 테스트

/account/hello_world 경로에서 헤더 부분의 login 버튼을 클릭한다.
이 때, /account/login 경로로 next 인자를 전달한다. (next="/account/hello_world")

next 인자를 받은 상태에서 로그인을 하게 되면

아까처럼 /profile 경로가 아닌 next 경로로 redirect 된 걸 볼 수 있다.


⎷ 로그아웃에서 에러가 왜나...?

그런데 로그아웃을 했더니 405 에러코드가 발생했다.

이는 로그아웃 호출시 적절하지 않은 Method를 사용한 것으로 보인다.
현상에 대해서 찾아보다 해당 링크를 발견했다.
(장고 5부터 LogoutView로의 요청은 POST 방식만 허용)

요약

  • 장고 이전 버전에서는 LogoutView 요청시 GET 방식도 허용이 됐다.
  • html <a>태그는 GET Method 방식으로만 보낸다.
  • 위에서 <a>태그를 이용해서 LogoutView를 요청했고, 결과적으로 GET 방식으로 요청한 꼴이 되었다.
  • 장고 5부터는 LogoutView 요청시 POST 방식으로 보내야 한다.
  • 즉, <a>태그가 아니라 <form>태그를 사용해서 POST 방식으로 보내는 방법을 구현해봐야 할 것 같다.

header.html 수정

< header.html 변경 전 >

    <div class="pragmatic_header">

        <div>
            <h1 class="pragmatic_logo">Pragmatic</h1>
        </div>

        <div>
            <span>nav1</span>
            <span>nav2</span>
            <span>nav3</span>
            {% if not user.is_authenticated %}
            <a href="{% url 'accountapp:login' %}?next={{ request.path }}">
                <span>login</span>
            </a>
            {% else %}
            <a href="{% url 'accountapp:logout' %}?next={{ request.path }}">
                <span>login</span>
            </a>
            {% endif %}
        </div>

    </div>

< header.html 변경 후 >

    <div class="pragmatic_header">

        <div>
            <h1 class="pragmatic_logo">Pragmatic</h1>
        </div>

        <div>
            <span>nav1</span>
            <span>nav2</span>
            <span>nav3</span>
            {% if not user.is_authenticated %}
            <a href="{% url 'accountapp:login' %}?next={{ request.path }}">
                <span>login</span>
            </a>
            {% else %}
            <form style="display: inline" action="{% url 'accountapp:logout' %}?next={{ request.path }}" method="post">
                {% csrf_token %}
                <input type="submit" class="btn" value="logout">
            </form>
            {% endif %}
        </div>

    </div>

로그아웃 부분에서 태그를 변경해주었더니 더이상 에러는 발생하지 않았다.


LOGIN_REDIRECT_URL, LOGOUT_REDIRECT_URL 설정하기

아직 남아있는 문제가 있다. 로그인 페이지에 직접적으로 접근을 한 뒤, 로그인을 하는 경우이다.

문제가 되는 이유는, 헤더의 login 버튼을 눌러서 로그인 페이지로 접근한 것이 아니기 때문에 next 인자를 전달하지 않고, LOGIN_REDIRECT_URL도 따로 설정된 것이 없으므로 Default(=/profile) 경로로 redirect 되기 때문이다.

따라서, 위처럼 next인자를 받지 않은 케이스에 대비하기 위해 settings.py 파일 내부에 LOGIN_REDIRECT_URL 값을 설정해주면 된다.

LOGIN_REDIRECT_URL 설정하는 김에, LOGOUT_REDIRECT_URL도 같이 설정해준다.

< settings.py >

LOGIN_REDIRECT_URL = reverse_lazy('accountapp:hello_world')
LOGOUT_REDIRECT_URL = reverse_lazy('accountapp:login')
  • 로그인하면 account/hello_world 경로로 호출
  • 로그아웃하면 account/login 경로로 호출 (로그인 페이지 다시 띄워주려고)

⎷ 느낀점: 장고 어렵네

FastAPI 프레임워크만 사용해보다가 이제 막 Django를 처음으로 입문하고 있는데,
확실히 Django가 러닝 커브가 좀 높은 것 같다.

FastAPI의 경우엔 소스코드만 봐도 어떤 순서로 어떻게 작동할 지 대충 예상이 가는데
Django는 소스코드만 봐서는 알기 어렵다.

Django 문서좀 들락날락 거리고, 계속 익숙해지려고 노력하는 수 밖에....

빛이 있으려면 어둠이 있어야 한다는 밥아저씨 명언처럼,
지금은 Django로 개발하는 것이 너무 어둡고 막막하지만, 이 시기를 잘 견뎌내면 나도 어엿한 Django 고수가 되어있기를 바래본다.

profile
simple is best

0개의 댓글