폼 모듈로 데이터 검증 더 쉽게 하기

jurin·2020년 12월 5일
0

플라스크 - python

목록 보기
6/17

책 '점프 투 플라스크'를 공부하면서 정리한 내용입니다.
출처 : https://wikidocs.net/book/4542

웹 프로그램에서 폼(form)은 사용자에게 입력 양식을 편리하게 제공하기 위해 사용한다. 폼 모듈을 사용하면 폼으로 전송되는 데이터의 필수 여부, 길이, 형식 등을 더 쉽게 검증할 수 있다.

플라스크 폼 모듈 설치

명령 프롬프트에서 Flask-WTF라는 라이브러리를 설치.

(myproject) c:\projects\myproject> pip install Flask-WTF

Flask-WTF를 사용하려면 플라스크 환경 변수 SECRET_KEY가 필요하다. SECRET_KEY는 CSRF(cross-site request forgery)라는 웹 사이트 취약점 공격을 방지하는 데 사용한다. CSRF는 사용자의 요청을 위조하는 웹 사이트 공격 기법인데 SECRET_KEY를 기분으로 해서 생성되는 CSRF 토큰은 폼으로 전송된 데이터가 실제 웹 페이지에서 작성된 데이터인지를 판단해 준다.

쉽게 말해 CSRF 토큰은 CSRF를 방어하려고 플라스크에서 생성하는 무작위 문자열이다.

프로젝트 루트에 있는 config.py 설정 파일을 열고 마지막 줄에 SECRET_KEY 환경 변수를 추가한다.

SECRET_KEY = "dev"

실제 서비스를 운영할 때에는 "dev"처럼 유추하기 쉬운 문자열을 입력하면 안된다.

질문 등록 기능

1. 질문 등록 버튼

질문 목록 조회 템플릿을 열고 table 태그 아래에 질문 등록 버튼을 생성한다.

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

2. 질문 등록 라우트 함수 추가

질문 등록 URL을 추가했으므로 question_views.py 파일에 라우트 함수 create를 추가해야 한다.

from ..forms import QuestionForm
(... 생략 ...)
@bp.route('/create/')
def create():
    form = QuestionForm()
    return render_template('question/question_form.html', form=form)

create 함수에서 QuestionForm 클래스의 객체 form을 생성하고 return 문에서 render_template 함수가 템플릿을 렌더링할 때 form 객체를 전달한다. QuestionForm 클래스는 질문 등록을 할 때 사용할 플라스크의 폼으로 후에 작성할 것이다. 이후 form 객체는 템플릿에서 라벨이나 입력 폼 등을 만들 때 사용한다.

3. 질문 등록 폼 클래스 작성

pybo 디렉토리에 forms.py 파일을 생성하고 질문 등록을 할 때 사용할 QuestionForm 클래스를 작성한다.

from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField
from wtforms.validators import DataRequired

class QuestionForm(FlaskForm):
    subject = StringField('제목', validators=[DataRequired()])
    content = TextAreaField('내용', validators=[DataRequired()])

QuestionForm 클래스는 Flask-WTF 모듈의 FlaskForm 클래스를 상속받으며 subject, content 속성을 포함한다. 폼 클래스의 속성과 모델 클래스의 속성은 비슷하다.

  • 플라스크 폼의 속성(필드) =
<input type="text"> 

또는

<textarea>

와 같은 입력 창에서 사용자가 작성한 값에 대응하는 자료형

  • StringField : 글자 수 제한 (제목). 첫 번째 인자(제목)는 폼 라벨로 사용되며 템플릿에서 이 값으로 라벨을 출력할 수 있다. 두 번째 인자 validators는 필드값을 검증할 때 사용하는 도구. 필수 항목인지 점검하는 DataRequired, 이메일인지 점검하는 Email, 길이를 점검하는 Length 등이 있다. ex) 필수값이면서 이메일이어야 하면 validators=[DataRequired(), Email()]

  • TextAreaField : 글자 수 제한 x (내용)

4. 질문 등록 템플릿 작성

question 디렉터리에 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">

        {{ form.subject.label }}
        {{ form.subject() }}

        {{ form.content.label }}
        {{ form.content() }}

        <button type="submit" class="btn btn-primary">저장하기</button>
    </form>
</div>
{% endblock %}

5. 질문 전송 방식 수정

질문 등록을 해보면 다음과 같은 오류가 발생한다.

현재 폼이 POST 방식으로 데이터를 전송하기 때문이다.

question_form.html 템플릿의 폼 엘리먼트를 보면 method 속성이 "post"이므로 폼에 입력한 데이터는 모두 POST 방식으로 전송되는데 현재 create 라우트에는 별도의 method 속성을 지정하지 않았으므로 기본 처리 방식인 GET 방식만 처리할 수 있다. -> POST 방식으로 데이터를 처리하게 하려면 질문 등록 라우트를 변경해야 한다.

