[Django] Django Form

작성:    

업데이트:

카테고리:

태그: ,

Form

  • 사용자의 데이터를 직접 받을 때 입력된 데이터의 유효성을 검증해야 한다.
  • 필요시에 입력된 데이터를 검증 결과와 함께 다시 표시 해야한다.
  • 사용자가 입력한 데이터는 개발자가 요구한 형식이 아닐 수 있음을 고려

  • 유효성 검증 : 사용자가 입력한 데이터를 검증
  • 유효성 검증을 모두 구현하는 것은 많은 노력이 필요
  • Django From : Django가 과중한 반복 코드를 줄여줌으로써 유효성 검증을 보다 쉽게!


Django Form

  • Django의 유효성 검사 도구 중 하나
  • 외부의 악의적 공격 및 데이터 손상에 대한 중요한 방어 수단

  • Form과 관련된 유효성 검사를 단순화/자동화 기능 제공
  • 개발자가 직접 작성하는 코드보다 더 안전하고 빠르게 수행하는 코드 작성 지원


Django의 form에 관련된 3가지 작업

  1. 렌더링을 위한 데이터 준비 및 재구성
  2. 데이터에 대한 HTML forms 생성
  3. 클라이언트로부터 받은 데이터 수신 및 처리


Django Form Class

  • Django Form 관리 시스템의 핵심

  • Form 내 field, field 배치, 디스플레이 widget, label, 초기값, 유효하지 않은 field에 관련된 에러 메세지 결정
  • 사용자의 데이터를 받을 때 해야 하는 과중한 작업과 반복 코드 줄여줌
    • 데이터 유효성 검증
    • 필요시 입력된 데이터 검증 결과 재출력 등


Django Form 사용

Form 선언

# articles/forms.py
from django import forms


class ArticleForm(forms.Form):
    title = forms.CharField(max_length=10)
    content = forms.CharField()              # Model과 달리 TextField가 아니다!! 자동완성에도 없다.
  • App 폴더는 forms.py가 없으므로 새로 만들어준다.
  • Model을 선언하는 것과 유사, 같은 필드타입 사용 (일부 매개변수도 유사)
  • forms 라이브러리에서 파생된 Form 클래스를 상속


Form 사용

  • views.py에 ArticleForm을 import한다.
# articles/views.py
from .forms import ArticleForm


# articles/views.py
def new(request):
    return render(request, 'articles/new.html')


# articles/views.py
def new(request):
    form = ArticleForm()
    context = {
        'form': form
    }
    return render(request, 'articles/new.html', context)


<!-- articles/templates/articles/new.html -->

<form action="{% url 'articles:create' %}" method="POST">
    {% csrf_token %}
    <label for="title">Title: </label>
    <input type="text" id="title" name="title"><br>
    <label for="content">Content: </label>
    <textarea name="content" id="content" cols="30" rows="10"></textarea>
    <input type="submit">
  </form>
  • new template의 기존 form에 대한 html 태그이다.
  • 위의 코드를 아래처럼 바꿔보자.


<!-- articles/templates/articles/new.html -->

<form action="{% url 'articles:create' %}" method="POST">
    {% csrf_token %}
    {{ form }}
    <input type="submit">
  </form>

image


Form rendering options

  • label과 input 쌍에 대한 3가지 출력 옵션


as_p()

  • 각 필드가 단락(p 태그)으로 감사져서 렌더링
  • 줄바꿈이 되는 것!


as_ul()

  • 각 필드가 목록 항목(li 태그)으로 감싸져서 렌더링됨
  • ul 태그는 직접 작성


as_table()

  • 각 필드가 테이블 행(tr 태그) 행으로 감싸져서 렌더링
  • table 태그는 직접 작성


<!-- articles/templates/articles/new.html -->

<form action="{% url 'articles:create' %}" method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit">
  </form>


image


Widgets

form fields

  • input에 대한 유효성 검사 로직을 처리
  • 템플릿에서 직접 사용


Widgets

  • 웹 페이지의 HTML input element 렌더링
  • GET/POST 딕셔너리에서 데이터 추출

  • idget은 단독적으로 사용되지 못한다 : field에 의존적
# articles/forms.py
from django import forms

class ArticleForm(forms.Form):
    title = forms.CharField(max_length=10)
    content = forms.CharField(widget=forms.Textarea)

form fields의 괄호 내에 widget= option으로 부여


주의사항

  • form fields와 혼동되어서는 안된다!
    • form fields : input 유효성 검사를 처리
    • widgets : 웹페이지에서 input element의 단순 raw한 렌더링 처리
  • 참고자료 : django 공식문서 - Widgets


Widgets 응용

드롭다운 select element를 만들어보자

# articles/forms.py
from django import forms

