본문 바로가기

공부 일지 #44 | Django 실습3: 로그인과 회원가입

@studying:)2025. 10. 4. 16:58

학습 날짜: 2025.09.30 ~ 2025.10.01


🔷Django 추가 실습_Zstargram 만들기

1. 프로젝트 기본 설정

  • 학습 일지 42  /  학습 일지 43 참고
  • 터미널에서 프로젝트 생성: django-admin startproject config .
  • 가상환경 및 PyCharm 인터프리터 설정
  • settings.py 에 templates, static, media 경로 설정
  • 시간대 변경:
# settings.py
TIME_ZONE = "Asia/Seoul"

2. Custom User 모델 설정

: 확장 가능하게 하기 위해 기본 auth_user 대신 사용자 정의 모델(User) 설정

# models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
# ↳ 이게 USER관리하는 라이브러리

class User(AbstractUser):
	# blank=True: Null 허용
    profile_image = models.ImageField("프로필 이미지", upload_to="users/profile", blank=True)
    short_description = models.TextField("소개글", blank=True)

----------------------------------------------------------------------------------------------

# settings.py
AUTH_USER_MODEL = "users.User" # {APP이름}.{Model이름}으로 설정!
INSTALLED_APPS = [
    "users",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]
  • 이미지가 들어가기 때문에 가상환경에서 Pillow 라이브러리 설치 필요 → pip install Pillow

3. DB 설정 (MySQL)

: 프로젝트 데이터를 저장할 데이터베이스 연결 설정

  • MySQL에서 프로젝트 이름으로 스키마 생성
  • config > settings.py에 데이터베이스 정보 입력
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "zstargram",	# 각자에 맞게 수정
        "USER": "root",
        "PASSWORD": "----", 	# 각자에 맞게 수정
        "HOST": "localhost",
        "PORT": 3306		# 각자에 맞게 수정
    }
}
  • Migrations 실행: python .\manage.py makemigrations → python .\manage.py migrate
    • .\ 는 있어도 되고, 없어도 됨
  • MySQL 에 가보면, 기본 테이블 auth_user 대신 users_user 생성되어 있음!


4. Admin 등록

: 관리자 페이지에서 사용자 모델을 관리할 수 있도록 등록

# admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from users.models import User

# 사용자(User) 모델을 관리자(admin) 페이지에 등록
@admin.register(User)
class UserAdmin(UserAdmin):
    # 기본 UserAdmin에서 보여줄 필드 구성 (User 수정/상세 페이지)
    fieldsets = [
        (None, {"fields": ("username", "password")}),
        ("개인정보", {"fields": ("first_name", "last_name", "email")}),
        ("추가정보", {"fields": ("profile_image", "short_description")}),
        ("권한", {"fields": ("is_active", "is_staff", "is_superuser")}),
        ("기록", {"fields": ("last_login", "date_joined")}),
    ]
    # 위의 섹션 제목("개인정보", "추가정보" 등)은 자유롭게 작성 가능
    # 다만, 여기에서 지정하지 않으면 admin 폼에서 해당 필드를 볼 수 없음

    # User 생성(Add User) 페이지에서 보이는 필드 구성
    add_fieldsets = (
        (None, {
            "classes": ("wide",),
            "fields": (
                "username", "email", "password1", "password2",
                "profile_image", "short_description"
            ),
        }),
    )

    # 관리자 목록(User 리스트)에서 보여줄 컬럼들
    list_display = (
        "username", "email", "first_name", "last_name",
        "profile_image", "short_description", "is_staff"
    )

5. 로그인 페이지 생성

: 사용자 입력을 받을 로그인 화면 구성

 

a. views.py

def login_view(request):
    return render(request, "users/login.html")

 

b. config/urls.py → users/urls.py 로 분리 관리

  • URL 연결 시, 모든 URL을 config > urls.py 에 직접 등록하면 함수 개수만큼 경로를 만들어야 해서 관리가 비효율적
  • 따라서 config > urls.py 에서는 include("users.urls") 로 부모-자식 구조를 만든 뒤, 세부 경로는 users > urls.py 에서 처리
  • 이렇게 하면 앱 단위로 수정이 편리하고 유지보수가 쉬움
# config > urls.py
from django.contrib import admin
from django.urls import path, include
from config.views import index

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", index),
    path("users/", include("users.urls")) # login 페이지 정보는 users.urls 가서 찾으라는 의미
]