# @bp.route('/create/')에서 아래와 같이 변경
@bp.route('/create/', methods=('GET', 'POST'))

이 후 질문 등록을 이용하면 오류는 보이지 않지만 아무런 반응 없이 질문 등록 화면만 보임 -> create 함수에 데이터를 저장하는 코드를 작성하지 않았기 때문.

6. 폼 데이터를 저장하는 코드 작성

create 함수에 POST 방식으로 요청된 폼 데이터를 DB에 저장하는 코드 추가

from flask import Blueprint, render_template, request, url_for
from werkzeug.utils import redirect
from pybo.models import Question
from ..forms import QuestionForm
from datetime import datetime
from .. import db

bp = Blueprint('question', __name__, url_prefix='/question')


@bp.route('/list/')
def _list():
    question_list = Question.query.order_by(Question.create_date.desc())
    return render_template('question/question_list.html', question_list=question_list)


@bp.route('/detail/<int:question_id>/')
def detail(question_id):
    question = Question.query.get_or_404(question_id)
    return render_template('question/question_detail.html', question=question)

@bp.route('/create/', methods=('GET', 'POST'))
def create():
    form = QuestionForm()
    if request.method == 'POST' and form.validate_on_submit():
        question = Question(subject=form.subject.data, content=form.content.data, create_date=datetime.now())
        db.session.add(question)
        db.sesstion.commit
        return redirect(url_for('main.index'))
    return render_template('question/question_form.html', form=form)

request.method : create 함수로 요청된 전송 방식을 의미. 여기서 POST 방식 요청을 걸러냄

form.validate_on_submit 함수 : POST 방식으로 전송된 폼 데이터의 정합성 점검. 폼을 생성할 때 각 필드에 지정한 DataRequired() 같은 점검 항목에 이상이 없는ㄴ지 확인

return redirect(url_for('main.index')) : 데이터 저장이 완료되면 main.index 페이지로 이동

핵심은 데이터 전송 방식이 POST인지 GET인지에 따라 달리 처리하는 부분이다. <질문 등록하기> 버튼이나 <저장하기> 버튼 모두 같은 페이지를 요청하므로 create 라우트가 이 요청을 받으면 request.method == 'POST' 코드로 요청 방식을 구분해서 렌더링한다.
<질문 등록하기> : GET 방식. 그대로 질문 등록 화면 보여줌.
<저장하기> : POST 방식. DB에 폼 데이터를 저장한 다음 질문 목록으로 이동.

여기까지 해도 <질문 등록하기> 버튼을 누르면 여전히 화면에 아무런 변화가 없다. 이것은 나중에 해결할 것이다.

7. 폼에 부트스트랩 적용하기

{{ form.subject() }}와 같은 코드는 HTML이 폼을 자동으로 생성하므로 부트스트랩을 적용할 수 없다. but 템플릿 수정으로 어느정도 적용 가능.

question_form.html에 form.subject나 form.content에 부트스트랩 클래스 class="form-control" 적용

(... 생략 ...)
        {{ form.subject.label }}
<!-- ------------------------------ [edit] -------------------------------- -->
        {{ form.subject(class="form-control") }}
<!-- ---------------------------------------------------------------------- -->

        {{ form.content.label }}
<!-- ------------------------------ [edit] -------------------------------- -->        
        {{ form.content(class="form-control") }}
<!-- ---------------------------------------------------------------------- -->        
(... 생략 ...)

8. 수작업으로 폼 작성하기

{{ form.subject() }} 처럼 폼을 생성하는 HTML 코드를 자동으로 생성하는 방식은 폼을 빠르게 만들지만 원하는 디자인을 적용하기 힘들다. 이 단점을 보완하고자 HTML을 직접 작성해서 질문 등록 기능을 완성한다.

question_form.html 파일을 열고 form 엘리먼트의 내용을 수정한다.

(... 생략 ...)
    <form method="post" class="post-form my-3">
        <div class="form-group">
            <label for="subject">제목</label>
            <input type="text" class="form-control" name="subject" id="subject">
        </div>
        <div class="form-group">
            <label for="content">내용</label>
            <textarea class="form-control" name="content" id="content" rows="5"></textarea>
        </div>
        <button type="submit" class="btn btn-primary">저장하기</button>
    </form>
</div>
{% endblock %}

수동으로 작성하면 form-group 클래스가 적용돼서 버튼 위에 간격이 생긴다.

질문 등록 기능 완성하기

데이터를 DB에 저장하는 기능을 추가 했지만 저장하기 버튼을 눌러도 변화가 없었다.

1. 오류 내용을 표시해 원인 알아내기

question_form.html에 오류 표시 코드 추가

<!-- 오류표시 Start -->
        {% for field, errors in form.errors.items() %}
        <div class="alert alert-danger" role="alert">
            <strong>{{ form[field].label }}</strong>: {{ ', '.join(errors) }}
        </div>
        {% endfor %}
        <!-- 오류표시 End -->

form.errors.items의 field는 subject나 content와 같은 입력 폼의 필드를 의미

