Mini Project: Pystagram 만들기
- 코드출처: 이한영의 Django 입문(디지털북스)
회원가입 기능 구현
- 회원가입 기능 기본 구조
- View: signup
- URL: /users/signup/
- Template: templates/users/signup.html
1. 기본 구조 생성
users/views.py
from django.shortcuts import render def signup(request): return render(request, 'users/signup.html')
users/urls.py
from django.urls import path from users.views import login_view, feeds, logout_view, signup urlpatterns = [ path("login/", login_view), path('feeds/', feeds), path("logout/", logout_view), path("signup/", signup), ]
templates/users/signup.html
<!doctype html> <html lang="ko"> <body> <h1>회원가입</h1> </body> </html>
2. SignupForm을 사용한 Tempalte 구성
- SignupForm 클래스 정의
users/forms.py
class SignupForm(forms.Form): username = forms.CharField() password1 = forms.CharField(widget=forms.PasswordInput) password2 = forms.CharField(widget=forms.PasswordInput) profile_image = forms.ImageField() short_description = forms.CharField()
- View에서 Template에 SignupForm 전달
users/views.py
from users.forms import LoginForm, SignupForm def signup(request): form = SignupForm() context = {"form": form} return render(request, "users/signup.html", context)
templates/users/signup.html
{ % raw %} <!doctype html> <html lang="ko"> <body> <h1>Sign up</h1> <form method="POST" enctype="multipart/form-data"> { % csrf_token %} { { form.as_p }} <button type="submit">회원가입</button> </form> </body> </html> { % endraw %}
3. View에 회원가입 로직 구현
Terminal에서 User 모델(클래스)의 create_user 메소드 사용 확인하기
python manage.py shell
- create 메소드
생성 후 다시 확인해 보면 잘 출력함
from users.models import User user = User.objects.create(username="sample", password="sample") print(user.id, user.username, user.password)
- 인증 처리 시 제대로 된 값을 가져오지 못함
- Django는 User의 비밀번호를 변형해서 저장하고 로드함
- 따라서 읽어올 때에도 복호화 기능을 적용하므로 그대로 읽어오지 못함
그대로 저장하는 것은 개인정보보호법 위반(국내에서는 create 메소드는 사용하면 안됨)
from django.contrib.auth import authenticate result = authenticate(username="sample", password="sample") print(result)
create_user 메소드
from users.models import User user2 = User.objects.create_user(username="sample2", password="sample2", short_description="sample2") print(user2.id, user2.short_description, user2.password)
- create 메소드
- SignupForm의 데이터 가져오기
- users/views.py
실행 시 데이터 정상 전달 여부 확인(터미널 로그)
def signup(request): if request.method == "POST": print(request.POST) print(request.FILES) form = SignupForm() context = {"form": form} return render(request, "users/signup.html", context)
문자열 데이터와 파일 데이터가 함께 전달되어야 함
def signup(request): if request.method == "POST": form = SignupForm(data=request.POST, files=request.FILES) if form.is_valid(): username = form.cleaned_data["username"] password1 = form.cleaned_data["password1"] password2 = form.cleaned_data["password2"] profile_image = form.cleaned_data["profile_image"] short_description = form.cleaned_data["short_description"] print(username) print(password1, password2) print(profile_image) print(short_description) context = {"form": form} return render(request, "users/signup.html", context) form = SignupForm() context = {"form": form} return render(request, "users/signup.html", context)
전달 후 로그 확인
- users/views.py
- User 생성하기
- User 생성 기준
- 비밀번호(password1)와 비밀번호 확인(password2)은 값이 같아야 함
- 같은 사용자명(username)을 사용하는 User는 생성 불가 및 오류 전달
- Terminal에서 테스트용 User 생성하기
존재하는 계정, 존재하지 않는 계정 확인
python manage.py shell
from users.models import User User.objects.filter(username="pystagram") User.objects.filter(username="pystagram").exists() User.objects.filter(username="no_user") User.objects.filter(username="no_user").exists()
- users/views.py
- 입력받은 username이 존재하면 Form에 에러 전달, 존재하지 않으면 생성
password1, password2가 같은지도 검사
from users.models import User def signup(request): if request.method == "POST": form = SignupForm(data=request.POST, files=request.FILES) if form.is_valid(): username = form.cleaned_data["username"] password1 = form.cleaned_data["password1"] password2 = form.cleaned_data["password2"] prifile_image = form.cleaned_data["profile_image"] short_description = form.cleaned_data["short_description"] if password1 != password2: form.add_error("password2", "비밀번호와 비밀번호 확인란의 값이 다릅니다.") if User.objects.filter(username=username).exists(): form.add_error("username", "입력한 사용자명은 이미 사용중입니다.") if form.errors: context = {"form": form} return render(request, "users/signup.html", context) else: user = User.objects.create_user( username = username, password = password1, profile_image = profile_image, short_description = short_description, ) login(request, user) return redirect("/users/feeds/") else: form = SignupForm() context = {"form": form} return render(request, "users/signup.html", context)
- User 생성 기준
4. SignupForm 내부에서 데이터 유효성 검사
- clean_username 메서드 작성
users/forms.py
from django import forms from django.core.exceptions import ValidationError from users.models import User class SignupForm(forms.Form): ... def clean_username(self): username = self.cleaned_data["username"] if User.objects.filter(username=username).exists(): raise ValidationError(f"입력한 사용자명({username})은 이미 사용중입니다", code="invalid") return None
- clean 메서드로 password1, password2 검증
users/forms.py
class SignupForm(forms.Form): def clean_username(self): ... def clean(self): password1 = self.cleaned_data["password1"] password2 = self.cleaned_data["password2"] if password1 != password2: self.add_error("password2", "비밀번호와 비밀번호 확인란의 값이 다릅니다")
- View 함수와 SignupForm 리팩토링
- 검증로직을 Form 내부로 이동했으므로 기존 코드는 정리
users/views.py
def signup(request): if request.method == "POST": form = SignupForm(data=request.POST, files=request.FILES) if form.is_valid(): username = form.cleaned_data["username"] password1 = form.cleaned_data["password1"] prifile_image = form.cleaned_data["profile_image"] short_description = form.cleaned_data["short_description"] user = User.objects.create_user( username = username password = password1 prifile_image = prifile_image short_description = short_description ) login(request, user) return redirect("/users/feeds/") else: context = {"form": form} return render(request, "users/signup.html", context) else: form = SignupForm() context = {"form": form} return render(request, "users/signup.html", context)
- cleaned_data로 사용자를 생성하던 로직도 Form 내부로 이동
users/forms.py
class SignupForm(forms.Form): def clean(self): ... def save(self): username = self.cleaned_data["username"] password1 = self.cleaned_data["password1"] profile_image = self.cleaned_data["profile_image"] short_description = self.cleaned_data["short_description"] user = User.objects.create_user( username=username, password=password1, profile_image=profile_image, short_description=short_description, ) return user
- 새로운 save()함수 적용
- View 내부의 사용자 생성로직 삭제
- 새로 만든 save()함수를 사용하도록 변경
users/views.py
def signup(request): if request.method == "POST": form = SignupForm(data=request.POST, files=request.FILES) if form.is_valid(): user = form.save() login(request, user) return redirect("/users/feeds/") else: context = {"form": form} return render(request, "users/signup.html", context) else: form = SignupForm() context = {"form": form} return render(request, "users/signup.html", context)
- 중복 출현 로직 제거
users/views.py
def signup(request): if request.method == "POST": form = SignupForm(data=request.POST, files=request.FILES) if form.is_valid(): user = form.save() login(request, user) return redirect("/users/feeds/") else: form = SignupForm() context = {"form": form} return render(request, "users/signup.html", context)
- 검증로직을 Form 내부로 이동했으므로 기존 코드는 정리
- View에서 처리되는 프로세스의 종류(프로세스의 내용만 비교할 것. 코딩작업 하지 않음)
- GET 요청
- SignupForm()으로 생성된 빈 form을 사용자에게 보여줌
users/views.py
def signup(request): if request.method == "POST": # 해당 없음 else: form = SignupForm() # context에 빈 Form이 전달됨 context = {"form": form} return render(request, "users/signup.html", context)
- POST 요청이며, 데이터를 받은 SignupForm이 유효한 경우
- SignupForm(data=…)으로 생성된 form의 save() 메서드로 User 생성, redirect로 경로가 변경됨
users/views.py
def signup(request): # POST 요청 시 form이 유효하다면 최종적으로 rediret 처리됨 if request.method == "POST": form = SignupForm(data=request.POST, files=request.FILES) if form.is_valid(): user = form.save() login(request, user) return redirect("/posts/feeds/") # 이후 로직은 실행되지 않음
- POST 요청이며, 데이터를 받은 SignupForm이 유효하지 않은 경우
- SignupForm(data=…)으로 생성된 form에는 error가 추가되며, 그 form을 사용자에게 보여줌
users/views.py
def signup(request): if request.method == "POST": form = SignupForm(data=request.POST, files=request.FILES) if form.is_valid(): # 검증에 실패하여 이 영역으로 들어오지 못함 # context에 error를 포함한 form이 전달됨 context = {"form": form} return render(request, "users/signup.html", context)
- GET 요청
5. Template 스타일링과 구조 리팩토링
templates/users/signup.html
{ % raw %} { % load static %} <!document html> <html lang="ko"> <head> <link rel="stylesheet" href="{ % static 'css/style.css' %}"> </head> <body> <div id="signup"> <form method="POST" enctype="multipart/form-data"> <h1>Pystagram</h1> { % csrf_token %} { { form.as_p }} <button type="submit" class="btn btn-signup">가입</button> </form> </div> </body> </html> { % endraw %}
- templates/base.html
Template을 확장하는 { % raw %}{ % extends %}{ % endraw %} 태그
{ % raw %} { % load static %} <!doctype html> <html lang="ko"> <head> <link rel="stylesheet" href="{ % static 'css/style.css' %}"> </head> <body> { % block content %}{ % endblock %} </body> </html> { % endraw %}
templates/users/login.html
{ % raw %} { % extends 'base.html' %} { % block content %} <div id="login"> <form method="POST"> <h1>Pystagram</h1> { % csrf_token %} { { form.as_p }} <button type="submit" class="btn btn-login">로그인</button> </form> </div> { % endblock %} { % endraw %}
templates/users/signup.html
{ % raw %} { % extends 'base.html' %} { % block content %} <div id="signup"> <form method="POST" enctype="multipart/form-data"> <h1>Pystagram</h1> { % csrf_token %} { { form.as_p }} <button type="submit" class="btn btn-signup">가입</button> </form> </div> { % endblock %} { % endraw %}
- templates/users/signup.html
회원가입과 로그인 페이지 간의 링크 추가
{ % raw %} <div id="signup"> <form method="POST" enctype="multipart/form-data"> ... <button type="submit" class="btn btn-signup">가입</button> <a href="/users/login/">로그인 페이지로 이동</a> </form> </div> { % endraw %}
templates/users/login.html
{ % raw %} <div id="login"> <form method="POST"> ... <button type="submit" class="btn btn-login">로그인</button> <a href="/users/signup/">회원가입 페이지로 이동</a> </form> </div> { % endraw %}