본문 바로가기
Research/Django

[Django] 점프 투 장고 튜토리얼 - 02-10. 폼

by RIEM 2021. 11. 15.

Django 점프 투 장고 정리

작성일 : 2021-11-11

문서버전 : 1.0

 

개요

이 문서는 점프 투 장고 사이트의 장고 튜토리얼 학습 내용을 정리한 내용입니다.

레퍼런스

점프 투 장고 https://wikidocs.net/72242

 

2-10. 폼

질문 등록

질문을 등록하기 전에 ‘질문 등록하기’ 버튼이 우선 만들어야 한다. 아래 버튼을 생성해주자ㅏ.

> ../projects/mysite/templates/pybo/question_list.html

(... 생략 ...)
    </table>
    <a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>
</div>
{% endblock %}

 

  <a href=”...”> 링크로 부트스트랩의 btn btn-primary 클래스를 적용했다. 버튼을 누르면 별칭 ‘pybo:question_create’인 URL이 호출된다.

 

URL 매핑

Pybo:question_create 별칭에 해당되는 URL매핑 추가하자.

> ../projects/mysite/pybo/urls.py

(... 생략 ...)
urlpatterns = [
    (... 생략 ...)
    path('question/create/', views.question_create, name='question_create'),
]

 

Views.question_create 함수를 호출하도록 매핑 완료!



폼(Form)

View 함수를 호출 하도록 설계했으나 question_create함수가 아직 없다. 그렇기 떄문에 views.question_create 함수를 작성해야 한다. 함수를 작성하기 전에 Form에 대해 좀 더 알아보는 시간을 가져보자.

 

폼(Form)이란?

폼은 클래스다. 페이지 요청 시 전달되는 파라미터들에 대한 관리를 쉽게하기 위해 사용한다. 폼의 사용 목적은 파라미나 값 누락 여부, 파라미터 형식의 적절성 등을 검증하기 위한 목적이다. 그 외 HTML을 자동으로 생성하거나 폼에 연결된 모델을 이용해 데이터를 저장할 수 있다.

 

아래와 같이 파일을 생성해주자.

> ../projects/mysite/pybo/forms.py

from django import forms
from pybo.models import Question


class QuestionForm(forms.ModelForm):
    class Meta:
        model = Question  # 사용할 모델
        fields = ['subject', 'content'# QuestionForm에서 사용할 Question 모델의 속성

 

QuestionForm은 ‘form.ModelForm’을 상속했다. 장고의 폼은 2가지로 나뉘는데 1) 일반 폼인 forms.Form 과 2) 모델 폼인 forms.ModelForm 이다. 그중 2)모델 폼의 경우, 모델(Model)과 연결되어서 폼을 저장할 때 연결된 모델의 데이터를 저장하는 폼이다. 모델 폼은 이너 클래스인 Meta 클래스가 필수다. Meta 클래스에서는 위와 같이 사용할 모델(Question)과 모델의 속성(subject, content)을 기재한다. 

 

뷰(View) 함수

다시 돌아와 views.question_create 함수 작성 단계를 진행해보자.

> ../projects/mysite/pybo/views.py

from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
from .models import Question
from .forms import QuestionForm

(... 생략 ...)

def question_create(request):
    """
    pybo 질문등록
    """
    form = QuestionForm()
    return render(request, 'pybo/question_form.html', {'form': form})

 

Question_create 함수는 위에서 작성한 QuestionForm을 사용했다. Render 함수에 전달한 {‘form’: form}은 템플릿에서 질문 등록 시 사용할 폼 엘리먼트를 생성할 때 쓰인다.

템플릿

아래와 같이 템플릿을 작성해보자.

> ../projects/mysite/templates/pybo/question_form.html

{% extends 'base.html' %}

{% block content %}
<div class="container">
    <h5 class="my-3 border-bottom pb-2">질문등록</h5>
    <form method="post" class="post-form my-3">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="btn btn-primary">저장하기</button>
    </form>
</div>
{% endblock %}

 

템플릿에서 사용한 {{form.as_p}}의 form은 question_create 함수에서 전달한 QuestionForm의 객체이다. {{ form.as_p }}는 폼에 정의한 subject, content 속성에 해당하는 HTML 코드를 자동으로 생성한다.

 