# users > urls.py
from django.urls import path
from users.views import login_view

urlpatterns = [
    path("login/", login_view)

]
  • 절차 정리:
    1. 요청이 들어오면 config > urls.py 먼저 확인
    2. include("users.urls") 확인 후 → users > urls.py 로 이동
    3. 세부 URL과 view 함수로 연결

 

c. templates > users > login.html 만들기


6. Posts 앱 생성 및 Feeds 페이지 추가

: 로그인 후 볼 수 있는 피드 화면 만들기

 

a. posts/views.py에 함수 추가 및 config/urls.py에 url 추가

# posts/views.py
def feeds(request):
    return render(request, "posts/feeds.html")

# config/urls.py
path("posts/", include("posts.urls"))

 

b. templates > posts > feeds.html 만들기


7. 로그인 여부 확인 후 피드 접근 제어

: 로그인하지 않은 사용자는 로그인 페이지로 Redirect 하기

 

a. posts/views.py 함수 수정

# posts > views.py
def feeds(request):
    # 로그인을 했더라면, 어떤 정보를 보내주고,
    # 로그인이 되어 있지 않다라면, 로그인을 하도록 해주자.
    if not request.user.is_authenticated:
        return redirect("/users/login/")

    # print(request.user, request.user.is_authenticated)
    # 로그인 상태면, username, True/False
    # 로그아웃 상태면, AnonymousUser, False
    # 인증받은 user만이 세션을 가짐. 세션은 일종의 통로

    return render(request, "posts/feeds.html")

 

b. users/views.py 함수 수정

# users > views.py
def login_view(request):
    if request.user.is_authenticated:
        return redirect("/posts/feeds/")
    return render(request, "users/login.html")
  • 세션이 있으면 로그인 상태를 유지해서 화면을 띄워주고, 세션이 없으면 로그인 화면으로 돌려보냄
  • 이 정보는 DB의 django_session 테이블에서 관리되며, 로그인/로그아웃 시 테이블 값이 달라짐

8. 로그인 폼(forms) 적용

: Django의 Form 객체를 활용해 입력값 검증 및 렌더링

 

a. users/forms.py 에 LoginForm 클래스 생성

  • django는 객체(Form)만 만들면 HTML에서 바로 다룰 수 있음
  • models 가 DB와 관련된 데이터를 다룬다면, forms 는 화면에서 입력되는 데이터를 다룸
  • min_length, widget 같은 제약 조건을 설정하면, 폼이 자동으로 검증 및 경고 메시지 출력
# users/forms.py
from django import forms

class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=3,
        widget=forms.TextInput(
            attrs={"placeholder": "사용자명(3자리 이상)"}
        ),
    )
    password = forms.CharField(
        min_length=4,
        widget=forms.PasswordInput(
            attrs={"placeholder": "비밀번호(4자리 이상)"}
        ),
    )

 

b. users > views.py수정

# users > views.py
from users.forms import LoginForm
def login_view(request):
    if request.user.is_authenticated:
        return redirect("/posts/feeds/")

	# 인증받지 않았다면,
    if request.method == "GET"
        form = LoginForm()
        context = {
            'form' : form
        }
        return render(request, "users/login.html", context)
    else:
        # Post로 전달이 된 경우,
        form = LoginForm(data=request.POST)

 

c. templates > users > login.html 수정

<!-- 아래 내용 추가 -->
<body>
    <form method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">로그인</button>
    </form>
</body>
  • {{ form.as_p }} → 각 필드가 <p> 태그로 감싸져 출력됨
  • {{ form.as_ul }} → 각 필드가 <li>로 감싸짐
  • {{ form.as_table }} → <tr><td> 구조로 출력

9. 로그인 로직 구현

: 입력한 아이디/비밀번호를 DB와 비교해 세션 생성

# users > views.py 에 추가
else:
        # Post로 전달이 된 경우,
        form = LoginForm(data=request.POST) # POST로 넘어온 데이터를 FORM형태로 변환해서 씀

        # 전달된 데이터에 대한 유효성 검사
        if form.is_valid(): # False/True: 맞게 들어왔는지만 확인
            username = form.cleaned_data["username"] # 유효한 데이터를 반환해줌
            password = form.cleaned_data["password"]

            # DB auth 정보와 일치하는 지 확인
            user = authenticate(username=username, password=password)

            if user:
                login(request, user) # django.contrib.auth에 login 기능도 있음.
                return redirect("/posts/feeds/")
            else:
		        # form을 사용해 error처리도 가능
                form.add_error(None, "입력한 ID나 PW에 해당하는 사용자가 없습니다.")

        context = {'form': form }
        return render(request, "users/login.html", context)

