웹 애플리케이션에서 사용자 입력을 처리하는 것은 매우 중요한 부분입니다. Flask에서는 Flask-WTF 확장을 통해 폼 처리와 유효성 검사를 효율적으로 구현할 수 있습니다. 이 글에서는 Flask-WTF를 활용한 폼 생성부터 데이터 검증, CSRF 방어까지 폼 처리의 모든 것을 알아보겠습니다.

1. Flask-WTF 소개 및 설치
Flask-WTF는 Flask 애플리케이션에서 WTForms 라이브러리를 쉽게 통합할 수 있게 해주는 확장 프로그램입니다. 이를 통해 폼 생성, 유효성 검사, CSRF 보호 등을 간편하게 구현할 수 있습니다.
설치 방법
pip를 사용하여 Flask-WTF를 설치할 수 있습니다:
pip install Flask-WTF
2. 기본 폼 생성하기
Flask-WTF를 사용하여 폼을 생성하는 방법을 알아보겠습니다. 기본적인 로그인 폼을 예로 들어 설명하겠습니다.
폼 클래스 정의
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length
class LoginForm(FlaskForm):
email = StringField('이메일', validators=[DataRequired(), Email()])
password = PasswordField('비밀번호', validators=[DataRequired(), Length(min=6)])
submit = SubmitField('로그인')
위 코드에서 LoginForm 클래스는 FlaskForm을 상속받아 이메일과 비밀번호 필드를 정의합니다. 각 필드에는 유효성 검사기(validators)를 추가하여 입력 데이터의 유효성을 검사할 수 있습니다.
Flask 애플리케이션에서 폼 사용하기
from flask import Flask, render_template, flash, redirect, url_for
from forms import LoginForm # 위에서 정의한 폼 클래스 임포트
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key' # CSRF 보호를 위한 비밀 키 설정
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# 폼 데이터가 유효한 경우 처리
flash(f'{form.email.data}로 로그인 성공!')
return redirect(url_for('index'))
return render_template('login.html', form=form)
if __name__ == '__main__':
app.run(debug=True)
템플릿에서 폼 렌더링
login.html 템플릿 파일에서 폼을 렌더링하는 방법입니다:
<!-- login.html -->
<form method="POST" action="{{ url_for('login') }}">
{{ form.hidden_tag() }}
<div>
{{ form.email.label }}
{{ form.email }}
{% if form.email.errors %}
<span class="error">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</span>
{% endif %}
</div>
<div>
{{ form.password.label }}
{{ form.password }}
{% if form.password.errors %}
<span class="error">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</span>
{% endif %}
</div>
<div>
{{ form.submit }}
</div>
</form>
3. 다양한 폼 필드 타입
WTForms는 다양한 폼 필드 타입을 제공합니다:
- StringField: 일반 텍스트 입력
- TextAreaField: 여러 줄 텍스트 입력
- PasswordField: 비밀번호 입력
- BooleanField: 체크박스
- SelectField: 드롭다운 선택
- RadioField: 라디오 버튼
- FileField: 파일 업로드
- DateField: 날짜 입력
- IntegerField: 정수 입력
- FloatField: 실수 입력
예제: 다양한 필드를 포함한 등록 폼
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SelectField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo
class RegistrationForm(FlaskForm):
username = StringField('사용자명', validators=[DataRequired(), Length(min=4, max=20)])
email = StringField('이메일', validators=[DataRequired(), Email()])
password = PasswordField('비밀번호', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('비밀번호 확인',
validators=[DataRequired(), EqualTo('password')])
country = SelectField('국가', choices=[
('kr', '대한민국'),
('us', '미국'),
('jp', '일본'),
('cn', '중국')
])
bio = TextAreaField('자기소개', validators=[Length(max=200)])
accept_tos = BooleanField('이용약관에 동의합니다', validators=[DataRequired()])
submit = SubmitField('가입하기')
4. 유효성 검사(Validation)
WTForms는 다양한 유효성 검사기(validators)를 제공하여 사용자 입력 데이터를 검증할 수 있습니다.
주요 유효성 검사기
- DataRequired(): 필드가 비어있지 않은지 확인
- Email(): 유효한 이메일 형식인지 확인
- Length(min, max): 문자열 길이 제한
- EqualTo(fieldname): 다른 필드와 값이 동일한지 확인 (비밀번호 확인 등에 사용)
- NumberRange(min, max): 숫자 범위 제한
- URL(): 유효한 URL인지 확인
- Regexp(regex): 정규식 패턴에 맞는지 확인
사용자 정의 유효성 검사기 만들기
특별한 유효성 검사 로직이 필요한 경우, 사용자 정의 유효성 검사기를 만들 수 있습니다:
from wtforms.validators import ValidationError
# 사용자 정의 유효성 검사기 함수
def validate_username(form, field):
forbidden_usernames = ['admin', 'root', 'system']
if field.data.lower() in forbidden_usernames:
raise ValidationError('이 사용자명은 사용할 수 없습니다.')
# 폼 클래스에 적용
class RegistrationForm(FlaskForm):
username = StringField('사용자명', validators=[
DataRequired(),
Length(min=4, max=20),
validate_username # 사용자 정의 검사기 추가
])
# 다른 필드들...
폼 클래스 내에서 유효성 검사 메서드 정의
class RegistrationForm(FlaskForm):
username = StringField('사용자명', validators=[DataRequired(), Length(min=4, max=20)])
email = StringField('이메일', validators=[DataRequired(), Email()])
# 다른 필드들...
# 클래스 내에서 유효성 검사 메서드 정의
def validate_username(self, username):
# 예: 데이터베이스에서 사용자명 중복 확인
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('이미 사용 중인 사용자명입니다. 다른 사용자명을 선택해주세요.')
def validate_email(self, email):
# 예: 데이터베이스에서 이메일 중복 확인
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('이미 사용 중인 이메일입니다. 다른 이메일을 입력해주세요.')
5. CSRF 보호
CSRF(Cross-Site Request Forgery)는 웹 애플리케이션의 취약점을 이용한 공격 방식입니다. Flask-WTF는 기본적으로 CSRF 보호 기능을 제공합니다.
CSRF 보호 설정
Flask 애플리케이션에서 SECRET_KEY를 설정하면 자동으로 CSRF 보호가 활성화됩니다:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-very-secure-secret-key' # 실제 운영 환경에서는 안전한 키 사용
템플릿에서 CSRF 토큰 포함하기
폼 템플릿에서 form.hidden_tag()를 사용하여 CSRF 토큰을 포함시킵니다:
<form method="POST" action="{{ url_for('register') }}">
{{ form.hidden_tag() }}
</form>
CSRF 보호 비활성화 (필요한 경우)
특정 폼에서 CSRF 보호를 비활성화해야 하는 경우 (예: API 엔드포인트):
class APIForm(FlaskForm):
class Meta:
csrf = False # 이 폼에서 CSRF 보호 비활성화
6. 파일 업로드 처리
Flask-WTF를 사용하여 파일 업로드를 처리하는 방법을 알아보겠습니다.
파일 업로드 폼 정의
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms import SubmitField
class UploadForm(FlaskForm):
photo = FileField('프로필 사진', validators=[
FileRequired(),
FileAllowed(['jpg', 'png', 'jpeg'], '이미지 파일만 업로드 가능합니다!')
])
submit = SubmitField('업로드')
파일 업로드 처리 라우트
import os
from werkzeug.utils import secure_filename
@app.route('/upload', methods=['GET', 'POST'])
def upload():
form = UploadForm()
if form.validate_on_submit():
f = form.photo.data
filename = secure_filename(f.filename)
upload_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
f.save(upload_path)
flash('파일이 성공적으로 업로드되었습니다!')
return redirect(url_for('index'))
return render_template('upload.html', form=form)
파일 업로드 템플릿
<form method="POST" action="{{ url_for('upload') }}" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div>
{{ form.photo.label }}
{{ form.photo }}
{% if form.photo.errors %}
<span class="error">
{% for error in form.photo.errors %}
{{ error }}
{% endfor %}
</span>
{% endif %}
</div>
<div>
{{ form.submit }}
</div>
</form>
7. 폼 데이터 사전 채우기
기존 데이터로 폼을 미리 채우는 방법을 알아보겠습니다. 이는 사용자 프로필 편집과 같은 기능에 유용합니다.
폼 객체 생성 시 데이터 채우기
@app.route('/edit_profile', methods=['GET', 'POST'])
def edit_profile():
# 현재 사용자 정보 가져오기 (예시)
user = User.query.get(current_user.id)
# 폼 객체 생성 시 데이터 채우기
form = ProfileForm(obj=user)
if form.validate_on_submit():
# 폼 데이터로 사용자 정보 업데이트
form.populate_obj(user)
db.session.commit()
flash('프로필이 업데이트되었습니다!')
return redirect(url_for('profile'))
return render_template('edit_profile.html', form=form)
수동으로 폼 데이터 채우기
@app.route('/edit_profile', methods=['GET', 'POST'])
def edit_profile():
user = User.query.get(current_user.id)
form = ProfileForm()
if request.method == 'GET':
# GET 요청 시 폼 필드에 데이터 채우기
form.username.data = user.username
form.email.data = user.email
form.bio.data = user.bio
if form.validate_on_submit():
# 폼 데이터로 사용자 정보 업데이트
user.username = form.username.data
user.email = form.email.data
user.bio = form.bio.data
db.session.commit()
flash('프로필이 업데이트되었습니다!')
return redirect(url_for('profile'))
return render_template('edit_profile.html', form=form)
8. 동적 폼 필드
상황에 따라 동적으로 폼 필드를 추가하거나 변경해야 할 때가 있습니다. 이를 구현하는 방법을 알아보겠습니다.
폼 초기화 시 동적 필드 추가
from wtforms import SelectField
class DynamicForm(FlaskForm):
name = StringField('이름', validators=[DataRequired()])
# 다른 기본 필드들...
def __init__(self, *args, **kwargs):
super(DynamicForm, self).__init__(*args, **kwargs)
# 데이터베이스에서 카테고리 목록을 가져와 동적으로 필드 옵션 설정
categories = Category.query.all()
self.category.choices = [(c.id, c.name) for c in categories]
# 동적으로 옵션이 설정될 필드 정의
category = SelectField('카테고리', coerce=int)
9. 다중 폼 처리
하나의 페이지에서 여러 폼을 처리해야 할 경우가 있습니다. 이를 구현하는 방법을 알아보겠습니다.
여러 폼 처리하기
@app.route('/account', methods=['GET', 'POST'])
def account():
profile_form = ProfileForm(prefix='profile')
password_form = PasswordChangeForm(prefix='password')
if 'profile-submit' in request.form and profile_form.validate_on_submit():
# 프로필 폼 처리
# ...
flash('프로필이 업데이트되었습니다!')
return redirect(url_for('account'))
if 'password-submit' in request.form and password_form.validate_on_submit():
# 비밀번호 변경 폼 처리
# ...
flash('비밀번호가 변경되었습니다!')
return redirect(url_for('account'))
return render_template('account.html',
profile_form=profile_form,
password_form=password_form)
템플릿에서 여러 폼 렌더링
<!-- 프로필 폼 -->
<form method="POST" action="{{ url_for('account') }}">
{{ profile_form.hidden_tag() }}
<h3>프로필 정보 수정</h3>
<div>
{{ profile_form.username.label }}
{{ profile_form.username }}
</div>
<div>
{{ profile_form.email.label }}
{{ profile_form.email }}
</div>
<button type="submit" name="profile-submit">프로필 업데이트</button>
</form>
<!-- 비밀번호 변경 폼 -->
<form method="POST" action="{{ url_for('account') }}">
{{ password_form.hidden_tag() }}
<h3>비밀번호 변경</h3>
<div>
{{ password_form.current_password.label }}
{{ password_form.current_password }}
</div>
<div>
{{ password_form.new_password.label }}
{{ password_form.new_password }}
</div>
<div>
{{ password_form.confirm_password.label }}
{{ password_form.confirm_password }}
</div>
<button type="submit" name="password-submit">비밀번호 변경</button>
</form>
10. 폼 매크로 사용하기
Jinja2 매크로를 사용하면 폼 렌더링 코드를 재사용할 수 있어 템플릿 코드를 간결하게 유지할 수 있습니다.
폼 매크로 정의
<!-- macros.html -->
{% macro render_field(field) %}
<div class="form-group">
{{ field.label }}
{{ field(class="form-control") }}
{% if field.errors %}
<div class="errors">
{% for error in field.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
매크로 사용하기
<!-- login.html -->
{% from "macros.html" import render_field %}
<form method="POST" action="{{ url_for('login') }}">
{{ form.hidden_tag() }}
{{ render_field(form.email) }}
{{ render_field(form.password) }}
<button type="submit" class="btn btn-primary">로그인</button>
</form>
11. 실전 예제: 완전한 등록 및 로그인 시스템
지금까지 배운 내용을 종합하여 완전한 등록 및 로그인 시스템을 구현해보겠습니다.
폼 클래스 정의
# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from models import User
class RegistrationForm(FlaskForm):
username = StringField('사용자명', validators=[DataRequired(), Length(min=4, max=20)])
email = StringField('이메일', validators=[DataRequired(), Email()])
password = PasswordField('비밀번호', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('비밀번호 확인',
validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('가입하기')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('이미 사용 중인 사용자명입니다.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('이미 사용 중인 이메일입니다.')
class LoginForm(FlaskForm):
email = StringField('이메일', validators=[DataRequired(), Email()])
password = PasswordField('비밀번호', validators=[DataRequired()])
remember = BooleanField('로그인 상태 유지')
submit = SubmitField('로그인')
라우트 구현
# app.py
from flask import Flask, render_template, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager, login_user, current_user, logout_user, login_required
from forms import RegistrationForm, LoginForm
from models import User, db
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db.init_app(app)
bcrypt = Bcrypt(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('home'))
form = RegistrationForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user = User(username=form.username.data, email=form.email.data, password=hashed_password)
db.session.add(user)
db.session.commit()
flash('계정이 생성되었습니다! 이제 로그인할 수 있습니다.', 'success')
return redirect(url_for('login'))
return render_template('register.html', title='회원가입', form=form)
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('home'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and bcrypt.check_password_hash(user.password, form.password.data):
login_user(user, remember=form.remember.data)
flash('로그인 성공!', 'success')
return redirect(url_for('home'))
else:
flash('로그인 실패. 이메일과 비밀번호를 확인해주세요.', 'danger')
return render_template('login.html', title='로그인', form=form)
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('home'))
@app.route('/')
def home():
return render_template('home.html', title='홈')
@app.route('/profile')
@login_required
def profile():
return render_template('profile.html', title='프로필')
if __name__ == '__main__':
app.run(debug=True)
결론
이 글에서는 Flask-WTF를 사용하여 폼 처리 및 유효성 검사를 구현하는 방법을 자세히 알아보았습니다. 기본적인 폼 생성부터 유효성 검사, CSRF 보호, 파일 업로드, 동적 폼 필드 등 다양한 기능을 살펴보았습니다.
Flask-WTF는 웹 애플리케이션에서 사용자 입력을 안전하고 효율적으로 처리할 수 있는 강력한 도구입니다. 이 글에서 배운 내용을 활용하여 보다 안전하고 사용자 친화적인 웹 애플리케이션을 개발하시기 바랍니다.
추가적인 정보와 고급 기능은 Flask-WTF 공식 문서와 WTForms 공식 문서를 참고하세요.
답글 남기기