Back-end/Forum with Django

05. CREATE Post (+Django Form)

JUTABI 2025. 6. 16. 21:49

깃허브에서 보기:

https://github.com/jutabi/create-a-forum-with-django/blob/5f2d5ceab020d982560c5fd9d81ac6d439be0ada/docs/05-CREATE-Post.md

 

create-a-forum-with-django/docs/05-CREATE-Post.md at 5f2d5ceab020d982560c5fd9d81ac6d439be0ada · jutabi/create-a-forum-with-djan

without DRF, frontend framework. Contribute to jutabi/create-a-forum-with-django development by creating an account on GitHub.

github.com

 

1. POST 요청을 이용해보자

지금까지 진행했던 post_list와 post_detail은 'GET' HTTP Method를 이용했다.
이번엔 'POST' HTTP Method를 사용하여 사용자가 데이터를 서버에 전송해보자.

파일 작성

post_form.html

게시물을 작성할 수 있는 페이지(템플릿)을 생성한다. (/templates/forum/post_form.html)

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <!-- html form태그를 사용하고 method로 "POST"를 선택한다. -->
    <!-- form 태그의 요청 주소(action)을 이전 템플릿들 처럼 url 태그를 사용해 forum:post_create로 설정한다. -->
    <form method="POST" action="{% url 'forum:post_create' %}">
        <!-- Django 템플릿의 보안 기본기능이다. POST method form 요청은 csrf_token을 가지고 있어야 한다. -->
        {% csrf_token %}

        <div class="title">
        <label>Title
            <!-- 서버로 전송할 데이터 입력 필드를 생성한다. name=""속성의 값이 요청받은 서버에서 request.***로 접근할 수 있는 값이 된다. -->
             <!-- 나중에는 서버에서도 처리하겠지만 일단 값을 비워놓고 제출하지 못하도록 required 속성을 추가한다. -->
            <input type="text" name="title" placeholder="title" required>
        </label>
        </div>

        <div class="content">
        <label>Content
            <textarea name="content" placeHolder="content" required></textarea>
        </label>
        </div>
        <!-- submit input, button 태그를 통해 폼 태그의 action 주소에 요청을 한다. -->
        <input type="submit" value="제출">
    </form>
</body>
</html>
CSRF

CSRF(Cross-Site Request Forgery 사이트간 요청 위조)는 해커가 사용자의 권한을 도용하여 의도하지 않은 요청을 보내는 공격 기법.

 

우리의 웹사이트를 예시로 들어보면. 해커는 제목, 내용 필드를 사용자가 의도하지 않은 내용으로 채워 넣고 그 요청 주소를 사용자가 클릭하도록 유도합니다.


사용자는 우리의 웹사이트에 로그인된 상태에서 (새탭 열기 포함(세션에 로그인 정보가 있다면 전부))그 버튼, 이미지, 링크를 클릭할 시 해커가 설정한 주소로 요청을 보내게 되고 담겨있는 내용이 게시판에 작성됩니다. 이는 사용자가 직접 요청한 것과 같기 때문입니다.

 

다른 예시를 들어보면 우리의 비밀번호 변경 url이 /member/password_change/라고 하면 해커는 폼 태그에 원하는 비밀번호를 미리 입력해두고 action또한 /member/password_change/로 바꾼 뒤 접속시 요청을 보내는 사이트를 만들어 놓습니다.


그리고 사용자가 이 사이트에 접속하면 로그인 된 사용자가 자신의 비밀번호를 변경하는 요청을 서버에 보냈으니 서버는 비밀번호를 변경하고 이것을 통해 해커는 사용자의 계정 비밀번호를 자신이 원하는 값으로 변경시킬 수 있습니다.

 

이를 막기 위해서 csrf 토큰을 사용하는 데 사용자가 form태그가 있는 url에 접속하면 사용자의 쿠키와 form에 csrf 토큰을 발행합니다. 그리고 서버는 어떠한 POST 요청이 온다면 그 토큰 값이 서로 일치하는지 확인한 후 요청에 대한 처리를 합니다.
이 값은 html의 hidden 값으로 설정되어 있습니다.


