WeniVooks

검색

Django 베이스캠프

form

django에서 데이터를 입력받아 데이터베이스에 저장하는 방법은 앞서 배운 방법으로도 충분히 할 수 있습니다. 다만 그랬을 경우 사용자가 입력한 데이터가 유효한지 검사하거나, 입력받은 데이터를 다양한 형태로 변환하는 등의 작업을 직접 해야 합니다. 이러한 작업을 편리하게 처리하기 위해 Django는 폼(Form) 시스템을 제공합니다.

Django의 폼은 사용자가 제공하는 데이터를 안전하게 입력받고 자동으로 검사합니다. 이 폼 기능을 사용하면 텍스트, 숫자, 이메일 등 다양한 유형의 데이터를 처리할 수 있는 여러 필드 타입을 제공합니다. 마치 모델처럼요. 이 폼을 잘 사용하면 더 쉽고 빠르게 웹 서비스를 만들 수 있습니다.

장고에 폼 기능을 사용하는 것은 필수가 아닙니다. 하지만 폼을 사용하면 사용자가 입력한 데이터를 쉽게 검사하고 처리할 수 있기 때문에 많은 개발자들이 이 폼 기능을 이용하여 서비스를 개발합니다.

1. 프로젝트 설정 및 기본 구조 만들기

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

아래 코드를 복사해서, 작업할 폴더의 터미널에 shift + insert합니다.

mkdir 04_1_form
cd 04_1_form
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 blog
mkdir 04_1_form
cd 04_1_form
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 blog

04_1_form 폴더를 생성하고, 가상환경 설치, django,pillow 라이브러리 설치, blog앱 생성까지 완료되었습니다. 여기서 앞 시간과 다른 명령어가 하나 있습니다. pip freeze > requirements.txt 명령어 입니다. 이 명령어는 가상환경에 설치한 라이브러리와 그 버전을 requirements파일로 만들어 줍니다. 이 파일로 나중에 다른 환경에 동일한 버전을 설치하는 것이 가능합니다.

아래는 방금 생성된 requirements파일입니다. 만약 새로운 가상환경에서 프로젝트를 시작할 때 설정해야 할 라이브러리가 너무 많으면 관리하기가 어려워집니다. 하지만 requirements 파일을 사용하면 필요한 모든 라이브러리를 한 번에 쉽게 설치할 수 있어서 매우 편리합니다. 또 팀 프로젝터에서도 매우 유용합니다. 모든 팀원이 이 파일을 통해 동일한 개발 환경을 쉽게 만들 수 있습니다. 다만 규모가 있는 프로젝트에서는 좀 더 다양한 기능을 지원하는 pipenvpoetry를 사용하는 것이 좋습니다.

asgiref==3.8.1
Django==5.1
pillow==10.4.0
sqlparse==0.5.1
typing_extensions==4.12.2
tzdata==2024.1
asgiref==3.8.1
Django==5.1
pillow==10.4.0
sqlparse==0.5.1
typing_extensions==4.12.2
tzdata==2024.1

pip install -r requirements.txt 명령어로 설치 할 수 있습니다.

1.2 기본 설정

config/settings.py 파일을 열어서, ALLOWED_HOSTS, INSTALLED_APPS를 수정하고, static파일과 media파일을 사용할 수 있게 합니다.

#config > settings.py
ALLOWED_HOSTS = ["*"]
 
INSTALLED_APPS = [
    ...
    "blog",
]
 
LANGUAGE_CODE = "ko-kr"
TIME_ZONE = "Asia/Seoul"
 
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
 
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
#config > settings.py
ALLOWED_HOSTS = ["*"]
 
INSTALLED_APPS = [
    ...
    "blog",
]
 
LANGUAGE_CODE = "ko-kr"
TIME_ZONE = "Asia/Seoul"
 
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
 
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

아래 명령어를 이용해서 static, media 폴더 생성합니다.

mkdir static
mkdir media
mkdir static
mkdir media
1.3 URL 설정

아래와 같이 URL을 설정합니다. 여기서 blog/create는 인증을 배운 후 로그인한 사용자만 작성할 수 있도록 수정할 예정입니다.

  • 앱이름: blog
URL views 함수이름 html 파일이름 비고
blog/ blog_list blog_list.html 블로그 글 목록
blog/<int:pk>/ blog_detail blog_detail.html 블로그 상세 글 읽기
blog/create/ blog_create blog_create.html 블로그 글 작성
  1. config/urls.py 파일을 다음과 같이 수정합니다.
