WeniVooks

검색

Django 베이스캠프

인증

Django의 인증 시스템은 웹 애플리케이션에서 사용자 계정을 관리하고, 로그인, 로그아웃, 권한 제어 등의 기능을 제공합니다. 이번 챕터에서는 Django의 인증 시스템을 이용한 간단한 블로그를 만들어보도록 하겠습니다.

이해를 돕기 위해 처음에는 forms.py를 사용하지 않고 views.py에 직접 작성한 코드를 사용하고, 나중에 리펙토링하여 forms.py를 사용하는 코드로 수정해보겠습니다. 이를 통해 Django의 인증을 좀 더 자세히 이해할 수 있습니다.

1. 프로젝트 설정

1.1 프로젝트 생성 및 가상환경 설정

프로젝트를 생성하기 전 기획이 필요합니다. 이 단계에서 URL 설계와 ERD 설계, 화면 설계 등을 진행합니다. 여기서는 URL 설계만 해보도록 하겠습니다.

app: main
1.1 ''                          : 메인페이지

---
app: accounts
1.2 'accounts/signup/'          : 회원가입
1.3 'accounts/login/'           : 로그인
1.4 'accounts/logout/'          : 로그아웃 - 로그인 사용자
1.5 'accounts/profile/'         : 프로필 - 로그인 사용자

---
app: blog
2.1 'blog/'                     : 블로그 글 목록
2.2 'blog/<int:pk>/'            : 블로그 상세 글 읽기
2.3 'blog/create/'              : 블로그 글 작성 - 로그인 사용자
2.4 'blog/update/<int:pk>/'     : 블로그 글 업데이트(수정하기) - 로그인 사용자 & 내 글인 경우
2.5 'blog/delete/<int:pk>/'     : 블로그 글 삭제 - 로그인 사용자 & 내 글인 경우
app: main
1.1 ''                          : 메인페이지

---
app: accounts
1.2 'accounts/signup/'          : 회원가입
1.3 'accounts/login/'           : 로그인
1.4 'accounts/logout/'          : 로그아웃 - 로그인 사용자
1.5 'accounts/profile/'         : 프로필 - 로그인 사용자

---
app: blog
2.1 'blog/'                     : 블로그 글 목록
2.2 'blog/<int:pk>/'            : 블로그 상세 글 읽기
2.3 'blog/create/'              : 블로그 글 작성 - 로그인 사용자
2.4 'blog/update/<int:pk>/'     : 블로그 글 업데이트(수정하기) - 로그인 사용자 & 내 글인 경우
2.5 'blog/delete/<int:pk>/'     : 블로그 글 삭제 - 로그인 사용자 & 내 글인 경우

먼저 새로운 프로젝트를 위한 디렉토리를 만들고 가상환경을 설정합니다. 아래 명령어를 터미널에서 실행해주세요.

mkdir 04_2_blog_accounts
cd 04_2_blog_accounts
python -m venv venv
.\venv\Scripts\activate
pip install django
pip install pillow
pip freeze > requirements.txt
django-admin startproject config .
python manage.py migrate
python manage.py startapp main
python manage.py startapp blog
python manage.py startapp accounts
mkdir 04_2_blog_accounts
cd 04_2_blog_accounts
python -m venv venv
.\venv\Scripts\activate
pip install django
pip install pillow
pip freeze > requirements.txt
django-admin startproject config .
python manage.py migrate
python manage.py startapp main
python manage.py startapp blog
python manage.py startapp accounts

앞 챕터와 다른 점은 앱을 3개를 동시에 생성했다는 것입니다. main, blog, accounts 앱을 생성했습니다.

1.2 설정 파일 수정

프로젝트의 settings.py 파일을 열고 다음과 같이 수정해주세요.

ALLOWED_HOSTS = ['*']
 
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'main',
    'blog',
    'accounts',
]
 
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
 
LANGUAGE_CODE = "ko-kr"
 
TIME_ZONE = "Asia/Seoul"
 
STATIC_URL = "static/"
STATICFILES_DIRS = [
    BASE_DIR / "static",
]
 
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
ALLOWED_HOSTS = ['*']
 
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'main',
    'blog',
    'accounts',
]
 
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
 
