본문 바로가기

Python/Library

[Flask] - Quickstart

*목차

1. A Minimal Application

2. Debug mode

3. Route

4. Static Files

5. Rendering Templates

6. Accessing Request Data

7. Redirect and Errors

8. About Response

9. Sessions

10.Message Flashing

11. Logging

12. Hooking in WSGI Middleware

13. Using Flask Extensions

14. Deploying to a Web Server

 

 

 

 


* 본 문서는 Flask 공식 튜토리얼을 참조하였습니다.

* linux ubuntu 22.04 LTS server, vscode 환경에서 진행하였습니다.

 

Flask의 QuickStart 내용을 예제와 함께 다뤄보았습니다.

 

 


1. A Minimal Application

Flask를 실행할 수 있는 최소 Application 단위 코드입니다.

from flask import Flask 	## Flask를 불러옵니다.

app = Flask(__name__) 		## __name__은 Flask가 어떤 경로로 실행되고 있는지를 알려주기 위함입니다.

@app.route("/")     		## decorator문법을 활용해서 url을 표현합니다.
def hello_world():   		## 함수입니다.
    return "<p>Hello, World!</p>"	## 내보낼 HTML 문서입니다.

 

위의 코드파일명이 app.py, wsgi.py인 경우 flask run으로 실행할 수 있습니다. 그 외의 이름(예 : python_file_name.py)을 사용한 경우에는 다음과 같이 실행할 수 있습니다.

flask --app python_file_name run

 

flask를 실행할 때, host와 port를 조절해서 원하는 주소로 열 수 있습니다.

flask run --host=0.0.0.0 --port=9000

 

만약 script를 통해서 실행이 아닌 python 자체에서 실행하고 싶으시다면 다음과 같이 작성할 수 있습니다.

 

from flask import Flask


app = Flask(__name__)


@app.route('/')
def hello():
    return "<p>Hello, World!</p>"


if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5000)

 


2.  Debug Mode

flask 실행 파일의 문제가 생긴 경우 그 문제에 대해서 상세하게 알려주는 mode입니다. 이 모드는 실제 Web 운영 server에서는 적용하지 않아야 합니다.

Debug Mode는 --debug 옵션을 사용하면 됩니다.

flask run --debug

 

예를 들어보겠습니다. 아래와 같은 코드가 있다고 했을 때,

 

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return '<p>Hello, World!</p>' + a 	## 없는 변수 참조

 

이 상태로 Flask를 실행시킨다면 Internal Server Error가 발생합니다. 하지만 원인을 브라우저에서 알기 어렵습니다.

하지만 Debug mode를 켜면 브라우저에서 Not Found가 된 원인을 알려줍니다.

 

 


3. Route

URL에 어떤 값을 입력했을 때, 규칙에 맞는 function을 찾아 동작하는 것을 의미합니다.

3.1 Routing

지금까지는 address를 " / "만 할당했지만, 다른 address도 할당해 볼 수 있습니다.

@app.route('/bye')
def bye():
    return '<p>Goodbye, World!</p>'

 

이렇게 하면 " /bye "로 접속 했을 때에 return 되는 내용을 볼 수 있습니다.

 

 

뿐만 아니라 url에서 value를 가져올 수도 있습니다.

@app.route('/<name>')
def hello_name(name):
    return f'<p>Hello, {name}</p>'

 

url의 tester 단어를 읽어서 출력하였다.

하지만 이런 방식은 매우 위험합니다. name에 만약에 html tag를 삽입해 버린다면 어떻게 될까요?

url에 tag가 삽입되어 의도하지 않은 동작이 발생

 

3.2 HTML Escaping

이런 공격(xxs)을 방지하기 위해 외부에서 입력이 가능한 변수들을 escape 처리해야 합니다.

from flask import Flask
from markupsafe import escape

app = Flask(__name__)

@app.route('/<name>')
def hello_name(name):
    return f'<p>Hello, {escape(name)}</p>'

 

tag가 그대로 출력된다.

 

3.3 Variable Rules

이렇게 받아오는 변수들의 type에 따라 규칙을 정해줄 수 있습니다.

Type 내용
string 슬래시 없는 text
int 정수
float 소수
path 슬래시 포함 text
uuid uuid 문자열

 

간단하게 string, int, float의 예시로 routing 해보겠습니다.

from flask import Flask
from markupsafe import escape

app = Flask(__name__)

@app.route('/<string:value>')
def string_value(value):
    return f'<p>{escape(value)} is String.</p>'

@app.route('/<int:value>')
def int_value(value):
    return f'<p>{escape(value)} is Integer.</p>'

@app.route('/<float:value>')
def float_value(value):
    return f'<p>{escape(value)} is Float.</p>'

 

텍스트 예제
정수 예제
소수 예제

 