#config > urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
]
 
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
#config > urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
]
 
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
  1. blog 앱에 blog/urls.py 파일을 생성하고 다음 내용을 추가합니다.
# 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"),
]
# 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"),
]

2. 기본 뷰 및 템플릿 구현

views.py 코드가 조금씩 복잡해져 가는 것을 느끼실 수 있을겁니다. 이렇게 수백줄이 되면 함수형 뷰로는 관리하기 어려워집니다. 이럴 때에는 클래스형 뷰를 섞어 사용하면 좋습니다. 또한 ChatGPT를 사용하여 모르는 코드의 상세한 주석을 요구할 수 있으니 모르는 것이 있을 경우 넘어가지 말고, 상세한 주석을 달아놓기를 권해드립니다. 주석을 달 때 docstring을 사용하면 좋습니다.

2.1 View
#blog > views.py
from django.shortcuts import render
from django.db.models import Q
from .models import Post
from django.shortcuts import redirect
# from .forms import PostForm
 
def blog_list(request):
    if request.GET.get("q"):
        db = Post.objects.filter(
            Q(title__contains=request.GET.get("q"))
            | Q(contents__contains=request.GET.get("q"))
        ).distinct()
    else:
        db = Post.objects.all()
    context = {"object_list": db}
    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 == "POST":
        title = request.POST.get("title")
        contents = request.POST.get("contents")
        Post.objects.create(title=title, contents=contents)
        return redirect("blog_list")
    return render(request, "blog/blog_create.html")
#blog > views.py
from django.shortcuts import render
from django.db.models import Q
from .models import Post
from django.shortcuts import redirect
# from .forms import PostForm
 
def blog_list(request):
    if request.GET.get("q"):
        db = Post.objects.filter(
            Q(title__contains=request.GET.get("q"))
            | Q(contents__contains=request.GET.get("q"))
        ).distinct()
    else:
        db = Post.objects.all()
    context = {"object_list": db}
    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 == "POST":
        title = request.POST.get("title")
        contents = request.POST.get("contents")
        Post.objects.create(title=title, contents=contents)
        return redirect("blog_list")
    return render(request, "blog/blog_create.html")
2.2 Template

blog앱 안에 templates > blog 폴더를 생성해서 아래와 같은 파일을 만들어 줍니다. 빈 파일로 두어도 좋습니다.

blog > templates > blog > blog_list.html
blog > templates > blog > blog_detail.html
blog > templates > blog > blog_create.html
blog > templates > blog > blog_list.html
blog > templates > blog > blog_detail.html
blog > templates > blog > blog_create.html

명령어로 파일 생성하는 방법
터미널을 키고, 아래 명령어로 파일 생성이 가능합니다. 이러한 터미널 명령어를 익혀두시면 보다 빠르고 간편하게 파일을 생성할 수 있습니다. 또한 반복 작업의 경우에는 미리 ChatGPT에게 스니펫을 요청해 파일로 만들어두는 것을 권합니다.

  1. window인 경우
  • cmd
echo.>blog_list.html
echo.>blog_detail.html
echo.>blog_create.html
echo.>blog_list.html
echo.>blog_detail.html
echo.>blog_create.html
  • powershell
New-Item -Path . -Name "blog_list.html" -ItemType "file" -Value ""
New-Item -Path . -Name "blog_detail.html" -ItemType "file" -Value ""
New-Item -Path . -Name "blog_create.html" -ItemType "file" -Value ""
 
# 또는
 
''>blog_list.html;''>blog_detail.html;''>blog_create.html
New-Item -Path . -Name "blog_list.html" -ItemType "file" -Value ""
New-Item -Path . -Name "blog_detail.html" -ItemType "file" -Value ""
New-Item -Path . -Name "blog_create.html" -ItemType "file" -Value ""
 
# 또는
 
''>blog_list.html;''>blog_detail.html;''>blog_create.html
  1. 맥북이나 리눅스 계열 노트북, git bash인 경우
touch blog_list.html blog_detail.html blog_create.html
touch blog_list.html blog_detail.html blog_create.html
  • blog_list.html
<!-- blog/blog_list.html -->
<h1>게시판</h1>
<form action="" method="get">
    <input type="search" name="q">
    <button type="submit">검색</button>