LANGUAGE_CODE = "ko-kr"
 
TIME_ZONE = "Asia/Seoul"
 
STATIC_URL = "static/"
STATICFILES_DIRS = [
    BASE_DIR / "static",
]
 
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"

이 설정은 우리가 만든 앱들을 프로젝트에 등록하고, 템플릿 디렉토리를 지정하며, 한국어와 서울 시간대를 사용하도록 설정합니다. 또한 정적 파일과 미디어 파일의 위치를 지정합니다.

1.3 필요한 디렉토리 생성

프로젝트 04_2_blog_accounts 디렉토리에서 다음 명령어를 실행하여 필요한 디렉토리를 생성합니다.

mkdir static
mkdir media
mkdir templates
mkdir static
mkdir media
mkdir templates
1.4 관리자 계정 생성

다음 명령어로 관리자 계정을 생성합니다.

python manage.py createsuperuser
python manage.py createsuperuser

프롬프트에 따라 사용자 이름, 이메일, 비밀번호를 입력합니다.

2. 블로그 앱 설정

2.1 모델 정의

blog/models.py 파일을 열고 다음과 같이 Post 모델을 정의합니다:

from django.db import models
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    contents = models.TextField()
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
 
    def __str__(self):
        return self.title
from django.db import models
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    contents = models.TextField()
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
 
    def __str__(self):
        return self.title

이 모델은 제목, 내용, 작성자를 가진 블로그 포스트를 나타냅니다.

2.2 관리자 페이지에 모델 등록

blog/admin.py 파일을 열고 다음과 같이 수정합니다.

from django.contrib import admin
from .models import Post
 
admin.site.register(Post)
from django.contrib import admin
from .models import Post
 
admin.site.register(Post)

이렇게 하면 관리자 페이지에서 Post 모델을 관리할 수 있습니다.

2.3 데이터베이스 마이그레이션

모델을 정의했으니 데이터베이스에 반영해야 합니다. 다음 명령어를 실행합니다.

python manage.py makemigrations
python manage.py migrate
python manage.py makemigrations
python manage.py migrate

3. URL 설정

3.1 프로젝트 URL 설정

config/urls.py 파일을 다음과 같이 수정합니다.

from django.contrib import admin
from django.urls import path, include
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
    path("accounts/", include("accounts.urls")),
]
from django.contrib import admin
from django.urls import path, include
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
    path("accounts/", include("accounts.urls")),
]
3.2 블로그 앱 URL 설정

blog/urls.py 파일을 생성하고 다음과 같이 작성합니다.

from django.urls import path
from . import views
 
urlpatterns = [
    path("", views.blog_list, name="blog_list"),
    path("<int:pk>/", views.blog_detail, name="blog_detail"),
    path("create/", views.blog_create, name="blog_create"),
    path("update/<int:pk>/", views.blog_update, name="blog_update"),
    path("delete/<int:pk>/", views.blog_delete, name="blog_delete"),
]
from django.urls import path
from . import views
 
urlpatterns = [
    path("", views.blog_list, name="blog_list"),
    path("<int:pk>/", views.blog_detail, name="blog_detail"),
    path("create/", views.blog_create, name="blog_create"),
    path("update/<int:pk>/", views.blog_update, name="blog_update"),
    path("delete/<int:pk>/", views.blog_delete, name="blog_delete"),
]
3.3 계정 앱 URL 설정

accounts/urls.py 파일을 생성하고 다음과 같이 작성합니다:

from django.urls import path
from . import views
 
urlpatterns = [
    path("signup/", views.user_signup, name="user_signup"),
    path("login/", views.user_login, name="user_login"),
    path("logout/", views.user_logout, name="user_logout"),
    path("profile/", views.user_profile, name="user_profile"),
]
from django.urls import path
from . import views
 
urlpatterns = [
    path("signup/", views.user_signup, name="user_signup"),
    path("login/", views.user_login, name="user_login"),
    path("logout/", views.user_logout, name="user_logout"),
    path("profile/", views.user_profile, name="user_profile"),
]

4. 뷰 구현

4.1 블로그 앱 뷰

blog/views.py 파일을 다음과 같이 작성합니다. 앞 챕터에서 했던 내용입니다.