10. 로그아웃 기능 추가

: 로그아웃 버튼을 누르면 세션을 종료하고 로그인 페이지로 이동시키기

 

a. users > views.py에 함수 추가하고, users > urls.py에도 url추가

# users > views.py
def logout_view(request):
    logout(request)
    return redirect("/users/login")

-------------------------------------------------------------------------------------------

# users > urls.py
from users.views import login_view, logout_view

urlpatterns = [
    path("login/", login_view),
    path("logout/", logout_view) # 추가
]

 

b. templates > posts > feeds.html에서 로그아웃 버튼 만들기

<!--templates > posts > feeds.html에 추가 -->
<body>
    <div>{{user.username}}</div>
    <div>ID: {{ user.id }}</div>
    <a href="/users/logout/">로그아웃</a> <!-- 단순히 링크로 추가하기 -->
</body>

11.  회원가입 기능 추가

: 새 사용자를 DB에 추가하고 자동 로그인 처리

 

a. users > forms.py 에 회원가입 폼 추가

# 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()

 

b. users > views.py에 signup 함수 추가

# users > views.py
from django.shortcuts import render, redirect
from users.forms import LoginForm, SignupForm
from users.models import User # for username의 사용 여부 검사

def signup(request):
    if request.method == "POST":
        form = SignupForm(data=request.POST, files=request.FILES)

        # form data 유효성 검사
        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"]

            # password 검사
            if password1 != password2:
                # Field: None으로 하면 상단에 에러메시지가 나타나게 됨
                form.add_error(None, "비밀번호가 같지 않습니다.")

            # username의 사용 여부 검사
            if User.objects.filter(username=username).exists():
                form.add_error(None, "사용 중인 사용명입니다.")

            # form에 error 가 있으면 다시 작성하도록 함.
            if form.errors:
                context = {"form": form}
                return render(request, "users/signup.html")
            # 오류가 없으면 DB에 추가
            else:
                user = User.objects.create_user(
                    username = username,
                    password = password1,
                    profile_image = profile_image,
                    short_description = short_description,
                )
                login(request, user)
                return redirect("/posts/feeds/")
    
    # POST 아니면 그냥 form만 출력
    else:
        form = SignupForm()
        context = {
            "form":form
        }
        return render(request, "users/signup.html", context)

 

c. templates > users> signup.html 만들기

<!--templates > users> signup.html-->
<body>
    <h2>signup page</h2>
    
    <!--"multipart/form-data" 추가해야 이미지랑 텍스트가 같이 전달됨-->
    <form method="POST" enctype="multipart/form-data">
        {%% csrf_token %}
        {{ form.as_div }}
        <button type="submit">회원가입</button>
    </form>
</body>

 

d. users > urls.py 에 url 추가

# users > urls.py
from users.views import login_view, logout_view, signup

urlpatterns = [
    path("login/", login_view),
    path("logout/", logout_view),
    path("signup/", signup), # 추가
]

12. 필드 유효성 검사 추가

: 회원가입 시 입력값을 자동으로 검증하도록 Form 커스터마이징하기

  • 기존에는 views.py에서 직접 username 중복, 비밀번호 일치 여부 등을 검사했지만, Django의 Form 클래스 내부에서 직접 유효성 검사를 수행하도록 변경
  • 이를 위해 users > forms.py와 users > views.py 수정

a. users > forms.py 수정

# 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()

    def clean_username(self):
        username = self.cleaned_data["username"]

        # username의 사용 여부 검사
        if User.objects.filter(username=username).exists():
            # 강제 에러 발생(raise)
            raise ValidationError(f"{username}은 이미 사용 중 입니다.")
        else:
            return username
            
    def clean(self):
        password1 = self.cleaned_data["password1"]
        password2 = self.cleaned_data["password2"]

        # password 검사
        if password1 != password2:
            # 방법2
            self.add_error(None, "비밀번호가 같지 않습니다.")
  • clean_fieldname : 단일 필드 검증
  • clean() : 두 개 이상 필드 검증
  • 함수 옆 과녁 표시는 mother에서 가지고 있는 함수를 overriding한다는 의미
  • 검증 로직이 Form 내부로 들어가면서 views 코드가 훨씬 간결해짐

 