</form>
<ul>
    {% for post in object_list %}
    <li>
        <a href="{% url 'blog_detail' post.id %}">{{ post.title }}</a>
        <p>{{ post.contents }}</p>
    </li>
    {% endfor %}
</ul>
<!-- blog/blog_list.html -->
<h1>게시판</h1>
<form action="" method="get">
    <input type="search" name="q">
    <button type="submit">검색</button>
</form>
<ul>
    {% for post in object_list %}
    <li>
        <a href="{% url 'blog_detail' post.id %}">{{ post.title }}</a>
        <p>{{ post.contents }}</p>
    </li>
    {% endfor %}
</ul>
  • blog_detail.html
<!-- blog > blog_detail.html -->
<h1>게시판</h1>
<p>{{ object.title }}</p>
<p>{{ object.contents }}</p>
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
<p>{{ object.id }}</p>
{% if object.main_image %}
<img src="{{ object.main_image.url }}" alt="">
{% endif %}
<a href="{% url 'blog_list' %}">뒤로가기</a>
<!-- blog > blog_detail.html -->
<h1>게시판</h1>
<p>{{ object.title }}</p>
<p>{{ object.contents }}</p>
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
<p>{{ object.id }}</p>
{% if object.main_image %}
<img src="{{ object.main_image.url }}" alt="">
{% endif %}
<a href="{% url 'blog_list' %}">뒤로가기</a>

create.html은 아직 내용을 채워넣지 않겠습니다.

3. 모델 및 관리자 페이지 설정

대부분 앞서 설명했던 내용이어서 부연설명을 생략하겠습니다. 모델을 정의하고, 관리자 페이지를 설정해봅시다.