이렇게 url의 값에 따라 어느 route를 따라가게 할지 규칙을 정할 수 있습니다. 만약 규칙이 겹치는 경우가 있다면 먼저 정의된 함수 순서대로 동작하게 됩니다.

 

3.4 Unique URL, Redirection Behavior

app.route에 url을 작성할 때, 주소 마지막 문자가 " / " 유무에 따라 동작이 달라집니다.

@app.route('/projects/')	## /projects는 /propjects/로 redirect
def projects():
    return 'The project page'

@app.route('/about')		## /about만 허용. /about/은 허용하지 않음
def about():
    return 'The about page'

 

/about/은 접속할 수 없다.

3.5 URL Building

url_for()이라는 함수를 사용해서 URL을 Build 할 수 있습니다. url_for는 함수의 이름을 input으로 받아 URL을 output합니다. url_for의 장점은 URL을 하드코딩하지 않고 절대경로를 항상 얻을 수 있다는 점입니다.

from flask import Flask, url_for
from markupsafe import escape

app = Flask(__name__)

@app.route('/<string:value>')
def string_value(value):
    return f'<p>{escape(value)} is String.</p>'

@app.route('/<int:value>')
def int_value(value):
    return f'<p>{escape(value)} is Integer.</p>'

@app.route('/<float:value>')
def float_value(value):
    return f'<p>{escape(value)} is Float.</p>'

@app.route('/projects/')
def projects():
    return 'The project page'

@app.route('/about')
def about():
    return 'The about page'

with app.test_request_context():
    print(url_for('string_value', value="test_string"))
    print(url_for('int_value', value="123"))
    print(url_for('float_value', value="1.23"))
    print(url_for('projects', value="test_string"))
    print(url_for('about', value="test_string"))

 

함수를 실행하기 위한 url

3.6 HTTP Methods

HTTP Method로는 GET, POST 등 여러 가지가 있지만 기본적으로 @app.route는 GET method로만 접근이 가능합니다. 만약 @app.route로 GET, POST를 둘 다 요청받고 싶다면 다음과 같이 작성할 수 있습니다.

from flask import Flask, url_for, request
from markupsafe import escape
import requests
import json


app = Flask(__name__)

@app.route('/get_and_post', methods=['GET', 'POST'])
def get_and_post():
    if request.method == "POST":
        return "<p>POST</p>"
    elif request.method == "GET":
        return "<p>GET</p>"

@app.route('/post_request', methods=['GET'])
def post_request():
    data = {
        "key1": "value1",
        "key2": "value2",
    }
    ## target url은 각자의 flask server ip를 작성합니다.
    target_url = "xxx.xxx.xxx.xxx:port/get_and_post"
    result = requests.post(target_url, data=json.dumps(data)).text
    return result

이렇게 되면 /get_and_post로 접속할 시 <p>GET</p>를 출력하게 되고, /post_request 로 접근할 시 /get_and_post url로 post request하여 <p>POST</p>를 출력하게 됩니다.

get_and_post가 GET method로 처리했다.
get_and_post가 POST method로 처리했다.

만약 GET, POST를 따로 처리하고 싶으면 app.get과 app.post로 나눌 수 있습니다.

@app.get('/login')
def login_get():
    return show_the_login_form()

@app.post('/login')
def login_post():
    return do_the_login()

4. Rendering Templates

지금까지의 예제는 return "<tag/>" 였지만 HTML file을 rendering 해야 하는 것이 보통입니다.

from flask import render_template

@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)

 

그리고 hello.html에는 다음과 같이 적습니다. template의 문법은 html과 Jinja2을 사용하고 있습니다. 

<!DOCTYPE html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}

 

*Jinja란? python용 web tamplate engine으로, 템플릿이 랜더링 될 때 변수, 표현식, 논리제어 등을 할 수 있도록 해줍니다. Jinja가 생소하신 분들은 아래를 참고해 주세요. Jinja 기본 사항을 보여주는 예제입니다.

<!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>

 

 

Template Designer Documentation — Jinja Documentation (3.1.x)

Template Designer Documentation This document describes the syntax and semantics of the template engine and will be most useful as reference to those creating Jinja templates. As the template engine is very flexible, the configuration from the application

jinja.palletsprojects.com

 

 

그리고 hello.html을 "templates" 폴더에 넣습니다. 이것은 기본값입니다.

 

그러면 /hello에 접속했을 때 다음과 같은 화면을 볼 수 있습니다.

5. Static Files

templates에 html도 넣었으니 이제는 css 파일도 적용해 봅시다. static file은 "static" folder에서 관리됩니다.

먼저 hello.css파일을 다음과 같이 작성해 봅시다.

h1 {
  background-color: grey;
}

 

그리고 hello.html 파일을 다음과 같이 작성해 봅시다.

