
pip install -U Flask-WTF
또는
💡 requirements.txt 파일
- 프로젝트에 필요한 라이브러리(설치 패키지들)와 버전을 명시하는 파일
예)Flask==1.0.2- 필요한 패키지를 포함하지 않고 적은 용량으로 프로젝트 공유 가능
- 0.9.0 버전부터는 파일에
Flask-WTF뿐만 아니라wtforms도 추가해야 한다- 터미널에서 모든 파일 한꺼번에 설치
- 윈도우:
python -m pip install -r requirements.txt- 맥:
pip3 install -r requirements.txt
사용자 이름과 비밀번호를 정확하게 입력해야만 비밀키를 가진 페이지에 접근 가능한 웹사이트
🔍 유의 사항
- Quickstart 참조
- 메인 화면의 email과 password 항목을 StringFields로 만들기
(validators는 유효성 검사를 추가할 때 사용되므로 이번 단계에서 넘어감)<form>태그의{{ form.csrf_token }}은 CSRF 보호 기능을 추가하는 것- main.py에서 csrf_token을 생성하는 데 사용될 비밀키 생성하기
app.secret_key = "원하는 문자열"
⌨️ main.py
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField
class LoginForm(FlaskForm):
email = StringField('Email')
password = StringField('Password')
app = Flask(__name__)
app.secret_key = "원하는-문자열"
@app.route("/")
def home():
return render_template('index.html')
@app.route("/login")
def login():
login_form = LoginForm()
return render_template('login.html', form=login_form)
if __name__ == '__main__':
app.run(debug=True)
🏗️ login.html
<body>
<div class="container">
<h1>Login</h1>
<form method="POST" action="/login">
{{ form.csrf_token }}
{{ form.email.label }} {{ form.email(size=30) }}
{{ form.password.label }} {{ form.password(size=30) }}
<input type="submit" value="Go">
</form>
</div>
</body>
HTML로 <label>이나 <input> 요소를 직접 만들지 않고 양식 생성
🔍 유의 사항
- 1번에서 퀵스타트로 간단히 작성한 코드를 개선
- 패스워드 입력창을
StringField에서PasswordField으로 변경
(입력한 텍스트가 ••• 으로 가려짐)StringField,PasswordField생성시label키워드 인자를 추가해서 명확히 하기
(login.html 파일에 전달될 때.label)action을 동적 URL로 수정- WTForms으로 생성한 입력양식의 레이블과 입력창에 HTML 요소를 사용하여 레이아웃을 지정
- Go 버튼도
SubmitField로 변경
⌨️ main.py
…
from wtforms import StringField, PasswordField, SubmitField
class LoginForm(FlaskForm):
email = StringField('Email')
password = PasswordField('Password')
submit = SubmitField(label='Log In')
…
🏗️ login.html
<body>
<div class="container">
<h1>Login</h1>
<form method="POST" action="{{ url_for('login') }}">
{{ form.csrf_token }}
<p>{{ form.email.label }}<br>{{ form.email(size=30) }}</P>
<p>{{ form.password.label }}<br>{{ form.password(size=30) }}</P>
{{ form.submit }}
</form>
</div>
</body>
🔍 유의 사항
- Validators 참고
- 입력양식의 입력창에 validator 객체 추가
validators매개변수는 validator 객체의 리스트DataRequired를 지정한 입력창에 아무것도 입력하지 않으면 오류 발생- 입력양식 제출 후 오류가 많으면 errors 리스트가 생성됨
- 오류를 발생시킨 필드의 프로퍼티로 HTML에 전달:
form.<field>.errors- 반복문을 사용하여 오류들을 텍스트로 보여줄 수 있다
- 사용자가 제출 버튼을 눌렀을 때 입력값의 유효성 검증하기
- POST 요청에 응답 후 데이터를
validate_on_submit()으로 검증하도록 수정- 모든 사용자가 입력창의 유효성 검증을 받아야 한다
- 브라우저마다 내장된 검증 방법이 다르기 때문 (크롬의 경우 팝업 알림)
<form>태그에novalidate속성을 넣어서 브라우저 검증 끄기- 이메일 검증을 위해 email-validator 패키지 설치하기
⌨️ main.py
…
from wtforms.validators import DataRequired, Email, Length
class LoginForm(FlaskForm):
email = StringField('Email', validators=[
DataRequired(), Email(message='Invalid email address')
])
password = PasswordField('Password', validators=[
DataRequired(), Length(min=8, message='Field must be at least characters long')
])
submit = SubmitField(label='Log In')
…
🏗️ login.html
<body>
<div class="container">
<h1>Login</h1>
<form method="POST" action="{{ url_for('login') }}" novalidate>
{{ form.csrf_token }}
<p>
{{ form.email.label }}<br>{{ form.email(size=30) }}
{% for err in form.email.errors %}
<span style="color:red">{{ err }}</span>
{% endfor %}
</P>
<p>
{{ form.password.label }}<br>{{ form.password(size=30) }}
{% for err in form.password.errors %}
<span style="color:red">{{ err }}</span>
{% endfor %}
</P>
{{ form.submit }}
</form>
</div>
</body>
🔍 유의 사항
<form_object>.<form_field>.data로 폼 데이터 찾기validate_on_submit()의 반환값으로 요청이 GET 인지 POST인지 확인하기
- 사용자가 폼을 제출한 후 유효성 검증이 성공한 경우 True, 실패한 경우 False
- Day60에서는
if request.method == "POST"사용- 이메일이 admin@email.com, 비밀번호는 12345678
- 일치할 때 success.html 페이지 표시,
- 일치하지 않을 경우 denied.html 페이지가 표시되도록 수정
⌨️ main.py
@app.route("/login", methods=["GET", "POST"])
def login():
login_form = LoginForm()
if login_form.validate_on_submit():
if login_form.email.data == "admin@email.com" and login_form.password.data == "12345678":
return render_template("success.html")
else:
return render_template("denied.html")
return render_template('login.html', form=login_form)
🔍 유의 사항
- 전체 웹 사이트에 동일한 디자인 템플릿을 사용하지만 일부 코드를 변경해야할 경우 템플릿 상속 사용
- 클래스 상속과 유사해서 상위 템플릿을 가져와 하위 웹 페이지에서 스타일 확장 가능
- 상위 템플릿 base.html에 새 콘텐츠를 삽입할 수 있는 미리 정의된 영역(블록) 존재
- 하위 템플릿 success.html, denied.html이 상위 템플릿을 상속
- 슈퍼 블록
- super 키워드는 자식 클래스에 상속하는 부모 클래스를 나타냄
- 템플릿을 상속받을 때 일부는 유지하되 일부는 추가하고 싶은 경우 사용
- 부모 클래스에서 블록 지정(원하는 이름 사용 가능)
- 자식 클래스에서 슈퍼 블록
{{ super() }}을 추가하면 모든 코드가 하위 페이지에 삽입- ❗️모든 블록은
{% endblock %}으로 닫는 것 잊지 말기
🏗️ base.html
<head>
<meta charset="UTF-8">
<title>{% block title %}{% endblock %}</title>
<style>
{% block styling %}
body {
background: purple;
}
{% endblock %}
</style>
</head>
<body>
{% block content %}{% endblock %}
</body>
🏗️ success.html
<!-- 템플릿 엔진(진자)이 'base.html'을 이 페이지의 템플릿으로 사용하도록 명령 -->
{% extends "base.html" %}
<!-- 템플릿의 헤더에 사용자 정의된 제목을 삽입 -->
{% block title %}Success{% endblock %}
<!-- 웹 사이트의 콘텐츠를 제공하는 부분 -->
{% block content %}
<div class="container">
<h1>Top Secret </h1>
<iframe src="https://giphy.com/embed/Ju7l5y9osyymQ" width="480" height="360" frameBorder="0" class="giphy-embed" allowFullScreen></iframe>
<p><a href="https://giphy.com/gifs/rick-astley-Ju7l5y9osyymQ">via GIPHY</a></p>
</div>
{% endblock %}
🏗️ denied.html
{% extends "base.html" %}
{% block title %}Access Denied{% endblock %}
<!-- denied 페이지에만 다르게 적용할 스타일 -->
{% block styling %}
{{ super() }}
h1 {
color: red;
}
{% endblock %}
{% block content %}
<div class="container">
<h1>Access Denied </h1>
<iframe src="https://giphy.com/embed/1xeVd1vr43nHO" width="480" height="271" frameBorder="0" class="giphy-embed" allowFullScreen></iframe>
<p><a href="https://giphy.com/gifs/cheezburger-funny-dog-fails-1xeVd1vr43nHO">via GIPHY</a></p>
</div>
{% endblock %}
🔍 유의 사항
Flask-Bootstrap패키지 설치- 부트스트랩을 템플릿으로 사용하도록 변경
- 5번에서 작성한 denied.html의 슈퍼 블록 삭제
- base.html 에서 Bootstrap CSS 포함시키기
- 강의와 달리 부트스트랩 문서에서 CDN 링크를 복사해서 붙여야 작동함
- 링크에서 최신 버전 링크로 작성해야 스타일이 제대로 적용됨
- denied.html, success.html, login.html, index.html 코드 변경
⌨️ main.py
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length
from flask_bootstrap import Bootstrap
class LoginForm(FlaskForm):
email = StringField('Email', validators=[
DataRequired(), Email(message='Invalid email address')
])
password = PasswordField('Password', validators=[
DataRequired(), Length(min=8, message='Field must be at least 8 characters long')
])
submit = SubmitField(label='Log In')
app = Flask(__name__)
app.secret_key = "secret-key-string"
Bootstrap(app)
@app.route("/")
def home():
return render_template('index.html')
@app.route("/login", methods=["GET", "POST"])
def login():
login_form = LoginForm()
if login_form.validate_on_submit():
if login_form.email.data == "admin@email.com" and login_form.password.data == "12345678":
return render_template("success.html")
else:
return render_template("denied.html")
return render_template('login.html', form=login_form)
if __name__ == '__main__':
app.run(debug=True)
🏗️ base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% block styles %}
<!-- Bootstrap CSS CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
{% endblock %}
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
<!-- (필요할 경우) Bootstrap JS and dependencies CDN -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
🏗️ index.html
{% extends "base.html" %}
{% block title %}Secrets{% endblock %}
{% block content %}
<!--Using Boostrap classes for styling here-->
<div class="hero bg-light py-5">
<div class="container">
<h1>Welcome</h1>
<p>Are you ready to discover my secret?</p>
<a class="btn btn-primary btn-lg" href="{{ url_for('login') }}">Login</a>
</div>
</div>
{% endblock %}
🏗️ login.html
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="container">
<h1>Login</h1>
<form method="POST" action="{{ url_for('login') }}" novalidate>
{{ form.csrf_token }}
<p>
{{ form.email.label }}<br>{{ form.email(size=30) }}
{% for err in form.email.errors %}
<span style="color:red">{{ err }}</span>
{% endfor %}
</P>
<p>
{{ form.password.label }}<br>{{ form.password(size=30) }}
{% for err in form.password.errors %}
<span style="color:red">{{ err }}</span>
{% endfor %}
</P>
{{ form.submit }}
</form>
</div>
{% endblock %}
🏗️ denied.html
{% extends "base.html" %}
{% block title %}Access Denied{% endblock %}
{% block content %}
<div class="container">
<h1>Access Denied </h1>
<iframe src="https://giphy.com/embed/1xeVd1vr43nHO" width="480" height="271" frameBorder="0" class="giphy-embed" allowFullScreen></iframe>
<p><a href="https://giphy.com/gifs/cheezburger-funny-dog-fails-1xeVd1vr43nHO">via GIPHY</a></p>
</div>
{% endblock %}
🏗️ success.html
{% extends "base.html" %}
{% block title %}Access Granted{% endblock %}
{% block content %}
<div class="container">
<h1>Top Secret </h1>
<iframe src="https://giphy.com/embed/Ju7l5y9osyymQ" width="480" height="360" frameBorder="0" class="giphy-embed" allowFullScreen></iframe>
<p><a href="https://giphy.com/gifs/rick-astley-Ju7l5y9osyymQ">via GIPHY</a></p>
</div>
{% endblock %}
🔍 유의 사항
{% import "bootstrap/wtf.html" as wtf %}를 임포트하고
{{ wtf.quick_form(form) }}를 사용하여 한 줄의 코드로 간단히 폼 작성 가능
- 템플릿에 상속된 WTForm 객체(form)를 가져오는 코드 라인
- 폼에 대한 모든 레이블, 입력, 버튼, 스타일을 생성한다
novalidate=True옵션으로 브라우저 내장 유효성 검사 기능 끄기
🏗️ login.html
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="container">
<h1>Login</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form, novalidate=True) }}
</div>
</div>