3.1 모델 정의
#blog > models.py
from django.db import models
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    contents = models.TextField()
    main_image = models.ImageField(upload_to="blog/%Y/%m/%d/", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
 
    def __str__(self):
        return self.title
#blog > models.py
from django.db import models
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    contents = models.TextField()
    main_image = models.ImageField(upload_to="blog/%Y/%m/%d/", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
 
    def __str__(self):
        return self.title
3.2 관리자 페이지 설정
  1. admin.py 수정
# blog > admin.py
from django.contrib import admin
from .models import Post
 
admin.site.register(Post)
# blog > admin.py
from django.contrib import admin
from .models import Post
 
admin.site.register(Post)
  1. 관리자 계정 생성
python manage.py createsuperuser
python manage.py createsuperuser
3.3 데이터베이스 마이그레이션
python manage.py makemigrations
python manage.py migrate
python manage.py makemigrations
python manage.py migrate
3.4 실행 및 테스트
python manage.py runserver
python manage.py runserver

서버를 실행하고, 관리자 페이지에서 새 게시물 3개를 생성해주세요.

4. Form

폼(Form)은 웹 페이지에서 사용자로부터 입력을 받기 위한 HTML 요소입니다. <form> 태그는 action과 method 속성을 가지고 있습니다. 먼저 action 속성은 폼 데이터를 어느 URL로 전송할지 지정합니다. 이 속성이 비어있다면(action=""), 현재 페이지의 URL로 데이터를 전송합니다. method 속성은 데이터 전송 방식을 나타냅니다. <form>에서는 GETPOST 두 가지 방식이 사용됩니다. 다른 메서드를 사용하고 싶다면 JavaScript의 fetch를 사용해야 합니다. <form> 태그 안에서는 사용자에게 입력을 받을 <input>태그나 <textarea>태그 등을 포함할 수 있으며, 이를 전송할 때는 <button> 태그를 사용합니다.

폼을 사용하면 사용자로부터 입력받은 데이터를 서버로 전송하고, 서버에서 데이터를 처리할 수 있습니다. 이번 챕터에서는 폼을 사용하여 블로그 글을 작성하는 기능을 구현해보겠습니다.

4.1 form 추가

blog_create.html 파일을 수정하여 폼을 추가합니다.

<!-- blog_create.html -->
<form action="" method="post">
    {% csrf_token %}
    title: <input type="text" name="title"><br>
    contents: <input type="text" name="contents"><br>
    <button type="submit">저장</button>
</form>
<!-- blog_create.html -->
<form action="" method="post">
    {% csrf_token %}
    title: <input type="text" name="title"><br>
    contents: <input type="text" name="contents"><br>
    <button type="submit">저장</button>
</form>

<form>태그 안에 {% csrf_token %}가 있는 것을 확인할 수 있습니다. Django에서는 보안을 위해 csrf_token이 사용됩니다. 이는 Cross-Site Request Forgery 공격을 방지하기 위한 보안 토큰으로, {% csrf_token %} 템플릿 태그를 통해 폼에 추가됩니다. 이 태그를 통해 받은 고유 번호로 실제 이 서비스에 접속한 진짜 사람(올바른 요청)인지 확인하는 것입니다.

python manage.py runserver
python manage.py runserver

서버를 실행하고, http://127.0.0.1:8000/blog/create/로 접속하여 글을 작성해보세요. 작성한 글은 목록 페이지에서 확인할 수 있습니다.

4.2 forms.py 사용

지금은 view에서 모든 입력 데이터를 처리하고 있어, 코드가 반복되고, 복잡합니다. 원하는 데이터가 제대로 들어왔는지 유효성 검증도 여기서 해야 합니다. forms.py를 사용하면 데이터 검증과 처리를 체계적으로 관리할 수 있어, view의 코드 작성이 편해집니다. blog앱에서 forms.py 파일을 생성해줍니다.

#forms.py
from django import forms
 
class PostForm(forms.Form):
    title = forms.CharField()
    contents = forms.CharField()
#forms.py
from django import forms
 
class PostForm(forms.Form):
    title = forms.CharField()
    contents = forms.CharField()
4.3 views.py 수정

form을 새로 만들었으니, views.py를 수정해줍니다. 우선 그 전에 request로 들어온 데이터를 확인해봅시다.

#views.py
from django.shortcuts import render
from django.db.models import Q
from .models import Post
from django.shortcuts import redirect
from .forms import PostForm
 
def blog_list(request):
    if request.GET.get("q"):
        db = Post.objects.filter(
            Q(title__contains=request.GET.get("q"))
            | Q(contents__contains=request.GET.get("q"))
        ).distinct()
    else:
        db = Post.objects.all()
    context = {"object_list": db}
    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):
    print(request)
    print(request.method)
    print(request.POST)
    print(request.POST.get("title"))
    print(request.POST.get("contents"))
    # form = PostForm()
    # context = {"form": form}
    # return render(request, "blog/blog_create.html", context)
#views.py
from django.shortcuts import render
from django.db.models import Q
from .models import Post
from django.shortcuts import redirect
from .forms import PostForm
 
def blog_list(request):
    if request.GET.get("q"):
        db = Post.objects.filter(
            Q(title__contains=request.GET.get("q"))
            | Q(contents__contains=request.GET.get("q"))
        ).distinct()
    else:
        db = Post.objects.all()
    context = {"object_list": db}
    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):
    print(request)
    print(request.method)
    print(request.POST)
    print(request.POST.get("title"))
    print(request.POST.get("contents"))
    # form = PostForm()
    # context = {"form": form}
    # return render(request, "blog/blog_create.html", context)

확인을 했다면 views.py에 blog_create을 다음과 같이 수정해주세요. PostForm을 새로 임포트하고, 폼을 사용할 수 있게 blog_create를 수정합니다.

from .forms import PostForm
 
def blog_create(request):
    form = PostForm()
    context = {"form": form}
    return render(request, "blog/blog_create.html", context)
from .forms import PostForm
 
def blog_create(request):
    form = PostForm()
    context = {"form": form}
    return render(request, "blog/blog_create.html", context)
4.4 blog_create.html 수정
<!-- blog_create.html -->
<form action="{% url 'blog_create' %}" method="post">
    {% csrf_token %}
    {{ form }} 
    <button type="submit">저장</button>
</form>
<!-- blog_create.html -->
<form action="{% url 'blog_create' %}" method="post">
    {% csrf_token %}
    {{ form }} 
    <button type="submit">저장</button>
</form>

수정 전 HTML에서는 title과 contents를 직접 받아왔지만, Django의 폼 기능을 사용하면 더 간편하고 다양한 방식으로 폼을 렌더링할 수 있습니다. 아래는 기본 폼 구조와 다양한 렌더링 방식의 예시입니다.

  1. {{ form }} : 기본 렌더링
  2. {{ form.as_p }} : 각 필드를 p 태그로 감싸기
  3. {{ form.as_div }} : 각 필드를 div 태그로 감싸기
  4. <ul>{{ form.as_ul }}</ul> : 각 필드를 ul 태그로 감싸기
  5. <ol>{{ form.as_ol }}</ol> : 각 필드를 ol 태그로 감싸기
  6. <table>{{ form.as_table }}</table> : 각 필드를 테이블 행으로 표시
  7. {{ form.title }}, {{ form.contents }} : 필드 개별 렌더링