둘의 value가 다른 이유는 <input>csrf 태그는 값이 마스킹 처리되어 있기 때문입니다.
쿠키의 csrftoken은 서버가 생성한 원본 토큰입니다. 브라우저에 저장되어 클라이언트와 서버가 값을 공유합니다.
폼의(input)csrfmiddlewaretoken은 원본 토큰을 서버가 마스킹 작업을 하여 변형한 값입니다.
이 프로젝트에서 나중에는 이 input태그의 값을 페이지 내에서 활용할 예정인데 그렇다면 악의적인 스크립트 또한 이 값을 이용할 수 있습니다. 그렇기에 서버는 마스킹한 값을 input태그에 저장하고 폼이 제출될 때 이 값을 원래의 값으로 복원하여 쿠키의 csrftoken과 비교합니다.

forum/urls.py
urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('<int:pk>/', views.post_detail, name='post_detail'),
    # posts/new/ 요청을 post 생성 view 함수로 지정한다.
    path('new/', views.post_create, name='post_create'),
]
forum/views.py
from django.shortcuts import render, get_object_or_404, redirect
from forum.models import *

def post_list(request):
    ...

def post_detail(request, pk):
    ...

def post_create(request):
    # GET요청과 POST요청으로 분기가 이루어진다.
    # GET요청시 '/posts/new/'의 게시물 작성 페이지(템플릿)를 사용자에게 렌더해주기 위함이고
    # POST요청시 'posts/new/'페이지의 <form>태그에서 POST요청이 들어왔을 때 사용자가 입력한 데이터를 저장한다.

    # 만약 어떤 페이지(ex. post_list에서 글 작성 버튼을 눌렀다면)
    # 서버에 POST요청으로 어떠한 데이터를 전달한 것이 아닌 단순한 url 이동(/posts/new/ GET 요청)이기 때문에 
    # 신규 post를 작성할 수 있는 템플릿을 사용자에게 렌더해준다.
    if request.method == 'GET':
        return render(request, 'forum/post_form.html')
    # 여기서는 이미 사용자가 '/posts/new/'페이지에 접속한 상태에서 html의 <form>태그를 통해 입력데이터와 
    # 같이 보낸 POST 요청을 처리한다.
    elif request.method == 'POST':
        # Post 객체를 생성하는 방법은 이미 Django shell을 이용할 떄 배웠다.
        post = Post(
            # POST 요청의 ['title']접근으로 폼 태그의 <input name='title'>값에 접근할 수 있다.
            title=request.POST['title'],
            content=request.POST['content'],
            # created_date는 모델을 선언할 때 자동으로 모델이 생성될 떄 삽입될 수 있도록 해두었다.
        )
        # 아직 데이터베이스에 저장된 것이 아니다. 생성한 post객체를 .save()를 통해 데이터베이스에 저장한다.
        post.save()
        # redirect 메서드는 템플릿 태그때 배웠던 url 태그를 사용할 수 있다.
        # 게시물 생성이 완료되었다면 사용자를 본인이 작성한 게시물의 상세 페이지로 이동시킨다.
        return redirect('forum:post_detail', pk=post.pk)
post_list
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h2>글 목록</h2>
    <hr>
    {% if posts %}
        {% for post in posts %}
            <li>
            <a href="{% url 'forum:post_detail' pk=post.pk %}">{{ post.title }}</a>
            </li>
        {% endfor %}
    {% endif %}
    <hr>
    <!-- 메인 페이지 (post_list)에 글 작성 링크를 삽입한다. -->
    <a href="{% url 'forum:post_create' %}">글 작성</a>
</body>
</html>

결과 확인

[16/Jun/2025 14:25:17] "GET /posts/new/ HTTP/1.1" 200 677
[16/Jun/2025 14:25:29] "POST /posts/new/ HTTP/1.1" 302 0
[16/Jun/2025 14:25:29] "GET /posts/8/ HTTP/1.1" 200 229

서버 메세지와 화면에서 정상적으로 데이터가 삽입된 것을 볼 수 있다.

+form action

우리가 작성한 form 태그에는 action속성을 통해 어느 주소로 POST요청을 보낼지를 명시했다. 하지만 생략이 가능하다.

<form method="POST">
        {% csrf_token %}
        ...
        <input type="submit" value="제출">
    </form>

