앞서 detail.html 템플에서 투표용 라디오 버튼을 보여주는 폼을 작성했고, vote() 뷰 함수에서 이 폼을 처리하는 로직을 작성했다.
웹 사이트를 개발할 때 사용자로부터 입력을 받기 위해 폼을 사용한다. <form>...</form> 사이에 있는 엘리먼트들의 집합으로 폼을 통해 텍스트를 입력할 수도 있고, 항목을 선택할 수도 있다.
폼에 입력된 데이터는 서버로 보내지는데 폼은 <input> 엘리먼트 외에도 폼 데이터를 어디로 보낼지 지정해주는 action 속성과 어떤 HTTP 메소드로 보낼지 지정해주는 method 속성을 설정해줘야 한다.
HTTP 프로토콜 중 폼에서 사용할 수 있는 HTTP 메소드는 GET과 POST가 있는데 장고는 POST 방식만을 사용한다. 서버 시스템의 상태를 바꾸는 요청(DB 변경)은 POST 방식을 사용하고 아닌 경우 GET을 사용하는데 GET 방식은 URL이나 브라우저 히스토리, 서버의 로그에 텍스트로 보일 수 있기 때문에 보안에 취약하기 때문에 POST 방식만을 사용한다.
추가로 장고는 보안을 강화하기 위해 CSRF 방지 기능을 제공한다.
반면 검색 폼의 경우 GET 방식이 적절한데, GET 방식의 데이터가 URL에 포함되므로 URL을 북마크해두고 쉽게 공유하거나 재전송할 수 있기 때문이다.
Admin 사이트의 경우 여러 가지 타입의 많은 위젯이 폼 화면 출력용으로 준비되어야 하고, HTML로 렌더링되면, 적절한 인터페이스를 사용하여 입력 및 수정되고, 서버로 보내져서 데이터가 유효한지 검증을 거친 후에 저장, 전달된다.
장고는 이러한 폼 기능들을 단순화하고 자동화해서 개발자가 직접 코딩하는 것보다 훨씬 안전하게 처리해준다.
웹 개발에 있어서 폼이라는 용어는 HTML의 <form>일 수도 있고, <form>을 만들어내는 장고의 Form 클래스일 수도 있고, 서버로 제출된 구조화된 데이터일 수도 있다. 이 모든 것을 통칭해서 폼이라고도 한다.
장고의 모델 클래스가 DB 테이블의 논리적인 구조 및 동작 기능, 우리에게 보여지는 방식들을 기술하는 것과 마찬가지로, 폼 클래스는 폼을 기술하고 폼이 어떻게 작동하고 어떻게 보이는지를 결정한다.
모델 클래스의 필드가 DB의 필드로 매핑되듯이, 폼 클래스의 필드도 HTML 폼의 <input> 엘리먼트에 매핑된다. 폼 클래스의 필드는 폼 데이터를 저장하는 클래스로 폼이 제출되면 자신의 데이터에 대한 유효성 검사를 실시한다. 필드는 저장하는 데이터의 종류에 따라 자신의 타입을 가지고, 브라우저에서 HTML 위젯으로 표현된다. 필드 타입마다 디폴트 위젯 클래스를 가지고 있으며 필요 시 오버라이딩될 수 있다. 따라서 폼 클래스의 필드를 정할 때도 대상이 되는 데이터에 맞는 적절한 타입을 지정해줘야 한다.
폼도 결국은 템플릿의 일부이므로 템플릿 코드에 포함되어서 렌더링 절차를 거친다.
폼도 객체이기 때문에 템플릿에서 폼을 렌더링하는 작업은 객체를 렌더링하는 것과 거의 동일하지만 폼 객체에는 데이터가 없을 수도 있다는 점을 주의해야 한다.
폼 객체는 렌더링 이후 사용자가 데이터를 채우는 것이 보통이므로 빈 객체를 렌더링하는 일이 자주 발생한다. 폼 객체는 보통 뷰 함수에서 생성하는데 뷰 함수에서 데이터 없이 만들 것인지, 채워서 만들것인지 구분해야 한다.
테이터를 채울 때는 저장된 모델 객체로부터 채울 수도, 직전에 제출된 HTML 폼으로부터 채울 수도 있다. 후자는 폼 필드가 여러 개인 경우 하나의 필드에서 에러가 발생하여 다시 폼 데이터를 입력할 때 에러가 없는 다른 필드들은 직전에 제출된 폼으로부터 데이터를 채우고 사용자에게 보여주는 경우가 될 수 있다.
데이터가 없는 폼을 언바운드 폼이라고 하며, 렌더링되어 사용자에게 보여질 때 비어있거나 디폴트 값으로 채워진다. 바운드 폼은 제출된 데이터를 갖고 있어서 데이터의 유효성 검사를 하는 데 사용된다.
사용자의 이름을 취득하기 위해 간단한 폼을 만든다고 가정해보자. 최종적으로 필요한 템플릿은 다음과 같다.
<form action="/your-name/" method="post">
<label for="your_name">Your name: </label>
<input id="your_name" type="text" name="your_name" value="{{ current_name }}">
<input type="submit" value="OK">
</form>
POST 방식을 이용해 브라우저에게 폼 데이터를 URL/your-name/으로 보내달라고 요청하고 있다. {{ current_name }} 템플릿 변수는 템플릿 렌더링을 요청하는 뷰에서 그 변수값을 지정해줄 것이다.
폼이 제출되면 POST 요청에 폼 데이터가 담겨서 서버로 보내집니다. 그리고 URL/your-name/에 해당하는 뷰가 있어서, 요청 데이터에서 이름/값 쌍을 찾아서 뷰의 로직에 따라 적절한 처리를 할 것이다.
장고는 이처럼 <form> 엘리먼트의 기능을 제공하기 위해 폼 클래스를 정의한다. 모든 폼 클래스는 django.forms.Form의 자식 클래스로 생성된다.
폼 클래스 정의
from django import forms
class NameForm(forms.Form):
your_name = forms.CharField(label='Your name', max_length=100)
위는 필드가 your_name 하나인 폼 클래스이다. label 속성도 정의했는데 렌더링되면 <label> 엘리먼트로 나타날 것이다.
각각의 폼 필드는 위젯 클래스를 갖고 있고, 위젯 클래스는 <input type="text">와 같은 HTML 폼 위젯으로 대응된다. 대부분의 폼 필드는 디폴트 위젯을 갖고 있고, 위에서 사용된 CharField 필드 타입은 TextInput 위젯이 디폴트 위젯이며, HTML <input type="text">로 변환된다.
디폴트 위젯을 <textarea>로 변경하려면 폼 필드를 정의할 때 명시적으로 지정하면 된다.
your_name = forms.CharField(label='Your name', max_length=100, widget=forms.Textarea)
장고의 폼 클래스는 모든 필드에 대해 유효성 검사 루틴을 실행시키는 is_valid() 메소드를 갖고 있고, 이 메소드가 호출되어 유효성 검사를 하고, 그 결과 모든 필드가 유효하다면 is_valid() 메소드는 True를 반환하고 폼 데이터를 cleaned_data 속성에 넣는다.
위의 폼 클래스가 템플릿 시스템에 의해 렌더링되면 다음과 같은 결과가 나온다.
<label for="your_name">Your name: </label>
<input id="your_name" type="text" name="your_name" maxlength="100">
렌더링 결과에 <form> 태그나 submit 버튼은 없는데, 이건 개발자가 직접 템플릿에 넣어줘야 한다.
<form action="/your-name/" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
폼 클래스는 {{ form }} 변수로 사용하였다. {{ form }} 변수는 뷰에서 컨텍스트 변수에 포함하여 템플릿 시스템으로 넘겨주게 된다.
폼을 처리하는 뷰는 2개가 필요하다.
2개의 뷰는 하나의 뷰로 통합하여 처리할 수 있는데 장고에서는 이를 권장한다.
하나의 뷰에서 2가지 기능을 처리하려면 처음 사용자에게 보여주는 폼과 사용자가 데이터를 입력한 후 제출된 폼을 구분하여 처리할 수 있어야 한다. 장고에선 이를 HTTP 메소드로 구분한다.
즉, 뷰가 GET 방식으로 요청받은 경우 사용자에게 처음으로 폼을 보여주도록 처리하고, POST 방식으로 요청받은 경우 데이터가 담긴 제출된 폼으로 간주하여 처리하게 된다.
from django.shortcuts import render
from django.http import HttpResponseRedirect
def get_name(request):
# POST 방식이면 데이터가 담긴 제출된 폼으로 간주한다.
if request.method == 'POST':
# request에 담긴 데이터로 클래스 폼을 생성
form = NameForm(request.POST)
# 폼에 담긴 데이터가 유효한지 체크
if form.is_valid():
# 폼 데이터가 유효하면 데이터는 cleaned_data로 복사
new_name = form.cleaned_data['name']
# 로직에 다라 추가적인 처리를 한다.
# 새로운 URL로 리다이렉션 시킨다.
return HttpResponseRedirect('/thanks/')
# POST 방식이 아니면(GET)
# 빈 폼을 사용자에게 보여준다.
else:
form = NameForm()
return render(request, 'name.html', {'form': form})
폼 클래스를 템플릿으로 변환하기 위해서는 폼 객체를 생성해서 이를 템플릿 시스템에 넘겨주면 된다. 템플릿 시스템에서 템플릿 문법 및 폼 객체를 해석해서 HTML 템플릿 파일을 만들어준다.
{{ form }}구문은 HTML의 <label>과 <input> 엘리먼트 쌍으로 렌더링된다. HTML <label>/<input> 쌍으로 변환 시, {{ form }} 이외에도 3가지 옵션이 있다.
<label>/<input> 태그 쌍을 감싸는 <table> 혹은 <ul> 태그는 개발자가 직접 추가해야 한다.
ContactForm 폼 클래스를 정의하고
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField(widget=forms.Textarea)
sender = forms.EmailField()
cc_myself = forms.BooleanField(required=False)
{{ form.as_p }}옵션으로 변환하는 경우 그 결과 템플릿 파일은 다음과 같은 내용이 될 것이다.
<p><label for="id_subject">Subject:</label>
<input id="id_subject" type="text" name="ubject" maxlength="100"></p>
<p><label for="id_message">Message:</label>
<input type="text" name="message" id="id_message"></p>
<p><label for="id_sender">Sender:</label>
<input type="email" name="sender" id="id_sender"></p>
<p><label for="id_cc_myself">Cc_myself:</label>
<input type="checkbox" name="cc_myself" id="id_cc_myself"></p>
<label> 태그에 나타나는 텍스트는 각 필드를 정의할 때 명시적으로 지정할 수 있다. 위에서는 지정하지 않았기 때문에 디폴트 레이블 텍스트를 사용하였다. 디폴트 레이블 텍스트는 필드면에서 첫 자를 대분자로 하고 밑줄은 빈칸으로 변경하여 만든다. <input id> 태그 속성도 각 필드의 필드명을 사용하여 id_필드명 형식으로 만든다. <label for> 태그 속성에도 사용된다.
출처: Django로 배우는 파이썬 웹 프로그래밍(기초) - 김석훈님