예를 들어 아래 코드처럼 테이블로 입력하면, 아래 이미지처럼 table 형식으로 렌더링 된것을 볼 수 있습니다.

<form action="{% url 'blog_create' %}" method="post">
    {% csrf_token %}
    <table>
    {{ form.as_table }} 
    </table>
    <button type="submit">저장</button>
</form>
<form action="{% url 'blog_create' %}" method="post">
    {% csrf_token %}
    <table>
    {{ form.as_table }} 
    </table>
    <button type="submit">저장</button>
</form>

forms.Form의 실습은 여기까지입니다. 다만 위에 로직으로는 저장하는 로직이 없습니다. 이를 구현하기 위해서는 아래와 같이 수정해야 합니다. 그러면 이전 코드와 크게 달라진 점이 없어보이는데요. 오히려 복잡해진 것 같기도 합니다. 템플릿에서 form을 자동으로 만들었다는 것 외에는 큰 이점이 없는 것처럼 느껴집니다.

def blog_create(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            title = form.cleaned_data['title']
            contents = form.cleaned_data['contents']
            Post.objects.create(title=title, contents=contents)
            return redirect("blog_list")
    else:
        form = PostForm()
    
    context = {"form": form}
    return render(request, "blog/blog_create.html", context)
def blog_create(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            title = form.cleaned_data['title']
            contents = form.cleaned_data['contents']
            Post.objects.create(title=title, contents=contents)
            return redirect("blog_list")
    else:
        form = PostForm()
    
    context = {"form": form}
    return render(request, "blog/blog_create.html", context)
4.5 form과 view 수정

실무에서 사용하는 것처럼 ModelForm으로 바꿔보고, views.py도 수정해봅시다.

  • forms.py
#forms.py
from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    title = forms.CharField()
    contents = forms.CharField()
 
    class Meta:
        model = Post
        fields = ["title", "contents"]
#forms.py
from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    title = forms.CharField()
    contents = forms.CharField()
 
    class Meta:
        model = Post
        fields = ["title", "contents"]

이 코드에서 PostForm 클래스는 forms.ModelForm을 상속받아 정의됩니다. ModelForm은 Django의 forms 라이브러리에 정의된 클래스로, 모델 필드를 폼 필드로 바꿔주는 등 편하게 form을 작성할 수 있게 도와줍니다. 위 코드에서 Meta 클래스도 ModelForm에 정의 되어있는 클래스로, model = Post와 같이 실제 모델과 폼을 연결하는 역할입니다. ModelForm을 상속받아 사용하면, 개발자가 모델과 폼 사이의 반복적인 작업을 줄이고 일관성 있는 코드를 작성할 수 있다는 장점이 있습니다.

ModelForm과 Form의 차이

  • forms.Form

    • 모델과 직접적인 연관이 없는 일반적인 폼을 만들 때 사용합니다.
    • 필드를 수동으로 정의해야 합니다.
    • 데이터 저장 로직을 직접 구현해야 합니다.
    • 임시 데이터나 모델과 관련 없는 데이터를 처리할 때 유용합니다.
  • forms.ModelForm

    • 특정 모델과 연관된 폼을 만들 때 사용합니다.
    • 모델의 필드를 기반으로 폼 필드를 자동으로 생성합니다.
    • 모델 인스턴스를 생성하거나 업데이트하는 메서드(save())가 내장되어 있습니다.
    • 데이터베이스와 직접 상호작용하는 폼을 만들 때 효율적입니다.

form.py에서 아래와 같이 생략 후 사용할 수 있습니다.

# forms.py
from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    # title = forms.CharField()
    # contents = forms.CharField()
 
    class Meta:
        model = Post
        fields = ["title", "contents"]
# forms.py
from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    # title = forms.CharField()
    # contents = forms.CharField()
 
    class Meta:
        model = Post
        fields = ["title", "contents"]
  • views.py
#views.py에서의 blog_create 함수 수정
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():
            # print(form)
            # print(form.cleaned_data)
            # print(form.cleaned_data.get("title"))
            # print(type(form))
            # print(dir(form))
            post = form.save()
            return redirect("blog_list")
        else:
            context = {"form": form}
            return render(request, "blog/blog_create.html", context)
#views.py에서의 blog_create 함수 수정
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():
            # print(form)
            # print(form.cleaned_data)
            # print(form.cleaned_data.get("title"))
            # print(type(form))
            # print(dir(form))
            post = form.save()
            return redirect("blog_list")
        else:
            context = {"form": form}
            return render(request, "blog/blog_create.html", context)

위 코드에서는 ModelForm의 메서드를 사용하도록 수정하였습니다. 사용자가 create URL로 들어오는 경우는 총 3가지의 경우의 수로 나눌 수 있습니다.

  1. GET 요청일 때: 폼을 렌더링하여 사용자에게 보여줍니다.
  2. POST 요청이며, 유효성 검사를 통과한 경우: 데이터를 저장하고, 목록 페이지로 리다이렉트합니다.
  3. POST 요청이며, 유효성 검사를 통과하지 못한 경우: 폼을 다시 렌더링하여 사용자에게 보여줍니다.

여기서 form.is_valid()로 유효성 검사를, form.save()로 데이터 저장을 할 수 있습니다. form.cleaned_dataform.is_valid()유효성 검사를 통과한 데이터를 말합니다. 이때 form.is_valid()을 하지 않으면 form.cleaned_data사용할 수 없습니다.

5. 에러 메세지

Django는 사용자 입력을 검사하고 오류를 처리하는 기능을 제공합니다. 이 기능은 사용자가 입력한 정보가 정확한지 확인하고, 잘못된 정보가 우리의 웹사이트 서버로 전송되는 것을 막는 역할을 합니다. 이제 이 기능을 직접 테스트해보면서 어떻게 작동하는지 알아봅시다.

# forms.py 
from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    title = forms.CharField(max_length=10)
    contents = forms.CharField(widget=forms.Textarea)
 
    class Meta:
        model = Post
        fields = ["title", "contents"]
# forms.py 
from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    title = forms.CharField(max_length=10)
    contents = forms.CharField(widget=forms.Textarea)
 
    class Meta:
        model = Post
        fields = ["title", "contents"]

우선 위의 코드처럼 title필드의 글자 수를 10자로 제한하고, contents 필드를 Textarea로 설정해줍니다. Textarea는 text보다 좀 더 많은 내용을 담을 수 있는 텍스트 입력 상자로 사용자가 크키를 조절 할 수 있습니다.

서버를 실행하고, title에 문자를 입력해보면, 위 이미지처럼 10글자만을 입력할 수 있습니다. 이는 사용자 측 유효성 검사의 한 예시입니다. 하지만 이 제한을 푸는 방법을 살펴보겠습니다.

주의!!
개발자 도구를 이용한 웹 페이지 조작은 매우 위험하고 불법적인 행위가 될 수 있습니다. 타인의 웹사이트에 무단으로 적용할 경우 심각한 법적 처벌을 받을 수 있으며, 개인정보 유출이나 시스템 손상 등의 보안 위험을 초래할 수 있습니다. 개발자 도구는 반드시 자신의 웹사이트나 명시적인 허가를 받은 경우에만 사용해야 하며, 교육 목적으로는 안전한 환경에서만 실습해야 합니다.

우선 브라우저에서 F12키를 눌러 개발자 도구를 엽니다. 개발자 도구 창이 열리면, 왼쪽 상단의 화살표 아이콘을 클릭합니다. 이 도구를 사용해 글자 제한이 있는 title 입력 필드를 클릭합니다. 그러면 해당 요소가 있는 코드가 나타나는데, 이 코드에서 maxlength="10" 속성을 찾을 수 있습니다. 이 maxlength="10" 속성을 삭제하거나 수정합니다. 수정 후, 입력 필드에 다시 텍스트를 입력해보면, 이제 10글자 이상 입력이 가능해집니다. 10글자 이상의 텍스트를 입력한 후 저장 버튼을 클릭해봅시다.

저장 버튼을 누르면, 위와 같은 문구가 출력됩니다. 10글자가 넘었다는 에러 메세지가 뜬 것을 볼 수 있습니다.

기본으로 제공되는 에러 처리 뿐만 아니라 우리가 직접 에러메세지를 조정할 수도 있습니다.

  • views.py
# blog > views.py의 blog_create 수정
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)
# blog > views.py의 blog_create 수정
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)