일반적으로 form 태그에는 action 속성을 지정하여 submit 실행 시 action에 정의된 url로 폼을 전송한다. 하지만 <form method=”post” class=”post-form my-3”>의 경우 form 태그에 action속성을 지정하지 않았다. Form 태그에 action 속성 지정하지 않으면 현재 페이지의 URL이 디폴트 action으로 설정된다. 명확하게 지정한다면 아래와 같이 지정된다.

 

<form method="post" class="post-form my-3" action="{% url 'pybo:question_create' %}">

Form의 속성 비워두는 이유

이렇게 할 경우 question_form 템플릿은 ‘질문 등록’에서만 사용 가능하다. 추후 ‘질문 수정’을 하고싶다면 action 값을 달리 해야 한다. 따라서 동일한 템플릿을 여러 기능이 함께 사용하는 경우 form의 action 속성을 비워둔다.

GET과 POST

자, 로컬서버를 재구동해서 화면을 확인해보자.

질문 등록 페이지가 생성되었다. 질문 등록하기 페이지를 들어가면 아래처럼 나온다.

아직 question_create 함수에 데이터 저장하는 코드를 작성하지 않았기 때문에 질문 저장은 되지 않는다. 해당 코드를 작성해보자.


> ../projects/mysite/pybo/views.py

def question_create(request):
    """
    pybo 질문등록
    """
    if request.method == 'POST':
        form = QuestionForm(request.POST)
        if form.is_valid():
            question = form.save(commit=False)
            question.create_date = timezone.now()
            question.save()
            return redirect('pybo:index')
    else:
        form = QuestionForm()
    context = {'form': form}
    return render(request, 'pybo/question_form.html', context)

 

URL요청을 POST 또는 GET 요청 방식에 따라 다르게 처리한다. 

질문 목록 화면에서 “질문 등록하기” 버튼을 클릭한 경우 /pybo/question/create/ 페이지가 GET방식으로 요청되어 question_create 함수가 실행된다. 왜냐하면 <a href=”{% url ‘pybo:question_create’ %}” class=”btn btn-primary”>질문 등록하기</a>과 같이 링크 통해 페이지 요청할 경우 무조건 GET 방식이 사용되기 때문이다. 따라서 이 경우 request.method 값이 GET되어 if..else..에서 else 구문을 타게되어 결국 질문 등록 화면을 보여 줄 것이다.

 

그리고 질문 등록 화면에서 subject, content 항목에 값을 기입하고 ‘저장하기’ 버튼 누르면 동일한 /pybo/question/create/ 페이지가 POST방식으로 요청된다. 왜냐하면 form 태그에 action 속성이 지정되지 않으면 현재 페이지가 디폴트 action으로 설정되기 때문이다. 따라서 질문 등록 화면에서 “저장하기” 버튼 클릭하면 question_create 함수가 실행되고 request.method 값은 POST가 되어 아래와 같은 코드가 실행될 것이다.

 

    if request.method == 'POST':
        form = QuestionForm(request.POST)
        if form.is_valid():
            question = form.save(commit=False)
            question.create_date = timezone.now()
            question.save()
            return redirect('pybo:index')

 

GET 방식에서는 form = QuestionForm()처럼 QuestionForm을 인수 없이 생성했다. 

하지만! POST 방식에서는 form = QuestionForm(request.POST) 처럼 request.POST를 인수로 생성했다. request.POST를 인수로 QuestionForm 생성할 경우 request.POST에 담긴 subject, content 값이 QuestionForm의 subject, content 속성에 자동으로 저장되어 객체가 생성된다. 참고로 request.POST에는 화면에서 사용자가 입력한 내용들이 담겨있다.

 

form.is_valid()는 form이 유효한지 검사한다. 만약 form에 저장된 subject, content 값이 올바르지 않다면 form에 오류 메시지가 저장되고, form.is_valid()가 실패해 다시 질문 등록 화면으로 돌아간다. Form에 저장된 오류 메시지는 질문 등록 화면에 표시된다.

 