form.validate_on_submit이 실패하면 폼에 오류 내용이 자동으로 등록된다. 이 오류 정보는 form.errors 속성으로 표시할 수 있다.

아무 값도 입력하지 않고 저장하기 버튼을 눌렀을 경우

CSRF Token 오류 : CSRF는 보안 관련 항목으로 form 엘리먼트를 통해 전송된 데이터가 실제 웹 사이트에서 만들어진 데이터인지 검증하는 데 필요한 CSRF 토큰이 빠졌다는 의미

2. CSRF 토큰 오류 처리

question_form.html의 form 엘리먼트 바로 밑에 {{ form.csrf_token }} 코드로 CSRF 토큰 오류를 탈출한다.

(... 생략 ...)
    <h5 class="my-3 border-bottom pb-2">질문등록</h5>
    <form method="post" class="post-form my-3">
        {{ form.csrf_token }}
(... 생략 ...)

이제 저장하기 버튼을 누르면 질문 목록 화면에 등록되어 있는 것을 볼 수 있다.

입력한 값 유지하고 오류 메시지 한글로 바꾸기

1. 입력한 값 유지하기

폼을 전송했을 때 오류가 있더라도 이미 입력한 값을 유지하도록 질문 폼 템플릿을 수정한다.

(... 생략 ...)
        <div class="form-group">
            <label for="subject">제목</label>
            <input type="text" class="form-control" name="subject" id="subject"
                value="{{ form.subject.data or '' }}">
        </div>
        <div class="form-group">
            <label for="content">내용</label>
            <textarea class="form-control" name="content"
                id="content" rows="10">
                {{ form.content.data or '' }}
            </textarea>
        </div>
(... 생략 ...)

2. 오류 메시지 한글로 바꾸기

필수 항목을 입력하지 않았을 때 발생하는 오류 메시지 우리말로 바꾸기.

forms.py 파일을 열어 DataRequired에 한글 메시지 설정

[DataRequired('제목은 필수입력 항목입니다.')])

답변 등록 기능 수정하기

각 질문에 답변을 달 수 있는 기능도 폼을 사용할 수 있도록 한다.

1. 답변 등록 폼 추가

forms.py

class AnswerForm(FlaskForm):
    content = TextAreaField('내용', validators=[DataRequired('내용은 필수입력 항목입니다.')])
  1. 답변 등록 라우트 함수 수정

answer_views.py 파일에서 create 함수가 AnswerForm을 사용하도록 변경

from datetime import datetime

from flask import Blueprint, url_for, request, render_template
from werkzeug.utils import redirect

from pybo import db
from ..forms import AnswerForm
from pybo.models import Question, Answer

bp = Blueprint('answer', __name__, url_prefix='/answer')

@bp.route('/create/<int:question_id>', methods=('POST',))
def create(question_id): # 매개변수 question_id는 URL에서 전달된다.
    # question = Question.query.get_or_404(question_id)
    #
    # # form 엘리먼트를 통해 전달된 데이터들은 create 함수에서 request 객체로 얻을 수 있다.
    # # POST 폼 방식으로 전송된 데이터 항목 중 name 속성이 'content'인 값을 의미한다.
    # content = request.form['content']
    #
    # answer = Answer(content=content, create_date=datetime.now())
    #
    # question.answer_set.append(answer) # ‘질문에 달린 답변들’을 의미
    # # Question과 Answer 모델이 연결되어 backref에 설정한 answer_set을 사용할 수 있다.
    #
    # db.session.commit()
    #
    # # 답변을 생성한 후 화면을 이동하도록 redirect 함수를 사용
    # return redirect(url_for('question.detail', question_id=question_id))
    # # question_id는 question_views.py 파일에 있는 detail 함수의 매개변수로 전달

    # 폼 사용용
    form = AnswerForm()
    question = Question.query.get_or_404(question_id)
    if form.validate_on_submit():
        content = request.form['content']
        answer = Answer(content=content, create_date=datetime.now())
        question.answer_set.append(answer)
        db.session.commit()
        return redirect(url_for('question.detail', question_id=question_id))
    return render_template('question/question_detail.html', question=question, form=form)
  1. CSRF 코드와 오류 표시 기능 추가
    <form action="{{ url_for('answer.create', question_id=question.id) }}" method="post" class="my-3">
        {{ form.csrf_token }}
        <!-- 오류표시 Start -->
        {% for field, errors in form.errors.items() %}
        <div class="alert alert-danger" role="alert">
            <strong>{{ form[field].label }}</strong>: {{ ', '.join(errors) }}
        </div>
        {% endfor %}
        <!-- 오류표시 End -->
        <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>
  1. 상세 보기 라우트에도 폼 적용

질문 상세 조회 템플릿에 폼이 추가되었으므로 question_views.py 파일의 detail 함수도 폼을 사용해야 한다.

완성!

profile
anaooauc1236@naver.com

0개의 댓글