🔥 Mini Project: Django Login & Logout 기능 만들기
이번 포스트에서는 Django에서 로그인과 로그아웃 기능을 구현하는 방법을 정리합니다. 인증 로직의 핵심은 Django 내장 함수인 authenticate와 사용자 정의 LoginForm을 활용하는 것입니다.
✅ 핵심 개념 정리
용어 설명
authenticate() | 이메일과 비밀번호로 사용자 인증 시도. 성공 시 User 객체 반환, 실패 시 None |
login() | 인증된 사용자를 세션에 등록해 로그인 상태로 만듦 |
logout() | 현재 로그인된 사용자 세션 삭제 |
LoginForm | 사용자로부터 이메일과 패스워드를 입력받고 인증하는 커스텀 폼 |
✅ 로그인 폼 정의하기 (LoginForm)
# member/forms.py
from django import forms
from django.contrib.auth import authenticate
class LoginForm(forms.Form):
email = forms.CharField(
label='이메일',
required=True,
widget=forms.EmailInput(attrs={
'placeholder': 'example@example.com',
'class': 'form-control'
})
)
password = forms.CharField(
label='패스워드',
required=True,
widget=forms.PasswordInput(attrs={
'placeholder': 'password',
'class': 'form-control'
})
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = None
def clean(self):
cleaned_data = super().clean()
email = cleaned_data.get('email')
password = cleaned_data.get('password')
self.user = authenticate(email=email, password=password)
if self.user is None:
raise forms.ValidationError('이메일 또는 비밀번호가 올바르지 않습니다.')
if not self.user.is_active:
raise forms.ValidationError('유저가 인증되지 않았습니다.')
return cleaned_data
✅ 로그인 뷰 작성
from django.contrib.auth import get_user_model, login
from django.core import signing
from django.core.signing import TimestampSigner, SignatureExpired
from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render
from member.forms import SignupForm, LoginForm
from utils.email import send_email
User = get_user_model()
class SignupView(FormView):
template_name = 'auth/signup.html'
form_class = SignupForm
def form_valid(self, form):
user = form.save()
# 이메일 발송
signer = TimestampSigner()
signed_user_email = signer.sign(user.email)
signer_dump = signing.dumps(signed_user_email)
# print(signer_dump)
#
# decoded_user_email = signing.loads(signer_dump)
# print(decoded_user_email)
# email = signer.unsign(decoded_user_email, max_age=60 * 30)
# print(email)
url = f"{self.request.scheme}://{self.request.META["HTTP_HOST"]}/verify/?code={signer_dump}"
subject = '[Pystagram] 이메일 인증을 완료해주세요'
message = f'다음 링크를 클릭해주세요. <br><a href="{url}">url</a>'
send_email(subject, message, user.email)
return render(self.request, 'auth/signup_done.html', {'user': user})
def verify_email(request):
code = request.GET.get('code', '')
signer = TimestampSigner()
try:
decoded_user_email = signing.loads(code)
email = signer.unsign(decoded_user_email, max_age=60 * 30)
except (TypeError, SignatureExpired):
return render(request, template_name='auth/not_verified.html')
user = get_object_or_404(User, email=email, is_active=False)
user.is_active = True
user.save()
# return redirect(reverse('login'))
return render(request, template_name='auth/email_verified_done.html', context={'user': user})
class LoginView(FormView):
template_name = 'auth/login.html'
form_class = LoginForm
success_url = reverse_lazy('login')
def form_valid(self, form):
user = form.user
login(self.request, user)
next_page = self.request.GET.get('next')
if next_page:
return HttpResponseRedirect(next_page)
return HttpResponseRedirect(self.get_success_url())
✅ URL 연결
# config/urls.py
from django.urls import path
from member.views import LoginView, LogoutView
urlpatterns = [
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
]
✅ 로그인 템플릿 (login.html)
<!-- templates/auth/login.html -->
{% extends 'base.html' %}
{% block content %}
<h1 class="title">로그인</h1>
<form method="POST">
{% csrf_token %}
{% include 'include/form.html' %}
<button class="btn btn-primary">로그인</button>
</form>
{% endblock %}
signup.html
<!-- templates/auth/signup.html -->
{% extends 'base.html' %}
{% block content %}
<div>
<h1 class="title">회원가입</h1>
<form method="POST">
{% csrf_token %}
{% include 'include/form.html' %}
<button class="btn btn-primary">회원가입</button>
</form>
</div>
{% endblock %}
templates/include/form.html
{% for field in form %}
<div class="form-group row mb-2">
<label class="form-label col-md-2" for="{{ field.auto_id }}">{{ field.label }}</label>
<div class="col-md-10">
{{ field }}
</div>
{% for error in field.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{% endfor %}
{% for error in form.non_field_errors %}
<p class="text-danger">{{ error }}</p>
{% endfor %}
base.html 버튼 추가
<!-- templates/base.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pystagram</title>
<link rel="stylesheet" href="{% static 'css/bootstrap.css' %}">
</head>
<body>
<nav class="d-flex justify-content-between py-2 px-4 bg-black text-white">
<div>
{# TODO : main 연결 #}
<a href="" class="text-decoration-none text-white"></a>
</div>
<div class="text-end">
{% if request.user.is_authenticated %}
{{ request.user.nickname }}
<form action="{% url 'logout' %}" method="post" class="d-inline ms-2">
{% csrf_token %}
<button class="btn btn-dark btn-sm">로그아웃</button>
</form>
{% else %}
<a class="btn btn-dark btn-sm" href="{% url 'signup' %}">회원가입</a>
<a class="btn btn-dark btn-sm" href="{% url 'login' %}">로그인</a>
{% endif %}
</div>
</nav>
<div class="container">
{% block content %}{% endblock %}
</div>
<script src="{% static 'js/bootstrap.bundle.js' %}"></script>
</body>
</html>
settings.py 에 리다이렉트 설정
LOGIN_URL = '/login/'
LOGOUT_REDIRECT_URL = '/'
🔐 보안 팁
- authenticate() 함수는 AUTH_USER_MODEL에 정의된 USERNAME_FIELD 값을 기준으로 인증합니다 (예: email)
- 로그인 성공 후 반드시 login(request, user) 호출로 세션 처리 필요
- 로그인 폼의 clean() 메서드에서 사용자 상태도 함께 체크해야 보안상 안전합니다
📌 마무리 체크리스트
- 로그인 폼 커스텀 생성
- 로그인 뷰 구현 (GET, POST)
- 로그아웃 뷰 구현
- URL 연결 및 템플릿 작성
다음 포스트에서는 ✅ 로그인 상태에 따라 네비게이션 분기 처리나 ✅ 비밀번호 재설정(Reset) 기능으로 이어가보겠습니다!
'Django' 카테고리의 다른 글
Chapter 9-1 Django Post 기능 구현하기 (0) | 2025.05.14 |
---|---|
🔍 Django에서 User.objects.model의 정체는? (0) | 2025.05.12 |
Chapter 8-5 Django 이메일 인증을 위한 SMTP 설정 가이드 (0) | 2025.05.12 |
Chapter 8-4 환경변수 관리와 python-dotenv 사용법 (0) | 2025.05.12 |
Chapter 8-3 Django 회원가입 페이지 만들기 (정적 파일 + 폼 커스텀 + 뷰 구현) (0) | 2025.05.12 |