위의 코드처럼 action 속성을 삭제해도 정상적으로 게시물이 작성되는 것을 확인할 수 있는데 그 이유는 form태그는 현재 접속한 url에 요청을 보내기 때문이다.
GET 요청을 통해 /posts/new/에 들어왔고 그에따라 form은 /posts/new/에 POST 요청을 보낸다.
게시물 수정기능을 구현할 때 템플릿을 재사용하기 위해 생략한 채로 사용.


2. Django Form

위의 실습에서는 post = Post() 객체를 선언하여 데이터를 저장하는 법을 배웠다.
다만 이 방법은 객체의 속성값이 많거나 오류 처리(Null값 전송, 특수문자, 쿼리문 입력 등등)가 있을 떄는 사용하기 불편해진다.
그래서 Django에서는 'Form'기능을 제공한다.

Django Form 생성

forum/forms.py 파일을 생성하고 아래의 내용을 작성하자.

from django import forms
from forum.models import *

# 명명 규칙은 사용할모델Form의 파스칼케이스로 작성한다.
class PostForm(forms.ModelForm):
    # 필수 클래스. 사용할 모델과 해당 모델의 속성을 정의
    class Meta:
        model = Post
        fields = ['title', 'content']

forum/views.py 수정

from django.shortcuts import render, get_object_or_404, redirect

# PostForm 불러오기
from forum.forms import PostForm
from forum.models import *

...

def post_create(request):
    if request.method == 'GET':
        return render(request, 'forum/post_form.html')
    elif request.method == 'POST':
        # post = Post(
        #     title=request.POST['title'],
        #     content=request.POST['content'],
        # )
        # post.save()

        # 이전의 코드는 request.POST['title'], ['content']처럼 각자 저장해야 했다면
        # Django Form은 선언했던 fields = ['title', 'content']의 이름에 자동으로 매칭하여 객체를 생성해준다.
        form = PostForm(request.POST)
        # is_valid()는 폼 객체 내의 값이 오류가 있을 경우 그 오류를 form 객체에 삽입해 준다. (유효성 검사)
        if form.is_valid():
            # 만약 유효한 값이 전달되었다면:
            # 추가 작업이 필요한 경우 commit=False 폼에 데이터를 임시 저장할 수 있다. 
                # (post = form.save(commit=False), ..., post.save())
            post = form.save()
            # 저장이 완료되었음으로 사용자를 해당 게시글 페이지로 이동시킨다.
            return redirect('forum:post_detail', pk=post.pk)
        # 만약 폼의 값에 문제가 있다면 위의 save와 redirect가 실행되지 않고
        # 해당 폼에 오류 메세지를 담은 채 사용자자에게 GET 요청때 처럼 post_form.html 을 다시 렌더해준다.
        return render(request, 'forum/post_form.html', {'form': form})

post_form 수정

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form method="POST">
        {% csrf_token %}
        <!-- context 로 전달받은 form 객체에 오류가 포함되어 있다면: -->
        {% if form.errors %}
            <div class="error-message">
            <!-- form의 필드 각각['title', 'content']에 접근하여 -->
            {% for field in form %}
            <!-- 그 필드에 에러가 있다면 -->
            {% if field.errors %}
                <!-- 라벨(Django Form 에서 설정가능. 기본적으는 필드 이름 파스칼케이스)과 에러 메세지를 화면에 출력한다. -->
                <strong>{{ field.label }}</strong>
                {{ field.errors }}
            {% endif %}
            {% endfor %}
            </div>
        {% endif %}
        <div class="title">
        <label>Title
            <!-- 유효한 값이 전달되지 않아 사용자가 다시 입력해야 하는 경우 기존의 입력 데이터를 사용자에게 다시 전달하기 위하여 value값을 설정합니다. -->
            <input type="text" name="title" required value="{{ form.title.value }}">
        </label>
        </div>
        <div class="content">
        <label>Content
            <textarea name="content" required>{{ form.content.value }}</textarea>
        </label>
        </div>
        <input type="submit" value="제출">
    </form>
</body>
</html>

결과 확인

개발자 도구에 들어가 input, textarea의 required 속성을 지우고 제출버튼을 눌러보자.

오류메세지가 정상적으로 출력된다.

값을 입력한 뒤 제출하여도 정상적으로 게시물 작성이 완료된다.