<!DOCTYPE html>
<link rel="stylesheet" href="{{ url_for('static', filename='hello.css') }}" />
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}

 

그리고 파일들의 위치를 그림과 같이 지정해 봅시다.

 

그러면 다음과 같은 화면을 볼 수 있습니다.

6. Accessing Request Data

웹어플리케이션의 경우 클라이언트가 서버로 전송하는 데이터에 반응하는 것이 중요합니다. Flask에서 이 정보는 전역 요청 객체(Global request object)에 의해 제공됩니다. 어떻게 객체는 Global하게 되고 Flask는 Thread 안전성을 유지할까요? 답은 Context Local입니다.

6.1 Context Local (내용이 좀 어렵습니다 어려우신 분들은 skip)

Flask의 특정 객체(ex : application, request 등)들은 Global하지만 일반적이지는 않습니다. 이 객체들은 Local Context의 대리자(Proxy)입니다. 

 

이렇게 말하면 좀 어렵죠...? 그럼 다음 예제를 봅시다.

from flask import Flask, request
import requests
import json

app = Flask(__name__)

@app.route('/get_and_post/1', methods=['GET', 'POST'])
def get_and_post():
    if request.method == "POST":
        return "<p>POST</p>"
    elif request.method == "GET":
        return "<p>GET</p>"

@app.route('/get_and_post/2', methods=['GET', 'POST'])
def get_and_post2():
    if request.method == "POST":
        return "<p>POST2</p>"
    elif request.method == "GET":
        return "<p>GET2</p>"

@app.route('/post_request/<int:number>', methods=['GET'])
def post_request(number):
    data = {
        "key1": "value1",
        "key2": "value2",
    }
    target_url = f"{server_address}/get_and_post/{number}"
    
    result = requests.post(target_url, data=json.dumps(data)).text
    return result

 

 

보시면 2개의 url에 대해서 get, post를 둘 다 처리한 모습을 볼 수 있습니다. 하지만 이 모든 동작은 "request"에 의해 동작되었습니다. 그러면 request는 전역 객체인가요?

 

아닙니다. request는 proxy역할을 할 뿐, 전역객체가 아닙니다. 실제 요청값(Context)은 Global 객체인 Request Context에 Stack형태로 저장되어 있습니다. 그리고 context가 request로 push되면 비로소 request를 사용할 수 있게 됩니다.

https://flask.palletsprojects.com/en/2.2.x/reqcontext/

 

The Request Context — Flask Documentation (2.2.x)

The request context keeps track of the request-level data during a request. Rather than passing the request object to each function that runs during a request, the request and session proxies are accessed instead. This is similar to The Application Context

flask.palletsprojects.com

 

위의 예제에서 함수가 실행되면 context가 push되며 request가 활성화 되고 활용하게 됩니다. 그리고 함수가 종료되면 request는 제 역할이 끝나고 사라집니다. 그렇다면 context push 없이 request를 호출하면 어떻게 될까요?

from flask import Flask, request


app = Flask(__name__)

print(request.method) 	## RuntimeError: Working outside of request context.

 

RuntimeError가 발생합니다. request에 push해줄 context가 없는데 request를 호출해서 생긴 Error입니다.

Unit test를 할 경우에는 request가 없는 상태에서 test가 진행되버리는 문제가 생길 수 있습니다. 이런 점을 방지하기 위해서 test_request_context() 함수를 사용합니다.

from flask import request

with app.test_request_context('/hello', method='POST'):
    # now you can do something with the request until the
    # end of the with block, such as basic assertions:
    assert request.path == '/hello'
    assert request.method == 'POST'

 

이렇게 하면 /hello의 url, POST method를 가진 request가 생성되고, with문 안에서 활성화 됩니다. 실제 공식 문서에서도 이게 가장 쉬운 방법의 unit test 방법이라고 소개하고 있습니다.

 

6.2 The Request Object

 

Flask에서 사용하는 request는 이곳에서 더 자세하게 알아볼 수 있습니다.

 

API — Flask Documentation (2.3.x)

API This part of the documentation covers all the interfaces of Flask. For parts where Flask depends on external libraries, we document the most important right here and provide links to the canonical documentation. Application Object class flask.Flask(imp

flask.palletsprojects.com

 

flask module에서 request를 불러올 수 있습니다.

from flask import request

 

request method는 method attribute을 통해 사용할 수 있습니다. form data에 접근하려면 form을 통해 사용할 수 있습니다. 

이에 대한 예시는 다음과 같습니다.

@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)

 

'username', 'password'를 form에서 key값으로 데이터를 얻어내고 있는데, 만약 key값이 없으면 KeyError가 raise되고 http 400오류페이지(잘못된 요청)이 뜹니다.

 