from django.shortcuts import render, redirect, get_object_or_404
from django.db.models import Q
from .models import Post
from .forms import PostForm
 
def blog_list(request):
    if request.GET.get("q"):
        posts = Post.objects.filter(
            Q(title__contains=request.GET.get("q"))
            | Q(contents__contains=request.GET.get("q"))
        ).distinct()
    else:
        posts = Post.objects.all()
    context = {"object_list": posts}
    return render(request, "blog/blog_list.html", context)
 
def blog_detail(request, pk):
    post = Post.objects.get(pk=pk)
    context = {"object": post}
    return render(request, "blog/blog_detail.html", context)
 
def blog_create(request):
    if request.method == "GET":
        form = PostForm()
        context = {"form": form}
        return render(request, "blog/blog_create.html", context)
    elif request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save()
            return redirect("blog_list")
        else:
            context = {
                "form": form,
                "error": "입력이 잘못되었습니다. 알맞은 형식으로 다시 입력해주세요!",
            }
            return render(request, "blog/blog_create.html", context)
 
def blog_update(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            return redirect("blog_detail", pk=post.pk)
    else:
        form = PostForm(instance=post)
        context = {"form": form, "pk": pk}
        return render(request, "blog/blog_update.html", context)
 
def blog_delete(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        post.delete()
    return redirect("blog_list")
from django.shortcuts import render, redirect, get_object_or_404
from django.db.models import Q
from .models import Post
from .forms import PostForm
 
def blog_list(request):
    if request.GET.get("q"):
        posts = Post.objects.filter(
            Q(title__contains=request.GET.get("q"))
            | Q(contents__contains=request.GET.get("q"))
        ).distinct()
    else:
        posts = Post.objects.all()
    context = {"object_list": posts}
    return render(request, "blog/blog_list.html", context)
 
def blog_detail(request, pk):
    post = Post.objects.get(pk=pk)
    context = {"object": post}
    return render(request, "blog/blog_detail.html", context)
 
def blog_create(request):
    if request.method == "GET":
        form = PostForm()
        context = {"form": form}
        return render(request, "blog/blog_create.html", context)
    elif request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save()
            return redirect("blog_list")
        else:
            context = {
                "form": form,
                "error": "입력이 잘못되었습니다. 알맞은 형식으로 다시 입력해주세요!",
            }
            return render(request, "blog/blog_create.html", context)
 
def blog_update(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            return redirect("blog_detail", pk=post.pk)
    else:
        form = PostForm(instance=post)
        context = {"form": form, "pk": pk}
        return render(request, "blog/blog_update.html", context)
 
def blog_delete(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        post.delete()
    return redirect("blog_list")
4.2 계정 앱 뷰

accounts/views.py 파일을 다음과 같이 작성합니다.

from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
 
def user_signup(request):
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        email = request.POST.get("email", "")
        if not (username and password):
            return HttpResponse("이름과 패스워드는 필수입니다.")
        if User.objects.filter(username=username).exists():
            return HttpResponse("유저이름이 이미 있습니다.")
        if email and User.objects.filter(email=email).exists():
            return HttpResponse("이메일이 이미 있습니다.")
        user = User.objects.create_user(username, email, password)
        user.save()
        user = authenticate(username=username, password=password)
        login(request, user)
        return redirect("user_profile")
    else:
        return render(request, "accounts/signup.html")
 
def user_login(request):
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            return redirect("user_profile")
        else:
            return render(
                request,
                "accounts/login.html",
                {"error": "아이디나 패스워드가 맞지 않습니다."},
            )
    else:
        return render(request, "accounts/login.html")
 
def user_logout(request):
    logout(request)
    return redirect("user_login")
 
@login_required
def user_profile(request):
    return render(request, "accounts/profile.html", {"user": request.user})
from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
 
def user_signup(request):
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        email = request.POST.get("email", "")
        if not (username and password):
            return HttpResponse("이름과 패스워드는 필수입니다.")
        if User.objects.filter(username=username).exists():
            return HttpResponse("유저이름이 이미 있습니다.")
        if email and User.objects.filter(email=email).exists():
            return HttpResponse("이메일이 이미 있습니다.")
        user = User.objects.create_user(username, email, password)
        user.save()
        user = authenticate(username=username, password=password)
        login(request, user)
        return redirect("user_profile")
    else:
        return render(request, "accounts/signup.html")
 
def user_login(request):
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            return redirect("user_profile")
        else:
            return render(
                request,
                "accounts/login.html",
                {"error": "아이디나 패스워드가 맞지 않습니다."},
            )
    else:
        return render(request, "accounts/login.html")
 
def user_logout(request):
    logout(request)
    return redirect("user_login")
 
@login_required
def user_profile(request):
    return render(request, "accounts/profile.html", {"user": request.user})

가장 먼저 user_signup부터 살펴보도록 하겠습니다. 읽이 쉽도록 주석으로 설명을 추가하겠습니다.

def user_signup(request):
    '''
    사용자가 회원가입하는 기능을 구현합니다.
    '''
    # POST 요청이 들어왔을 때
    if request.method == "POST":
        # POST 요청으로 받은 데이터에서 username, password, email을 가져옵니다.
        username = request.POST["username"]
        password = request.POST["password"]
        email = request.POST.get("email", "")
        # username과 password는 필수이므로 둘 중 하나라도 없으면 에러 메시지를 반환합니다.
        if not (username and password):
            return HttpResponse("이름과 패스워드는 필수입니다.")
        # username이 이미 존재하는지 확인하고, 존재하면 에러 메시지를 반환합니다.
        if User.objects.filter(username=username).exists():
            return HttpResponse("유저이름이 이미 있습니다.")
        # email이 존재하고 이미 존재하는지 확인하고, 존재하면 에러 메시지를 반환합니다.
        if email and User.objects.filter(email=email).exists():
            return HttpResponse("이메일이 이미 있습니다.")
        # User 모델의 create_user 메서드를 사용하여 새로운 사용자를 생성합니다.
        # 여기서 중요한 점은 create_user를 사용하면 비밀번호가 해시로 저장된다는 것입니다.
        # 만약 create_user를 사용하지 않고 create 메서드를 사용하면 비밀번호가 평문으로 저장됩니다.
        # 또한 user = User(username=username, email=email)로 사용자를 생성하고 패스워드만 user.set_password(password)로 비밀번호를 해쉬해서 저장할 수 있습니다.
        user = User.objects.create_user(username, email, password)
        user.save()
        # 사용자를 로그인시키고 프로필 페이지로 리다이렉트합니다.
        # authenticate 함수는 사용자를 인증하는 함수입니다. 인증이 될 경우 사용자 객체를 반환합니다.
        # login 함수는 사용자를 로그인시키는 함수입니다.
        # 로그인이 될 경우 request.user에 사용자 정보가 저장됩니다.
        user = authenticate(username=username, password=password)
        login(request, user)
        return redirect("user_profile")
    else:
        return render(request, "accounts/signup.html")
def user_signup(request):
    '''
    사용자가 회원가입하는 기능을 구현합니다.
    '''
    # POST 요청이 들어왔을 때
    if request.method == "POST":
        # POST 요청으로 받은 데이터에서 username, password, email을 가져옵니다.
        username = request.POST["username"]
        password = request.POST["password"]
        email = request.POST.get("email", "")
        # username과 password는 필수이므로 둘 중 하나라도 없으면 에러 메시지를 반환합니다.
        if not (username and password):
            return HttpResponse("이름과 패스워드는 필수입니다.")
        # username이 이미 존재하는지 확인하고, 존재하면 에러 메시지를 반환합니다.
        if User.objects.filter(username=username).exists():
            return HttpResponse("유저이름이 이미 있습니다.")
        # email이 존재하고 이미 존재하는지 확인하고, 존재하면 에러 메시지를 반환합니다.
        if email and User.objects.filter(email=email).exists():
            return HttpResponse("이메일이 이미 있습니다.")
        # User 모델의 create_user 메서드를 사용하여 새로운 사용자를 생성합니다.
        # 여기서 중요한 점은 create_user를 사용하면 비밀번호가 해시로 저장된다는 것입니다.
        # 만약 create_user를 사용하지 않고 create 메서드를 사용하면 비밀번호가 평문으로 저장됩니다.
        # 또한 user = User(username=username, email=email)로 사용자를 생성하고 패스워드만 user.set_password(password)로 비밀번호를 해쉬해서 저장할 수 있습니다.
        user = User.objects.create_user(username, email, password)
        user.save()
        # 사용자를 로그인시키고 프로필 페이지로 리다이렉트합니다.
        # authenticate 함수는 사용자를 인증하는 함수입니다. 인증이 될 경우 사용자 객체를 반환합니다.
        # login 함수는 사용자를 로그인시키는 함수입니다.
        # 로그인이 될 경우 request.user에 사용자 정보가 저장됩니다.
        user = authenticate(username=username, password=password)
        login(request, user)
        return redirect("user_profile")
    else:
        return render(request, "accounts/signup.html")

user_login 함수는 사용자가 로그인하는 기능을 구현한 것입니다. 사용자가 로그인에 성공하면 user_profile 페이지로 리다이렉트합니다. 로그인에 실패하면 로그인 페이지로 다시 리다이렉트하고 에러 메시지를 표시합니다. 로그아웃까지 한 번에 설명하도록 하겠습니다.

def user_login(request):
    '''
    사용자가 로그인하는 기능을 구현합니다.
    '''
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        user = authenticate(request, username=username, password=password)
        if user is not None:
            # 사용자가 인증되면 로그인시키고 프로필 페이지로 리다이렉트합니다.
            login(request, user)
            return redirect("user_profile")
        else:
            # 사용자가 인증되지 않으면 로그인 페이지로 리다이렉트하고 에러 메시지를 표시합니다.
            return render(
                request,
                "accounts/login.html",
                {"error": "아이디나 패스워드가 맞지 않습니다."},
            )
    else:
        return render(request, "accounts/login.html")
 
def user_logout(request):
    # 로그아웃 함수입니다. 로그아웃 후 로그인 페이지로 리다이렉트합니다.
    # 리다이렉트되는 URL은 settings.py에 LOGIN_URL로 설정된 URL입니다.
    # 우리가 설정할 수 있습니다.
    # logout 함수는 사용자를 로그아웃시키는 함수입니다.
    # request.user에 사용자 정보가 저장되어 있으므로 이를 사용하여 로그아웃합니다.
    # 앞으로 request에는 사용자 정보가 담기지 않게 됩니다.
    logout(request)
    return redirect("user_login")
def user_login(request):
    '''
    사용자가 로그인하는 기능을 구현합니다.
    '''
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        user = authenticate(request, username=username, password=password)
        if user is not None:
            # 사용자가 인증되면 로그인시키고 프로필 페이지로 리다이렉트합니다.
            login(request, user)
            return redirect("user_profile")
        else:
            # 사용자가 인증되지 않으면 로그인 페이지로 리다이렉트하고 에러 메시지를 표시합니다.
            return render(
                request,
                "accounts/login.html",
                {"error": "아이디나 패스워드가 맞지 않습니다."},
            )
    else:
        return render(request, "accounts/login.html")
 
def user_logout(request):
    # 로그아웃 함수입니다. 로그아웃 후 로그인 페이지로 리다이렉트합니다.
    # 리다이렉트되는 URL은 settings.py에 LOGIN_URL로 설정된 URL입니다.
    # 우리가 설정할 수 있습니다.
    # logout 함수는 사용자를 로그아웃시키는 함수입니다.
    # request.user에 사용자 정보가 저장되어 있으므로 이를 사용하여 로그아웃합니다.
    # 앞으로 request에는 사용자 정보가 담기지 않게 됩니다.
    logout(request)
    return redirect("user_login")

마지막으로 user_profile 함수는 사용자 프로필 페이지를 보여주는 함수입니다. 이 함수는 로그인이 필요한 함수이므로 @login_required 데코레이터를 사용하여 로그인 여부를 확인합니다.

@login_required
def user_profile(request):
    return render(request, "accounts/profile.html", {"user": request.user})
@login_required
def user_profile(request):
    return render(request, "accounts/profile.html", {"user": request.user})

여기서 @login_required 데코레이터는 사용자가 로그인되어 있는지 확인하는 데코레이터입니다. 사용자가 로그인되어 있지 않으면 로그인 페이지로 리다이렉트됩니다. 로그인이 필요한 모든 뷰에 이 데코레이터를 사용할 수 있습니다.

이 데커레이터는 from django.contrib.auth.decorators import login_required에 있으며 경로명은 \venv\Lib\site-packages\django\contrib\auth\decorators.py에 있습니다. 이 데커레이터는 사용자가 로그인되어 있지 않으면 로그인 페이지로 리다이렉트합니다. 이 경로에 있는 permission_required, user_passes_test 데커레이터도 많이 사용되는 데커레이터 중 하나입니다. 우리 수업에서는 @login_required 데커레이터만 사용하겠습니다.

5. 폼 구현

blog/forms.py 파일을 생성하고 다음과 같이 작성합니다.

from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = "__all__"
from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = "__all__"

6. 템플릿 구현

각 앱의 templates 디렉토리 안에 필요한 HTML 파일들을 생성합니다.

  • blog/templates/blog/blog_list.html
  • blog/templates/blog/blog_detail.html
  • blog/templates/blog/blog_create.html
  • blog/templates/blog/blog_update.html
  • accounts/templates/accounts/profile.html
  • accounts/templates/accounts/login.html
  • accounts/templates/accounts/signup.html

각 템플릿 파일의 내용은 아래 제공된 HTML 코드를 참고하여 작성합니다.

################################
# blog_list.html
 
<ul>
    {% for blog in object_list %}
    <li><a href="/blog/{{blog.id}}">{{blog.title}}</a></li>
    {% endfor %}
</ul>
 
################################
# blog_detail.html
 
<p>{{object.title}}</p>
<p>{{object.contents}}</p>
 
<!-- 로그인을 했고, 내가 이 글에 글쓴이라고 한다면 삭제와 업데이트 버튼 노출 -->
{% if user.is_authenticated and user == object.author %}
    <a href="{% url 'blog_update' object.pk %}">수정</a>
    <form action="{% url 'blog_delete' object.pk %}" method="post">
        {% csrf_token %}
        <input type="submit" value="삭제">
    </form>
{% endif %}
 
################################
# blog_update.html
 
<form action="" method="post">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <input type="submit">
</form>
 
################################
# blog_create.html
 
<form action="" method="post">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <input type="submit">
</form>
 
################################
# profile.html
 
<h1>개인 프로필 페이지</h1>
<p>{{ user }}</p>
<p>{{ user.username }}</p>
<p>{{ user.email }}</p>
<p>{{ user.first_name }}</p>
<p>{{ user.last_name }}</p>
<p>{{ user.is_staff }}</p>
<p>{{ user.is_active }}</p>
<p>{{ user.is_superuser }}</p>
<p>{{ user.last_login }}</p>
<p>{{ user.date_joined }}</p>
 
<form action="{% url 'user_logout' %}" method="post">
  {% csrf_token %}
  <input type="submit" value="로그아웃">
</form>
 
################################
# login.html
 
<form method="post">
    {% csrf_token %}
    <label for="username_id">아이디</label>
    <input id="username_id" type="text" name="username">
 
    <label for="password_id">비밀번호</label>
    <input id="password_id" type="password" name="password">
 
    <button type="submit">로그인</button>
</form>
 
################################
# signup.html
# {% csrf_token %}는 form에 안쪽에 있어야 합니다.
 
<form action="" method="post">
    {% csrf_token %}
    <label for="username_id">아이디</label>
    <input id="username_id" type="text" name="username">
 
    <label for="email_id">이메일</label>
    <input id="email_id" type="text" name="email">
    
 
    <label for="password_id">비밀번호</label>
    <input id="password_id" type="password" name="password">
    
    <button type="submit">회원가입</button>
</form>
 
################################
################################
# blog_list.html
 
<ul>
    {% for blog in object_list %}
    <li><a href="/blog/{{blog.id}}">{{blog.title}}</a></li>
    {% endfor %}
</ul>
 
################################
# blog_detail.html
 
<p>{{object.title}}</p>
<p>{{object.contents}}</p>
 
<!-- 로그인을 했고, 내가 이 글에 글쓴이라고 한다면 삭제와 업데이트 버튼 노출 -->
{% if user.is_authenticated and user == object.author %}
    <a href="{% url 'blog_update' object.pk %}">수정</a>
    <form action="{% url 'blog_delete' object.pk %}" method="post">
        {% csrf_token %}
        <input type="submit" value="삭제">
    </form>
{% endif %}
 
################################
# blog_update.html
 
<form action="" method="post">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <input type="submit">
</form>
 
################################
# blog_create.html
 
<form action="" method="post">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <input type="submit">
</form>
 
################################
# profile.html
 
<h1>개인 프로필 페이지</h1>
<p>{{ user }}</p>
<p>{{ user.username }}</p>
<p>{{ user.email }}</p>
<p>{{ user.first_name }}</p>
<p>{{ user.last_name }}</p>
<p>{{ user.is_staff }}</p>
<p>{{ user.is_active }}</p>
<p>{{ user.is_superuser }}</p>
<p>{{ user.last_login }}</p>
<p>{{ user.date_joined }}</p>
 
<form action="{% url 'user_logout' %}" method="post">
  {% csrf_token %}
  <input type="submit" value="로그아웃">
</form>
 
################################
# login.html
 
<form method="post">
    {% csrf_token %}
    <label for="username_id">아이디</label>
    <input id="username_id" type="text" name="username">
 
    <label for="password_id">비밀번호</label>
    <input id="password_id" type="password" name="password">
 
    <button type="submit">로그인</button>
</form>
 
################################
# signup.html
# {% csrf_token %}는 form에 안쪽에 있어야 합니다.
 
<form action="" method="post">
    {% csrf_token %}
    <label for="username_id">아이디</label>
    <input id="username_id" type="text" name="username">
 
    <label for="email_id">이메일</label>
    <input id="email_id" type="text" name="email">
    
 
    <label for="password_id">비밀번호</label>
    <input id="password_id" type="password" name="password">
    
    <button type="submit">회원가입</button>
</form>
 
################################

7. 실행 및 테스트

모든 설정이 완료되었다면, 다음 명령어로 서버를 실행합니다.

python manage.py runserver
python manage.py runserver

이제 브라우저에서 다음 URL들을 테스트해볼 수 있습니다.

8. 리펙토링

앞서 작성한 코드는 forms.py를 사용하지 않고, views.py에 직접 작성한 코드입니다. 이 코드는 가독성이 떨어지고, 유지보수가 어렵습니다. 따라서 코드를 리펙토링하여 가독성을 높이고, 유지보수를 쉽게 할 수 있도록 수정해보겠습니다.

8.1 forms.py 작성

accounts/forms.py 파일을 생성하고 다음과 같이 작성합니다.

from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User
 
class SignUpForm(UserCreationForm):
    email = forms.EmailField(max_length=254, required=True, help_text='Required. Inform a valid email address.')
 
    class Meta:
        model = User
        fields = ('username', 'email', 'password1', 'password2')
 
    def clean_username(self):
        username = self.cleaned_data.get('username')
        if User.objects.filter(username=username).exists():
            raise forms.ValidationError("유저이름이 이미 있습니다.")
        return username
 
    def clean_email(self):
        email = self.cleaned_data.get('email')
        if User.objects.filter(email=email).exists():
            raise forms.ValidationError("이메일이 이미 있습니다.")
        return email
 
class LoginForm(AuthenticationForm):
    username = forms.CharField(max_length=254)
    password = forms.CharField(widget=forms.PasswordInput)
from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User
 
class SignUpForm(UserCreationForm):
    email = forms.EmailField(max_length=254, required=True, help_text='Required. Inform a valid email address.')
 
    class Meta:
        model = User
        fields = ('username', 'email', 'password1', 'password2')
 
    def clean_username(self):
        username = self.cleaned_data.get('username')
        if User.objects.filter(username=username).exists():
            raise forms.ValidationError("유저이름이 이미 있습니다.")
        return username
 
    def clean_email(self):
        email = self.cleaned_data.get('email')
        if User.objects.filter(email=email).exists():
            raise forms.ValidationError("이메일이 이미 있습니다.")
        return email
 
class LoginForm(AuthenticationForm):
    username = forms.CharField(max_length=254)
    password = forms.CharField(widget=forms.PasswordInput)
8.2 views.py 리펙토링

accounts/views.py 파일을 다음과 같이 수정합니다.

from django.shortcuts import render, redirect
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import SignUpForm, LoginForm
 
def user_signup(request):
    if request.method == "POST":
        form = SignUpForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect("user_profile")
    else:
        form = SignUpForm()
    return render(request, "accounts/signup.html", {'form': form})
 
def user_login(request):
    if request.method == "POST":
        form = LoginForm(request, data=request.POST)
        if form.is_valid():
            user = form.get_user()
            login(request, user)
            return redirect("user_profile")
    else:
        form = LoginForm()
    return render(request, "accounts/login.html", {'form': form})
 
def user_logout(request):
    logout(request)
    return redirect("user_login")
 
@login_required
def user_profile(request):
    return render(request, "accounts/profile.html", {"user": request.user})
from django.shortcuts import render, redirect
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import SignUpForm, LoginForm
 
def user_signup(request):
    if request.method == "POST":
        form = SignUpForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect("user_profile")
    else:
        form = SignUpForm()
    return render(request, "accounts/signup.html", {'form': form})
 
def user_login(request):
    if request.method == "POST":
        form = LoginForm(request, data=request.POST)
        if form.is_valid():
            user = form.get_user()
            login(request, user)
            return redirect("user_profile")
    else:
        form = LoginForm()
    return render(request, "accounts/login.html", {'form': form})
 
def user_logout(request):
    logout(request)
    return redirect("user_login")
 
@login_required
def user_profile(request):
    return render(request, "accounts/profile.html", {"user": request.user})
8.3 템플릿 수정

모든 템플릿은 forms.py에 맞게 수정해야 합니다. accounts/login.html, accounts/signup.html 파일을 다음과 같이 수정합니다.

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Submit</button>
</form>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Submit</button>
</form>
8.4 실행 및 테스트

모든 설정이 완료되었다면, 다음 명령어로 서버를 실행합니다.

python manage.py runserver
python manage.py runserver

이제 브라우저에서 다음 URL들을 테스트해볼 수 있습니다.

9. 마치며

실습은 하지 않지만 CBV로 작성하면 views.py가 아래와 같이 간단해집니다.

from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.views import LoginView, LogoutView
from django.views.generic import CreateView
from django.shortcuts import render
 
 
user_signup = CreateView.as_view(
    form_class=UserCreationForm,
    template_name="accounts/signup.html",
    success_url=settings.LOGIN_URL,
)
 
 
user_login = LoginView.as_view(
    template_name="accounts/login.html",
)
 
 
user_logout = LogoutView.as_view(
    next_page=settings.LOGIN_URL,
)
 
@login_required
def user_profile(request):
    return render(request, "accounts/profile.html")
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.views import LoginView, LogoutView
from django.views.generic import CreateView
from django.shortcuts import render
 
 
user_signup = CreateView.as_view(
    form_class=UserCreationForm,
    template_name="accounts/signup.html",
    success_url=settings.LOGIN_URL,
)
 
 
user_login = LoginView.as_view(
    template_name="accounts/login.html",
)
 
 
user_logout = LogoutView.as_view(
    next_page=settings.LOGIN_URL,
)
 
@login_required
def user_profile(request):
    return render(request, "accounts/profile.html")
# settings.py
LOGIN_URL = "/accounts/login/"
# settings.py
LOGIN_URL = "/accounts/login/"

이처럼 Django의 기능을 잘 활용하면 코드가 간결해지고 가독성이 높아집니다. 또한 캐시 기능 등 다양한 Django의 코드를 활용하면 더욱 효율적인 웹 애플리케이션을 만들 수 있습니다. User또한 커스터마이징 할 수 있습니다. 1:1 필드를 만들어 커스터마이징 하는 방법, AbstractUser를 상속받아 커스터마이징 하는 방법, AbstractBaseUser를 상속받아 커스터마이징 하는 방법 등이 있습니다.

여기서 미처 다루지 못한 내용들을 학습해가며 Django의 다양한 기능을 활용하여 여러 웹 애플리케이션을 개발해보고, 더 높은 수준의 Django 개발자가 되시길 바랍니다.

강의는 여기까지이며, 나머지는 부록과 프로젝트입니다. 고생 많으셨습니다.

4.1 form5장 project