class ArticleForm(forms.Form):
    title = forms.CharField(max_length=10)
    content = forms.CharField(widget=forms.Textarea)
    region = forms.ChoiceField(widget=forms.Select())
  • 위처럼 region parameter에 ChoiceField 설정
  • widget은 forms.Select (forms.Select()도 가능)
  • field는 자료형, widget은 user들에게 보이는 형식

image

  • 아무 option이 없기 때문에 rendering하면 아래와 같은 모습


  • select의 선택항목인 option을 추가해보자
# articles/forms.py
from django import forms

class ArticleForm(forms.Form):
    REGION_A = 'sl'
    REGION_B = 'dj'
    REGION_C = 'gj'
    REGION_CHOICES = [
        (REGION_A, '서울'),
        (REGION_B, '대전'),
        (REGION_C, '광주'),
    ]
    
    
    title = forms.CharField(max_length=10)
    content = forms.CharField(widget=forms.Textarea)
    region = forms.ChoiceField(choices=REGION_CHOICES, widget=forms.Select())
    # forms.Select도 되고 forms.Select()도 됨... 왜지?
  • ChoiceField의 경우 widget이 Select가 기본이기 때문에 괄호 내 widget 항목은 생략 가능
  • option들은 위와 같이 tuple로 작성
  • tuple 사용은 django coding style - model style 참고


결과

image

  • 드롭다운에 필요한 option들이 select element 내부에 있는 것처럼 들어갔다.


ModelForm

  • forms.py에서 Form에 대한 항목들을 하나하나 작성해주었다.
  • Django Form을 사용하다보면 Model에 정의한 필드를 유저로부터 입력받기 위해 Form에서 Model 필드를 재정의하는 행위가 중복될 수 있다.
  • Django는 Model을 통해 Form Class를 만들 수 있는 ModelForm이라는 Helper 제공


ModelForm Class

  • Model을 통해 Form Class를 만들 수 있는 Helper
  • 일반 Form Class와 완전히 같은 방식으로 view에서 사용 가능


# articles/forms.py
from django import forms
from .models import Article         # 사용할 모델을 import

class ArticleForm(forms.ModelForm): # ⭐ form.Form과 forms.ModelForm이 다름에 주의할 것
    
    class Meta:
        model = Article             # 어떤 모델을 사용할 것인지 지정
        fields = '__all__'          # 모델의 어떤 field들을 사용할 것인지 지정
        # exclude = ('title',)      # 출력에서 제외하는 것. tuple과 list 모두 가능
  • 정의한 class 내에 Meta 클래스 선언
  • 어떤 model을 기반으로 form을 작성할 것인지에 대한 정보 지정
  • widgets을 별도 설정하지 않아도 field의 type을 model에 의거해 지정


exclude?

  • 모든 field 중에 제외할 field를 명시해서 제외
  • field가 많고, 제외할 field가 적은 경우 사용
  • class 변수 field와 exclude는 동시에 사용 불가


Meta Class

  • Model에 대한 정보를 작성


Meta 데이터

  • 데이터에 대한 데이터


Inner Class

  • 클래스 내에 선언된 다른 클래스
  • 관련 클래스를 함께 그룹화 → 가독성 및 프로그램 유지 관리 지원(논리적으로 묶어 표현)
  • 외부에서 내부 클래스에 접근 불가 → 코드 복잡성 감소


유효성 검사

  • 요청한 데이터가 특정 조건에 충족하는지 확인하는 작업
  • 데이터베이스 각 필드 조건에 올바르지 않은 데이터가 서버로 전송/저장 되지 않도록 하는 것


is_valid() method

  • 유효성 검사를 실행하고, 데이터가 유효한지 여부를 boolean으로 반환
  • 데이터 유효성 검사를 보장하기 위한 많은 테스트에 대해 Django는 is_valid()를 제공


save() method

  • 주의 : 기존 model의 save method와 다르다!! ModelForm 고유의 save method
  • 그냥 form은 save method가 없다!
  • Form에 바인딩된 데이터에서 DB 객체를 만들고 저장
  • ModelForm의 하위클래스(subClass)는 기존 모델 인스턴스를 키워드 인자 instance로 수용 가능
    • 이것이 제공되면 save()는 해당 인스턴스를 수정(UPDATE)
    • 제공되지 않으면 save()는 지정된 모델의 새 인스턴스를 만듦(CREATE)
  • Form의 유효성이 확인되지 않은 경우, save()를 호출하면 form.errors를 확인하여 에러 목록 확인 가능


예시 코드

# POST data로부터 form 인스턴스 생성
form = ArticleForm(request.POST)

# CREATE
new_article = form.save()

# UPDATE
article = Article.objects.get(pk=1)
# instance 값이 있으면 수정인 것으로 django가 인식
form = ArticleForm(reqeust.POST, instance=article)
form.save()