b. users > views.py 수정

# users/views.py
def signup(request):
    if request.method == "POST":
        form = SignupForm(data=request.POST, files=request.FILES)

        if form.is_valid():
            # clean 함수에서 모든 검증이 끝났기 때문에 바로 저장 가능
            username = form.cleaned_data["username"]
            password1 = form.cleaned_data["password1"]
            profile_image = form.cleaned_data["profile_image"]
            short_description = form.cleaned_data["short_description"]

            user = User.objects.create_user(
                username=username,
                password=password1,
                profile_image=profile_image,
                short_description=short_description,
            )
            login(request, user)
            return redirect("/posts/feeds/")

        else:
            # 유효성 검사 실패 시 다시 폼 렌더링
            context = {"form": form}
            return render(request, "users/signup.html", context)

    else:
        form = SignupForm()
        return render(request, "users/signup.html", {"form": form})

13. save() 메서드로 코드 정리

: 검증 후 데이터 저장까지 Form 클래스에서 처리하도록 리팩토링

  • 기존에는 검증과 저장 로직을 views.py에서 모두 처리했음
  • 이제 SignupForm에 save() 메서드를 추가해 저장까지 Form 내부에서 처리
  • views.py는 단순히 form.save()만 호출하면 됨

a. users > forms.py > SignupForm에 save 함수 추가

# users/forms.py
class SignupForm(forms.Form):
    ...
    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

 

b. users > views.py 수정

# 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()  # ✅ 검증과 저장을 form 내부에서 처리
            login(request, user)
            return redirect("/posts/feeds/")
		
        # 에러 검사도 클래스에서 끝냈으니까 else면 바로 다시 폼 작성
        else:
            return render(request, "users/signup.html", {"form": form})

	# POST 아니면 그냥 form만 출력
    else:
        form = SignupForm()
        return render(request, "users/signup.html", {"form": form})
  • form.is_valid() → 검증
  • form.save() → 저장
  • views.py는 흐름만 제어하고, 로직은 전부 Form 안에서 처리
  • 코드 구조가 깔끔해지고 유지보수가 쉬워짐

⭐ 보충 개념 정리

1️⃣ config > views.py vs app > views.py

: 프로젝트 레벨과 앱 레벨 뷰의 차이

구분 프로젝트 레벨 views.py 앱 레벨 views.py
위치 config/views.py (보통 없음) blog/views.py, users/views.py 등
용도 전역 공용, 단순한 페이지 앱(기능)별 핵심 뷰 작성
권장 사용 최소화 → 보통 urls.py에서 바로 앱 연결 각 앱별 로직 분리하여 관리
예시 index 페이지, health check 블로그 글 목록, 댓글 작성, 로그인 등
  • 실제 프로젝트에서는 프로젝트 레벨 views.py는 거의 사용하지 않음
  • 대신 config/urls.py에서 바로 각 앱의 urls.py로 include 시키는 방식 선호
  • 정리
    • 프로젝트 레벨 views.py → 공용/테스트용, 최소한으로 사용
    • 앱 레벨 views.py → 각 기능 구현의 핵심, 주로 작업하는 영역

2️⃣ redirect vs render

: Django에서 페이지 이동 처리 방식

구분 redirect render
기능 페이지 이동 (URL 변경 발생) 페이지 출력 (URL 유지)
동작 방식 내부적으로 다른 view로 요청 전달 템플릿에 데이터를 담아 응답 생성
사용 예시 로그인 후 /posts/feeds/로 이동 로그인 화면 렌더링
핵심 개념 “서버 내에서 페이지 교체 작업” “화면에 내용을 쏘는 행위”

 

3️⃣ clean 함수와 유효성 검사 개념

  • clean_<fieldname>(): 각 필드별 검증
  • clean(): 두 개 이상 필드 동시 검증
  • 여러 조건 검증 시 clean() 내부에서 if문 또는 보조 메서드로 분리 처리
  • clean으로 시작하는 함수들은 유효성 검사(validation) 과정에서 자동 실행

 

studying:)
@studying:) :: what i studied

studying:) 님의 학습 여정을 기록하는 블로그입니다.

목차