에러 메시지를 사용자에게 표시하기 위해, 템플릿 파일을 아래 코드로 수정합니다.

<!-- blog > templates > blog > create.html -->
<p style="color:red;">{{ error }}</p>
<form action="{% url 'blog_create'%}" method="post">
    {% csrf_token %}
    {{ form }}
    <button type="submit">저장</button>
</form>
<!-- blog > templates > blog > create.html -->
<p style="color:red;">{{ error }}</p>
<form action="{% url 'blog_create'%}" method="post">
    {% csrf_token %}
    {{ form }}
    <button type="submit">저장</button>
</form>

이렇게 에러문구가 뜨는 것을 확인할 수 있습니다.

6. 삭제하기 버튼 추가

게시물 상세페이지에서 게시글을 삭제할 수 있도록 만들어봅시다. 우선 blog_detail.html 파일에서 삭제 버튼을 만들어 줍니다.

# blog > blog_detail.html
<h1>게시판</h1>
<p>{{ object.title }}</p>
<p>{{ object.contents }}</p>
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
<p>{{ object.id }}</p>
{% if object.main_image %}
<img src="{{ object.main_image.url }}" alt="">
{% endif %}
<a href="{% url 'blog_list' %}">뒤로가기</a>
 
<form action="{% url 'blog_delete' object.id %}" method="post">
    {% csrf_token %}
    <button type="submit">삭제하기</button>