실습 코드

기존 코드

# articles/views.py
def create(request):
    title = request.POST.get('title')
    content = request.POST.get('content')

    article = Article(title=title, content=content)
    article.save()

    return redirect('articles:detail', article.pk)


form 사용 및 유효성 검사

# articles/views.py
def create(request):
    form = ArticleForm(request.POST)  # request.POST 딕셔너리 내에 모든 정보가 있음
    # 유효성 검사
    if form.is_valid():     # form의 데이터가 유효하다면(True)
        # form 자체를 DB에 레코드로 저장
        # save() 메서드는 저장된 객체를 반환
        # 이를 article에 저장
        article = form.save()    
        return redirect('articles:detail', article.pk)
    return redirect('articles:new')
  • ArticleForm 클래스에 request.POST를 인자로 넣어 form 인스턴스를 생성한다.
  • is_valid 메서드를 이용하면 form 내부의 데이터를 유효성 검사
  • 이를 통과하여 True라면 form을 DB에 save
  • 이는 저장된 객체를 반환하므로 이를 article에 저장
  • article을 활용하여 redirect, 유효성 검사 통과하지 않았다면 다른 return


Form VS ModelForm

Form

  • 어떤 Model에 저장해야 하는지 알 수 없으므로 유효성 검사 이후 cleaned_data 딕셔너리 생성
  • cleaned_data 딕셔너리에서 데이터를 가져온 후 .save() 호출
  • Model에 연관되지 않은 데이터를 받을 때 사용


ModelForm

  • Django가 해당 model에서 양식에 필요한 대부분의 정보를 이미 정의
  • 어떤 레코드를 만들어야 할 지 알고 있으므로 바로 .save() 호출 가능


cleaned_data 구조 예시

def create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            title = form.cleaned_data.get('title')
            content = form.cleaned_data.get('content')
            article = Article.objects.create(title=title, content = content)
    else:
        form = ArticleForm()
    context = {
        'form': form,
    }
    return render(request, 'articles/create.html', context)


함수 구조 합체

CREATE

new와 create는 CREATE를 위해 힘쓰는 두 함수… 합쳐보자!

  • view 함수 new가 흡수되므로 urls.py에서 new 부분을 제거해준다.
  • template 중에서도 new를 호출하는 url tag들의 주소를 모두 create로 바꿔준다.
path('new/', views.new, name='new'),


기존 분리 코드

# articles/views.py
def new(request):
    form = ArticleForm()
    context = {
        'form': form
    }
    return render(request, 'articles/new.html', context)


def create(request):
    form = ArticleForm(request.POST)
    if form.is_valid():
        article = form.save()    
        return redirect('articles:detail', article.pk)
    return redirect('articles:new')


합체 코드

# articles/views.py
def create(request):
    # create
    if request.method == 'POST':
        form = ArticleForm(request.POST)    
        if form.is_valid(): 
            article = form.save()    
            return redirect('articles:detail', article.pk)
    # new
    else:
        form = ArticleForm()
    context = {
        'form': form,
    }
    return render(request, 'articles/create.html', context)
  • request method가 new는 GET, create는 POST
  • .method 메서드를 이용해 request의 메서드가 무엇인지 확인
  • GET일 때, POST일 때의 상황에 따라 상황과 처리 구분

  • is_valid 함수를 통과하지 못하면 else를 거치지 않고 return
  • method가 GET일 때는 아예 빈 form, is_valid에서 비었다면 error message를 담은 form을 context에 전달
  • return문은 context를 필요로 하므로 else(기존 new)문의 context를 함수 전역으로 내어쓰기


UPDATE

어, edit도 update와 합칠 수 있다!


def edit(request, pk):
    article = Article.objects.get(pk=pk)
    context = {
        'article': article,
    }
    return render(request, 'articles/edit.html', context)


def update(request, pk):
    article = Article.objects.get(pk=pk)
    article.title = request.POST.get('title')
    article.content = request.POST.get('content')
    article.save()
    return redirect('articles:detail', article.pk)
  • urls.py의 edit을 제거해준다.


위의 코드를 아래처럼 합칠 수 있다.

def update(request, pk):
    article = Article.objects.get(pk=pk)
    # update
    if request.method == 'POST':
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid():
            article = form.save()
            return redirect('articles:detail', article.pk)
    # edit
    else:
        form = ArticleForm(instance=article)
    context = {
        'article': article,
        'form': form,
    }
    return render(request, 'articles/update.html', context)
  • update이므로, 과거의 데이터이자 수정을 원하는 instance를 지정한다.
  • article이 해당 instance이므로 이를 값으로 넣어준다.


기타 코드의 비밀

