정보를 얻을 목적으로 이문서를 접하는 분이 있다면 바로 뒤돌아 가시기 바란다. 나도 정리가 안된 상태에서 개략적으로 공부중인 내용을 그저 기록용으로 남기는 포스팅임을 밝힌다.
Flask 는 Werkzeug, Jinja2 두가지 모듈에 기반한 마이크로 프레임웍이다. Werkzeug는 WSGI 모듈이고, Jinja2는 html template rendering engine이다.
웹개발도 생소하고 처음 접하는거라, 빠르고 쉬운 튜토리얼을 찾았었는데, 무슨소린지 하나도 모르겠더라. 그래서 돌아간 Flask 공식 문서를 봤는데, 문서화가 정말 잘되어 있다. 공부하는 입장에서도 이게 최고다.
설치문서 : https://flask.palletsprojects.com/en/1.1.x/installation/
퀵 스타트 : https://flask.palletsprojects.com/en/1.1.x/installation/ 여기에 가장 기본적인 내용들이 들어있다. 이 내용을 훑어봤고, 개략적으로 기록해보려한다. 혹시 필요하면 그냥 원본 문서 보는게 최고.
Install
batmask@DESKTOP-TGQQ06H /f/output_files/python/flask_myapp
$ python -m venv venv
batmask@DESKTOP-TGQQ06H /f/output_files/python/flask_myapp
$ . ./venv/Scripts/activate
(venv)
batmask@DESKTOP-TGQQ06H /f/output_files/python/flask_myapp
$ pip install Flask
Collecting Flask
...(생략)
(venv)
batmask@DESKTOP-TGQQ06H /f/output_files/python/flask_myapp
$
윈도우즈 msys2 환경에서 작업했다. venv로 가상환경을 만들어 줬고, activate 스크립트를 실행해서 가상환경을 만들어준다. 프롬프트 앞에 (venv)가 보이면 성공. pip로 Flask를 설치하면 알아서 의존성 모듈까지 다 설치가된다.
The First Run
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
Flask 객체를 생성하고 웹 루트인 ‘/’에서 실행할 함수를 지정해줬다.
$ export FLASK_APP=hello.py
$ export FLASK_ENV=development
$ flask run
위와같이 환경변수를 설정해주고 flask run으로 실행하면 서버가 구동된다. 두번째 줄은 개발중 디버그용으로 쓸때 사용한다.
또는 main함수를 만들고 그저 스크립트를 실행할 수도 있다.
if __name__ == "__main__":
app.run(debug=True)
$ python flask_myapp.py
URL 다루기 : routing, building
기본적으로 @app.route("path")
데코레이터를 함수에 붙이면, 해당 웹 디렉토리에 접근할 때 해당 함수가 실행된다.
@app.route('/path/<path:subpath>')
def show_subpath(subpath):
# show the subpath after /path/
return 'Subpath %s' % escape(subpath)
변수도 사용할 수 있는데, 예제에서 <variable> 형태로 사용하면 함수의 인자로 넘어간다. variable 타입은 string, int, float, path, uuid가 있다. path는 string과 같으나, ‘/’도 사용가능한 타입이다.
HTTP의 GET과 POST를 처리하기위해, 다음과 같이 ‘methods’ 인자를 사용한다.
from flask import request
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
return do_the_login()
else:
return show_the_login_form()
내부적으로 URL사용시, 하드코딩하지 않고 url_for()
함수를 쓴다. 주의할 점은 인자명이 경로명이 아니라 경로에 해당하는 함수명이다.
from flask import url_for, redirect
@app.route('/')
@app.route("/home")
def home():
return "home!"
@app.route("/register", methods=["GET", "POST"])
def register():
return redirect(url_for("home"))
redirect()
는 말 그대로 리디렉션 해주는 기능을 하고, 리디렉션으로 이동할 웹 디렉토리를 url_for()
로 생성하는걸 보여준다. 인자로 들어간 “home”은 def home()
을 의미한다.
css 같은 static 파일은 ‘/static’ 폴더를 생성하고 넣어준 후, 다음과 같이 참조가 가능하다.
url_for('static', filename='style.css')
Rendering templates
html 템플릿 엔진으로 Jinja2를 사용한다고 말했었다. 템플릿이란, html안에 코드가 들어가고 이를 템플릿 엔진인 Jinja2가 해석해서 출력할 html을 동적으로 생성해주는걸 말한다. 템플릿 엔진인 Jinja2의 문법은 공식 사이트를 참조. https://jinja.palletsprojects.com/en/2.11.x/
자세한 내용은 공식문서를 참고하고, html안에 어떻게 포함되는지만 언급하고 넘어가겠다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Webpage</title>
</head>
<body>
<ul id="navigation">
{% for item in navigation %}
<li><a href="{{ item.href }}">{{ item.caption }}</a></li>
{% endfor %}
</ul>
<h1>My Webpage</h1>
{{ a_variable }}
{# a comment #}
</body>
</html>
간단한 예제인데, 변수는 {{ }} 로 묶이고, if나 for 같은 구문은 {% %}로 묶인다. 주석은 {# #}이다.
템플릿은 base template을 만들고 이를 상속해서 사용도 가능하다.
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<link rel="stylesheet" href="style.css" />
<title>{% block title %}{% endblock %} - My Webpage</title>
{% endblock %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">
{% block footer %}
© Copyright 2008 by <a href="http://domain.invalid/">you</a>.
{% endblock %}
</div>
</body>
</html>
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
<style type="text/css">
.important { color: #336699; }
</style>
{% endblock %}
{% block content %}
<h1>Index</h1>
<p class="important">
Welcome to my awesome homepage.
</p>
{% endblock %}
{% extends "base.html" %}
로 base.html을 상속받고 있다. 부모에서 정의한 block을 동일하게 써서 오버라이드하며, {{ super() }}
로 부모에서 정의한 블럭을 사용하는게 보인다.
템플릿에선 request, session, g ( get_flashed_messages() )의 3가지 객체를 참조하게된다.
Request
클라이언트로부터 요청을 담은 객체. 자세한건 관련 문서 참조 https://flask.palletsprojects.com/en/1.1.x/api/#flask.Request
GET 형태는 주소뒤에 “?key=value” 형태로 붙어서 오는데, 이는 request의 args.get()으로 가져올 수 있다.
searchword = request.args.get('key', '')
POST 방식의 경우, form 에 입력되어 submit 된 값들은 다음과 같이 가져온다.
@app.route('/login', methods=['POST', 'GET'])
def login():
error = None
if request.method == 'POST':
if valid_login(request.form['username'],
request.form['password']):
return log_the_user_in(request.form['username'])
else:
error = 'Invalid username/password'
# the code below is executed if the request method
# was GET or the credentials were invalid
return render_template('login.html', error=error)
File uploads
파일 업로드의 경우, html form에 enctype="multipart/form-data"
속성을 주어야한다.
업로드된 파일은 메모리나 임시 저장소에 저장되며, 이 파일을 저장하고자 할 때 save()를 사용하여 저장한다.
from flask import request
from werkzeug.utils import secure_filename
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/' + secure_filename(f.filename))
...
임의이름이 아닌, 클라이언트상 파일이름을 가져와 사용할 땐, 보안상의 이슈가 있을 수 있어 secure_filename(f.filename)
을 사용해야 한다.
Cookies
쿠키는 cookies 속성을 사용하고, 다음처럼 request.cookies.get("key"), response.set_cookie("key", "value")
와 같이 사용한다. 보다시피, 값들은 key, value 쌍들인 dictionary 형태이다.
from flask import request
@app.route('/')
def index():
username = request.cookies.get('username')
# use cookies.get(key) instead of cookies[key] to not get a
# KeyError if the cookie is missing.
from flask import make_response
@app.route('/')
def index():
resp = make_response(render_template(...))
resp.set_cookie('username', 'the username')
return resp
Responses
Request를 에러코드와 함께 빠르게 반환하려면 abort()를 쓴다.
from flask import abort, redirect, url_for
@app.route('/')
def index():
return redirect(url_for('login'))
@app.route('/login')
def login():
abort(401)
this_is_never_executed()
커스텀 에러페이지를 보여주려면, errorhandler() 데코레이터를 사용한다.
from flask import render_template
@app.errorhandler(404)
def page_not_found(error):
return render_template('page_not_found.html'), 404
리턴값에 render_template() 뒤에 에러코드가 붙어있는걸 볼 수 있다. 사용법이 좀 묘한데, tuple형태의 리턴이며 Flask에게 에러코드를 알려주는 것이라고 한다.
Flask에선 리턴값에 따라 response처리가 다르게 된다.
- response object가 리턴되는 경우 : 그대로 보냄
- 문자열(String)이 리턴되는 경우 : 기본값을 이용해서 response object를 만들어 보냄
- dict 형식인 경우 : jsonify() 로 JSON 포맷으로 변환되어 보냄.
- tuple 형식인 경우 : (response, status), (response, headers), (response, status, headers) 처럼 사용이 가능하다. status는 앞에서처럼 에러코드만 써줘도 되며, headers는 list나 dictionary 형태가 가능하다.
- 그 외에는 WSGI application을 가정하여 response object로 변환해 보낸다고 한다.
Sessions and Secret key
단발성 request/response가 아닌, Login 유지와 같은 기능은 Session을 이용하게된다. session은 원래 cookie 기반으로 동작하는데, 직접 cookie를 다루지 말고, session 객체를 이용하라고 되어있다. 뭐 보안상의 문제겠지.
session을 사용하려면, encrypt된 값을 보기위한 secret key가 필요하다. 이에 대해 생성하는 방법이 여러가지가 있는데 ( stack overflow 참조 ), 간단한 방법은 python의 random 생성기이다.
python -c 'import os; print(os.urandom(16))'
b'\xba\xc7K\x1a\x06\xe7p\x17\xea\x0c\x13\xa9h\x9c\xac\xb0'
이 값을 app.secret_key = b'\xba\xc7K\x1a\x06\xe7p\x17\xea\x0c\x13\xa9h\x9c\xac\xb0'
와같이 저장한다.
공식 페이지에 있는 사용예는 다음과 같다.
from flask import Flask, session, redirect, url_for, request
from markupsafe import escape
app = Flask(__name__)
# Set the secret key to some random bytes. Keep this really secret!
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
@app.route('/')
def index():
if 'username' in session:
return 'Logged in as %s' % escape(session['username'])
return 'You are not logged in'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''
@app.route('/logout')
def logout():
# remove the username from the session if it's there
session.pop('username', None)
return redirect(url_for('index'))
Message Flashing
유저가 피드백을 받을 수 있게하는 시스템으로 flashing system을 제공한다고 한다. 이것은 request뒤에 메세지를 기록하고, 다음번 request에서 그 메세지를 읽어오도록 하는 방법이다. 정확한 방법은 모르겠으나, 써있는대로 이해해보면, layout template에서 template html에 작업이 되는거 같다. 메세지 쓸 때 flash(), 읽어올 때 get_flashed_messages() 를 이용한다.
Logging
Flask 0.3부터 로깅기능이 생겼다. Logger를 이용한다. 예는 다음과 같다.
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')
까먹지 않게 간단하게 메모만 하려고 했는데, 하나하나 되새김질 하다보니 결국 퀵스타트 문서 거의 전체를 긁어오다시피 했다. 새벽에 피곤해서 뒤로갈수록 더 심해진거 같은데, 일단 그냥 복습한번 했다고 생각하기로.