</form>
# blog > blog_detail.html
<h1>게시판</h1>
<p>{{ object.title }}</p>
<p>{{ object.contents }}</p>
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
<p>{{ object.id }}</p>
{% if object.main_image %}
<img src="{{ object.main_image.url }}" alt="">
{% endif %}
<a href="{% url 'blog_list' %}">뒤로가기</a>
 
<form action="{% url 'blog_delete' object.id %}" method="post">
    {% csrf_token %}
    <button type="submit">삭제하기</button>
</form>

삭제 버튼을 누르면 연결될 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("<int:pk>/delete/", views.blog_delete, name="blog_delete"),
]
# 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("<int:pk>/delete/", views.blog_delete, name="blog_delete"),
]

url 패턴과 연결될 새로운 뷰 함수를 추가합니다. django의 ORM기능을 이용해서, 삭제해주는 것 입니다.

  • view.py 추가
# blog > views.py
def blog_delete(request, pk):
    post = Post.objects.get(pk=pk)
    post.delete()
    return redirect("blog_list")
# blog > views.py
def blog_delete(request, pk):
    post = Post.objects.get(pk=pk)
    post.delete()
    return redirect("blog_list")

아래 이미지 처럼 삭제 버튼이 생긴 것을 확인할 수 있습니다. 버튼이 눌리면 삭제되는지 직접 서버를 실행해 확인해 봅시다.

7. 이미지 필드 추가

이미지 업로드 기능을 추가해 봅시다. 먼저 forms.py에 이미지 필드를 추가합니다.

7.1 forms.py 수정
# forms.py
from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    title = forms.CharField(max_length=100)
    contents = forms.CharField(widget=forms.Textarea)
 
    class Meta:
        model = Post
        fields = ["title", "contents", "main_image"]
        # fields = '__all__' #모든 필드를 포함
# forms.py
from django import forms
from .models import Post
 
class PostForm(forms.ModelForm):
    title = forms.CharField(max_length=100)
    contents = forms.CharField(widget=forms.Textarea)
 
    class Meta:
        model = Post
        fields = ["title", "contents", "main_image"]
        # fields = '__all__' #모든 필드를 포함

Django의 ModelForm을 사용할 때, fields 속성을 통해 폼에 포함할 필드를 지정할 수 있습니다. 여기서 fields = '__all__'를 사용하면 모델의 모든 필드를 폼에 포함시킬 수 있습니다. 하지만 사용자에게 보여서는 안 되는 필드(예: 생성 시간, 관리자 전용 필드 등)가 노출될 수 있고, 실제 서비스 환경에서 사용하기에는 보안이나 유지보수의 문제가 있기 때문에, 위의 코드처럼 명시적으로 나열하는 것을 권고합니다.

7.2 뷰 수정하기