form 태그의 action

  • form의 action 속성의 값이 없다면 현재 url을 기준으로 HTTP를 전송
  • 하지만 명시를 해주는 것을 권장


forms.py의 파일 위치

  • Form class는 forms.py 뿐만 아니라 다른 어느 위치에 두어도 상관 없다.
  • 하지만 되도록 app폴더/forms.py에 작성하는 것이 일반적인 구조


왜 method를 POST 먼저 확인하는가.

  • POST는 DB를 조작하는 method
  • 때문에 method가 POST일 때에만 DB를 조작하도록 해야 한다.
  • method를 [POST]와 [POST가 아닌 것]으로 구분하여 확인해야 한다.


Widget 활용하기

가. Meta class 내에 정의(비권장)

# articles/forms.py

class ArticleForm(forms.ModelForm): 
    
    class Meta:
        model = Article        
        fields = '__all__'     
        widgets = {
            'title': forms.TextInput(attrs={
                'class':'my-title',
                'placeholder': 'Enter the title',
                'maxlength': 10,
                }
            )
        }

Django에서 권장하는 스타일은 아님


나. ModelForm class 내에 정의(권장)

# articles/forms.py

class ArticleForm(forms.ModelForm): # ⭐ form.Form과 forms.ModelForm이 다름에 주의할 것
    title = forms.CharField(
        label='제목',
        widget=forms.TextInput(
            attrs={
                'class': 'my-title second-class',
                'placeholder': 'Enter the title',
            }
        ),
    )
    content = forms.CharField(
        label='내용',
        widget=forms.Textarea(
            # 인라인 속성 지정
            attrs={
                'class': 'my-content',
                'placeholder': 'Enter the content',
                'rows': 5,
                'cols': 50,
            }
        ),
        # error message customizing
        error_messages={
            'required': 'Please enter your content', # required 요소가 채워지지 않았을 때의 오류 메시지
        }
    )
    
    
    class Meta:
        model = Article    
        fields = '__all__' 


Rendering Fields Manually

form field를 template에서 어떻게 출력해야 할까요?


수동으로 Form 작성하기

Rendering fields manually

<form action="" method="POST">
  {% csrf_token %}
  <div>
    {{ form.title.errors }}
    {{ form.title.label_tag }}
    {{ form.title }}
  </div>
  <div>
    {{ form.content.errors }}
    {{ form.content.label_tag }}
    {{ form.content }}
  </div>
  <input type="submit">
</form>

참고자료 : django 공식문서 - forms : rendering fields manually


Looping over the form’s fields

<form action="" method="POST">
  {% csrf_token %}
  {% for field in form %}
    {{ field.errors }}
    {{ field.label_tag }}
    {{ field }}
  {% endfor %}
  <input type="submit">
</form>

참고자료 : django 공식문서 - forms : looping over the form’s fields


부트스트랩과 함께 사용하기

Bootstrap class with Widgets

  • Bootstrap Form을 사용해 적용
  • keyword : form-control
  • widget의 class에 form-control 클래스를 입력해주면 된다.


에러메시지 with bootstrap alert 컴포넌트

  • looping 방식으로 사용
<form action="" method="POST">
  {% csrf_token %}
  {% for field in form %}
    {% if field.errors %}
      {% for error in field.errors %}
        <div class="alert alert-warning" role="alert">{{ error|escape }}</div>
      {% endfor %}
    {% endif %}
    <div class="form-group">
      {{ field.label_tag }}
      {{ field }}
    </div>
  {% endfor %}
  <input type="submit">
</form>


⭐ Django Bootstrap Library : django-bootstrap v5

가. pip install

$ pip install django-bootstrap-v5


나. 앱 등록

#  settings.py

INSTALLED_APPS = [
    ...
    'bootstrap5',
    ...
]


다. base.html에 load

  • bootstrap5 CDN을 base.html 템플릿에 load해준다.
  • 부트스트랩의 css와 js CDN의 간소화가 가능해진다.
<!-- articles/base.html -->

{% load bootstrap5 %}   ⭐ bootstrap5 load

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  {% bootstrap_css %}  ⭐ css CDN의 간소화 기능
  <title>Document</title>
</head>
<body>
  <div class="container">
    {% block content %}
    {% endblock content %}
  </div>
  {% bootstrap_javascript %}  ⭐ javascript CDN의 간소화 기능
</body>
</html>


라. form에 적용

  • base.html을 extends하는 하위 template
<!-- article/update.html -->

{% extends 'base.html' %}
{% load bootstrap5 %}

...

<form action="" method="POST">
  {% csrf_token %}
  {% bootstrap_form form layout='horizontal' %}
  {% buttons submit="Submit" reset="Cancel" %}{% endbuttons %}
</form>

...

댓글남기기