url(GET 요청)에 있는 parameter에 접근하려면 args atttribute를 사용할 수 있습니다. 마치 python dictionary의 key값에 따른 value를 가져올 때와 같습니다.

searchword = request.args.get(key='key', default='')

 

사용자가 URL을 변경할 수 있으며, 이 경우에는 http 400 오류 페이지를 보여주는 것은 유저 친화적이지 않기 때문에 get attritbute를 사용해서 keyError가 되지 않도록 하는 좋습니다.

 

6.3 File Uploads

Flask에서는 upload된 file을 쉽게 다룰 수 있습니다. HTML 양식에 enctype="multipart/form-data" 속성을 설정하는 것을 잊으면 안됩니다. 그렇지 않으면 브라우저에서 데이터를 전송하지 않습니다.

 

업로드된 파일은 메모리 또는 파일 시스템의 임시 위치에 저장됩니다.Request method의 file attribute을 확인하여 해당 파일에 접근 할 수 있습니다. 업로드된 각 file은 해당 dictionary에 저장됩니다. 표준 python fiile object처럼 작동하지만 서버의 file system에 해당하는 save() method도 있습니다.

from flask import request

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        f = request.files['the_file']
        f.save('/var/www/uploads/uploaded_file.txt')
    ...

 

파일이 application에 업로드 되기 전에 클라이언트에서 파일이 어떻게 되었는지 알고 싶다면 filename attribute에 접근할 수 있습니다. 그러나 이 값은 위조될 수 있으므로 이 값을 신뢰할 수는 없습니다. 클라이언트의 파일명을 사용하여 서버에 파일을 저장하려면 Werkzeug에서 제공하는 secure_filename() method를 통해 전달해야 합니다.

from werkzeug.utils import secure_filename

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        file = request.files['the_file']
        file.save(f"/var/www/uploads/{secure_filename(file.filename)}")
    ...

 

더 나은 예제는 https://flask.palletsprojects.com/en/2.3.x/patterns/fileuploads/ 를 통해 확인할 수 있습니다.

import os
from flask import Flask, flash, request, redirect, url_for
from werkzeug.utils import secure_filename

UPLOAD_FOLDER = '/path/to/the/uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER


def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # check if the post request has the file part
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        # If the user does not select a file, the browser submits an
        # empty file without a filename.
        if file.filename == '':
            flash('No selected file')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('download_file', name=filename))
    return '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>
    '''

 

6.4 Cookies

쿠키에 접근하려면 cookies attribute를 사용할 수 있습니다. 쿠키 설정은 response objects의 set_cookie method를 사용하면 됩니다. request objects의 cookies attribute는 client가 전송하는 모든 쿠키가 포함된 dictionary 형태입니다. 세션을 사용하려면 쿠키를 직접 사용하지 말고 쿠키 위에 보안이 추가된 Flask Session을 사용해야 합니다.

 

쿠기를 읽으려면

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

 

쿠키는 응답 객체에 설정된다는 점을 고려해야 합니다. 일반적으로 View function에서 문자열을 반환하기 때문에 Flask가 이를 response objects로 변환합니다. 명시적으로 그렇게 하려면 make_response() 함수를 사용한 다음 수정하면 됩니다.

 

때로는 응답 객체가 아직 존재하지 않는 지점에 쿠키를 설정하고 싶을 수도 있습니다. 이는 Deferred request Callbacks(지연된 요청 콜백) 패턴을 활용하면 가능합니다. 

 

Deferred Request Callbacks — Flask Documentation (2.3.x)

Deferred Request Callbacks One of the design principles of Flask is that response objects are created and passed down a chain of potential callbacks that can modify them or replace them. When the request handling starts, there is no response object yet. It

flask.palletsprojects.com

 

더욱 자세히 알고싶다면 Response 문서도 참고 바랍니다.

 

Quickstart — Flask Documentation (2.3.x)

Quickstart Eager to get started? This page gives a good introduction to Flask. Follow Installation to set up a project and install Flask first. A Minimal Application A minimal Flask application looks something like this: from flask import Flask app = Flask

flask.palletsprojects.com

 


* reference

https://flask.palletsprojects.com/en/2.3.x/quickstart/#about-responses

https://flask.palletsprojects.com/en/2.3.x/patterns/fileuploads/

https://flask.palletsprojects.com/en/2.3.x/api/#flask.Request

https://jinja.palletsprojects.com/en/3.1.x/templates/

https://flask.palletsprojects.com/en/2.3.x/quickstart/

 

'Python > Library' 카테고리의 다른 글

[Redis] - Tutorial with Python  (4) 2024.09.21
[Alembic] - Tutorial  (7) 2024.09.20
[Django] REST framework tutorial - Requests and Responses  (0) 2023.06.23
[Django] REST framework tutorial - Serialization  (0) 2023.06.23
[Flask] - Tutorial  (0) 2023.04.17