Form이 유효하다면 if form.is_valid(): 이후 문장이 수행되어 질문 데이터가 생긴다. 

  • Question = form.save(commit=False) : form으로 Question데이터를 저장하기 위한 코드다. QuestionForm이 Question 모델과 연결된 모델 폼이기 때문에 이와 같이 사용한다. 여기서 ‘commit=False’는 임시 저장을 의미하는데, 실제 데이터는 DB에 저장되지 않은 상태를 말한다. 만약 ‘form.save(commit=False)’ 대신 ‘form.save()’를 수행하면 Question 모델의 create_date에 값이 없다는 오류가 발생할 것이다. 왜냐하면 QuestionForm에는 현재 subject, content 속성만 정의되어 있고 create_date 속성은 없기 때문이다. 이러한 이유로 임시 저장을 한 후 question 객체를 리턴받아 create_date에 값을 설정한 후 question.save()로 실제 저장하는 것이다. 
  • Create_date 속성은 데이터 저장 시점에 자동 생성해야 하는 값으로 QuestionForm에 등록하여 사용하지 않는다.
  • 마지막엔 Return redirect(‘pybo:index’) 호출하여 질문 목록 화면으로 이동한다.



이제 질문 등록이 정상적으로 진행되는지 확인해보자.

정상적으로 질문이 등록된 것을 확인했다!

 

폼 위젯(Form Widget)

문제가 생겼다. 우리는 부트스트랩이 있지만 {{form.as_p}} 태그는 HTML 코드 자동 생성하기 때문에 부트스트랩 적용이 어렵다. 아래와 같이 Question Form을 수정해보자.

> ../projects/mysite/pybo/forms.py