이미지 업로드를 처리하기 위해서 views.pyblog_create 함수를 수정합니다.

# views.py
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, request.FILES)  # request.FILES 추가
        if form.is_valid():
            post = form.save()
            return redirect("blog_list")
        else:
            context = {
                "form": form,
                "error": "입력이 잘못되었습니다. 알맞은 형식으로 다시 입력해주세요!",
            }
            return render(request, "blog/blog_create.html", context)
# views.py
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, request.FILES)  # request.FILES 추가
        if form.is_valid():
            post = form.save()
            return redirect("blog_list")
        else:
            context = {
                "form": form,
                "error": "입력이 잘못되었습니다. 알맞은 형식으로 다시 입력해주세요!",
            }
            return render(request, "blog/blog_create.html", context)

request.FILES를 추가하여 파일 업로드를 처리할 수 있게 되었습니다.

7.3 템플릿 수정하기

이미지 업로드를 위해 blog_create.html 템플릿을 수정합니다.

<!-- blog_create.html -->
<p style="color:red;">{{ error }}</p>
<form action="{% url 'blog_create'%}" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form }}
    <button type="submit">저장</button>
</form>
<!-- blog_create.html -->
<p style="color:red;">{{ error }}</p>
<form action="{% url 'blog_create'%}" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form }}
    <button type="submit">저장</button>
</form>

enctype="multipart/form-data"를 추가하여 파일 업로드를 할 수 있게 합니다.

create 페이지 게시물 상세 페이지

8. 게시글 수정 기능 구현

게시글 수정 기능을 위해 새로운 URL 패턴을 추가합니다.

  • urls.py
# 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("<int:pk>/delete/", views.blog_delete, name="blog_delete"),
    path("<int:pk>/update/", views.blog_update, name="blog_update"),
]
# 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("<int:pk>/delete/", views.blog_delete, name="blog_delete"),
    path("<int:pk>/update/", views.blog_update, name="blog_update"),
]

path("<int:pk>/update/", views.blog_update, name="blog_update"), 이 코드입니다. 그에 맞게 views.py에도 blog_update 함수를 추가합니다.

  • views.py
# views.py
from django.shortcuts import get_object_or_404
 
def blog_update(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = PostForm(request.POST, request.FILES, 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)
# views.py
from django.shortcuts import get_object_or_404
 
def blog_update(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = PostForm(request.POST, request.FILES, 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)

여기서 get_object_or_404 함수는 Post 모델에서 pk=pk인 객체를 가져오는데, 만약 해당 객체가 없다면 404 에러를 발생시킵니다. instance=post를 통해 수정할 게시글을 가져옵니다. 이제 수정 페이지를 위한 blog_update.html 템플릿을 생성합니다.

  • blog_update.html
<!-- blog_update.html -->
<p style="color:red;">{{ error }}</p>
<form action="" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form }}
    <button type="submit">저장</button>
</form>
<!-- blog_update.html -->
<p style="color:red;">{{ error }}</p>
<form action="" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form }}
    <button type="submit">저장</button>
</form>

마지막으로, blog_detail.html에 수정 링크를 추가합니다.

  • blog_detail.html
<!-- blog_detail.html -->
<h1>게시판</h1>
<p>{{ object.title }}</p>
<p>{{ object.contents }}</p>
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
<p>{{ object.id }}</p>
{% if object.main_image %}
<img src="{{ object.main_image.url }}" alt="">
{% endif %}
 
<form action="{% url 'blog_delete' object.id %}" method="post">
    {% csrf_token %}
    <button type="submit">삭제하기</button>
</form>
<a href="{% url 'blog_update' object.id %}">수정하기</a>
<a href="{% url 'blog_list' %}">뒤로가기</a>
<!-- blog_detail.html -->
<h1>게시판</h1>
<p>{{ object.title }}</p>
<p>{{ object.contents }}</p>
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
<p>{{ object.id }}</p>
{% if object.main_image %}
<img src="{{ object.main_image.url }}" alt="">
{% endif %}
 
<form action="{% url 'blog_delete' object.id %}" method="post">
    {% csrf_token %}
    <button type="submit">삭제하기</button>
</form>
<a href="{% url 'blog_update' object.id %}">수정하기</a>
<a href="{% url 'blog_list' %}">뒤로가기</a>
수정 전 상세 페이지 수정페이지 수정 후 상세 페이지
4장 form과 인증4.2 인증