class QuestionForm(forms.ModelForm):
    class Meta:
        model = Question
        fields = ['subject', 'content']
        widgets = {
            'subject': forms.TextInput(attrs={'class': 'form-control'}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
        }

 

Meta 클래스에 Widgets 속성을 지정하면 입력 필드에 form-control과 같은 부트스트랩 클래스를 추가할 수 있다. 자, 확인해보자.

 

이전 화면

 

현재 화면은 부트스트랩이 적용되어 더욱 깔끔해진 화면을 볼 수 있다. 

 

폼 레이블(Form Label)

질문 등록 화면에 표시되는 ‘Subject’, ‘Content’를 영문이 아닌 한글로 바꾸기 위해선 labels 속성을 지정하면 된다.

> ../projects/mysite/pybo/forms.py

from django import forms
from pybo.models import Question


class QuestionForm(forms.ModelForm):
    class Meta:
        model = Question
        fields = ['subject', 'content']
        widgets = {
            'subject': forms.TextInput(attrs={'class': 'form-control'}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
        }
        labels = {
            'subject': '제목',
            'content': '내용',
        }  

 

장고 form에 대해 더 알고싶다면 공식 문서를 참고하면 좋다.

 

수동 폼 작성

{{ form.as_p }} 를 사용하면 빠르게 템플릿 만들 수 있지만 HTML 코드가 자동으로 생성되어 디자인에 많은 제한이 생긴다. 예를 들어 특정 태그를 추가하거나 필요한 클래스를 추가하는 작업에 제한이 생긴다. 또 디자인 영역과 서버 프로그램 영역이 혼재되기도 한다. 

 

폼을 이용해 자동으로 HTML 코드를 생성하지 않고 직접 HTML 코드를 작성하는 방법은 없을까?

 

우선 아래와 같이 진행해보자.

> ../projects/mysite/pybo/forms.py

수작업에 불 필요한 forms.py 파일의 widget 항목 제거

from django import forms
from pybo.models import Question


class QuestionForm(forms.ModelForm):
    class Meta:
        model = Question  # 사용할 모델
        fields = ['subject', 'content'# QuestionForm에서 사용할 Question 모델의 속성
        widgets = {
            'subject': forms.TextInput(attrs={'class': 'form-control'}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
        }

        labels = {
            'subject': '제목',
            'content': '내용',
        }


> ../projects/mysite/templates/pybo/question_form.html

{% extends 'base.html' %}

{% block content %}
<div class="container">
    <h5 class="my-3 border-bottom pb-2">질문등록</h5>
    <form method="post" class="post-form my-3">
        {% csrf_token %}
        <!-- 오류표시 Start -->
        {% if form.errors %}
            <div class="alert alert-danger" role="alert">
            {% for field in form %}
                {% if field.errors %}
                <strong>{{ field.label }}</strong>
                {{ field.errors }}
                {% endif %}
            {% endfor %}
            </div>
        {% endif %}
        <!-- 오류표시 End -->
        <div class="form-group">
            <label for="subject">제목</label>
            <input type="text" class="form-control" name="subject" id="subject"
                  value="{{ form.subject.value|default_if_none:'' }}">
        </div>
        <div class="form-group">
            <label for="content">내용</label>
            <textarea class="form-control" name="content"
                      id="content" rows="10">{{ form.content.value|default_if_none:'' }}</textarea>
        </div>
        <button type="submit" class="btn btn-primary">저장하기</button>
    </form>
</div>
{% endblock %}

 

{{ form.as_p }}로 자동으로 생성되는 HTML 대신 제목과 내용에 해당되는 HTML 코드를 작성했다. 그리고 question_create함수에서 form.is_valid()가 실패할 경우 발생하는 오류의 내용을 표시하기 위해 오류 표시하는 영역을 추가했다. 

 

제목(subject) 항목의 value에는 {{ form.subject.value|default_if_none:’’ }}처럼 값을 대입해주었다. 이는 오류 발생 시 기존 입력값을 유지하는 기능이다. 뒤의 default_if_none=’’의 의미는 폼데이터(form.subject.value)에 값이 없을 경우 None이라는 문자열이 표시되는데 None 대신 공백으로 바꿔 표시하라는 템플릿 필터다. 

 

제목을 누락하고 게시글을 저장하기 버튼을 누르면 아래와 같은 화면이 뜬다.

놀랍다. 제목 누락 사실을 알고리즘이 바로 알아차려 오류메시지를 던졌다. 

 

답변 등록

질문 등록에 장고 폼을 적용한 것 처럼 답변 등록에도 장고 폼을 적용하자. 답변을 등록할 때 사용할 AnswerForm을 작성하자.

> ../projects/mysite/pybo/forms.py

from django import forms
from pybo.models import Question, Answer

(... 생략 ...)

class AnswerForm(forms.ModelForm):
    class Meta:
        model = Answer
        fields = ['content']
        labels = {
            'content': '답변내용',
        }


> ../projects/mysite/pybo/views.py

Answer_create 함수를 다음과 같이 수정하자

(... 생략 ...)
from .forms import QuestionForm, AnswerForm
(... 생략 ...)

def answer_create(request, question_id):
    """
    pybo 답변등록
    """
    question = get_object_or_404(Question, pk=question_id)
    if request.method == "POST":
        form = AnswerForm(request.POST)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.create_date = timezone.now()
            answer.question = question
            answer.save()
            return redirect('pybo:detail', question_id=question.id)
    else:
        form = AnswerForm()
    context = {'question': question, 'form': form}
    return render(request, 'pybo/question_detail.html', context)  

 

Question_create와 같은 방식으로 Answer_create를 작성했다. 사실 답변 등록의 경우 POST 방식만 사용되어서 else구문이 호출될 일은 없으나 패턴의 통일성을 위해 남겨둔다.


> ../projects/mysite/templates/pybo/question_detail.html

질문 상세 템플릿 또한 오류 표시를 위해 다음 영역을 추가해주자.

{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
    (... 생략 ...)
    <form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
        {% csrf_token %}
        {% if form.errors %}
        <div class="alert alert-danger" role="alert">
        {% for field in form %}
            {% if field.errors %}
            <strong>{{ field.label }}</strong>
            {{ field.errors }}
            {% endif %}
        {% endfor %}
        </div>
        {% endif %}
        <div class="form-group">
            <textarea name="content" id="content" class="form-control" rows="10"></textarea>
        </div>
        <input type="submit" value="답변등록" class="btn btn-primary">
    </form>
</div>
{% endblock %}



자, 이제 답변내용을 추가하지 않으면 아래와 같이 오류 메시지 창이 뜬다.

 

댓글