Notice
Recent Posts
Recent Comments
05-21 07:17
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Byeol Lo

Flask - 1. Quick start 본문

BackEnd/Flask

Flask - 1. Quick start

알 수 없는 사용자 2024. 2. 8. 14:57

웹서버 생성

이제 웹서버를 열어야 하는데 main.py에서 다음을 입력하자. (flask.py 로 이름을 지으시면 안됩니다.)

from flask import Flask

app = Flask(__name__)

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

 Flask 클래스는 WSGI 어플리케이션의 인스턴스를 반환시키는 클래스이다. 이제 이 서버에 route() 데코레이터를 통해 시그널을 걸어주고, 이는 URL("/")이 함수를 실행시키도록 시그널을 걸어준 것이다. 해당 시그널이 들어오면 hello_world()가 실행되고 반환된 값(HTML content)을 서버 인스턴스인 app이 HTTP 프로토콜을 사용하여 네트워크로 보낸다.

 

웹서버 구동

 이제 서버가 될 준비가 끝났으니 실행시켜주기만 하면 된다.

# python -m flask
flask --app main run

커맨드에서 위의 코드를 실행하자.

# 파일명이 app.py 또는 wsgi.py일 때
flask run

 파일명을 app.py, wsgi.py로 했다면 간단하게 위와 같이 치면 실행된다. 보통 이런 실행은 test만 하기에 적절한 실행이다. 만약에 deploy를 하고 싶다면 다음 링크를 확인하자.

https://flask.palletsprojects.com/en/3.0.x/deploying/

 

Deploying to Production — Flask Documentation (3.0.x)

Deploying to Production After developing your application, you’ll want to make it available publicly to other users. When you’re developing locally, you’re probably using the built-in development server, debugger, and reloader. These should not be us

flask.palletsprojects.com

 

 실행이 됐다면 http://127.0.0.1:5000/ 으로 내 컴퓨터에서 웹에 링크를 쳐서 들어갈 수 있다. 이때 웹이 제대로 동작하지 않는다면 우리는 Debugging을 해야한다.

 

Debug Mode

 플라스크는 디버그 모드를 통해 웹사이트에서 보기 쉽게 할 수 있는데,

flask --app hello run --debug

 이도 간편하게 커맨드에 --debug 옵션을 추가하여 할 수 있다. 하지만 이 디버깅 방식은 보안적으로 문제가 있는데, browser에게 임의의 Python code를 실행하도록 허락해줘서 자칫 보안 문제가 발생한다. 따라서 이를 배포 서버에서는 하지말고 개인 서버에서 동작하도록 하자.

 

HTML Escaping

 HTML 문서를 돌려줄때, 사용자가 제공한 어떤 output에 렌더링 될 수 있는 값들은 해킹 공격인 injection attack에 취약하다. 따라서 이는 반드시 피해야하는데(escaping) HTML 템플릿이 Jinja와 함께 사용된다면 이를 자동적으로 escaping 할 수 있도록 한다.

from markupsafe import escape

@app.route("/<name>")
def hello(name):
    return f"Hello, {escape(name)}"

 위와 같이 escape를 할 수 있고, 간결성을 위해 여기서는 대부분 예에서 생략되지만, 신뢰할 수 없는 데이터를 어떻게 사용하고 있는지 잘 컨트롤 해야한다. 만약에 return으로 〈script>alert("hello")</script>을 주고 접속을 하면 alert가 실행됨을 알 수 있다. 따라서, 이를 피하기 위해 다음과 같이 사용한다.

from flask import Flask
from markupsafe import escape

app = Flask(__name__)

@app.route('/')
def hello_world():
    msg = '<script>alert("hello")</script>'
    return f"{escape(msg)}"

 

Routing

 meaningful URL을 통해 클라이언트들에게 많은 도움을 줄 수 있는데, 해당 페이지가 어떤건지 URL로 알릴 수 있음으로써 사용자의 웹 페이지를 다시 방문할 수도 있고, url을 통한 간편한 조작도 할 수 있게 된다.

@app.route('/')
def index():
    return 'Index Page'

@app.route('/hello')
def hello():
    return 'Hello, World'

함수를 url에 바인딩(대응)시켜서 url을 통해 함수를 실행할 수 있도록 한다. 위에서 했던 것들이다.

 

Variable Rules

 routing을 할 때에 원하는 웹페이지 만큼 바운딩 할 함수를 만드는 것은 함수를 몇 백개 만들 수도 있는 개발자를 피곤하게 한다. 따라서 "<variable_name〉"의 형식을 사용해서 함수의 인자로 넣어줄 수 있다.

@app.route("/<index>")
def show_pages(index):
    return f"Index: {escape(index)}"

# Type도 명시 가능
@app.route("/<int:id>")
def show_id(id):
    return f'Post {id}'

@app.route("/path/<path:subpath>")
def show_subpath(subpath):
    return f'Subpath: {escape(subpath)}'

지원되는 변환 타입은 다음과 같다.

슬래쉬가 들어가서 랜더링을 하고 싶다면 path를 사용해야 함을 잊지 말자.

 

Unique URLs / Redirection Behavior

 보통 표준 URL은 맨 뒤에 /가 있는데, 만약 이 슬래쉬가 없다면 플라스크는 이를 redirect하여 /projects/로 만든다(신경 안써도 된다). 또한, URL을 바인딩 할 때 들어가는 인자로 "about"의 엔드포인트에는 후행 슬래시가 없어야 하는데, 이는 서버에서 특정 파일에 접근하도록 하기 위함이다. 그래서 "/about/" 과 같은 것은 404 Not Found 에러를 불러일으키는데, 이 URL은 프로그래밍 언어의 예약어 처럼 Flask가 이미 쓰고 있다고 생각하자.

 

URL Building

url_for()을 사용하면 그 url에 바인딩 된 특정 함수를 실행시키도록 할 수 있다. 이를 통해 build를 할 수 있다. 첫번째 인자는 바인딩 된 함수명, 두번째 부터는 keyword arguments, unknown variable은 쿼리 파라미터로 가도록 되어 있다.

from flask import url_for

# flask 예제
@app.route('/')
def index():
    return 'index'

@app.route('/login')
def login():
    return 'login'

@app.route('/user/<username>')
def profile(username):
    return f'{username}\'s profile'

with app.test_request_context():
    print(url_for('index'))
    print(url_for('login'))
    print(url_for('login', next='/'))
    print(url_for('profile', username='John Doe'))
/
/login
/login?next=/
/user/John%20Doe

 

HTTP Methods

 웹 어플리케이션은 HTTP 메소드들을 사용하여 URL에 접근을 한다. 이때, 서로 다른 HTTP 메소드를 사용할 수 있는데, 가령 GET 방식과 POST 방식은 서로 다르기 때문에 같은 url에서도 동작 방식을 달리 해야한다. 기본적으로 Flask에서는 route에서 GET 요청만 받도록 되어 있고, decorator에 인자로 methods를 사용해서 원하는 HTTPS 메소드로 제어 가능하다.

from flask import request

# Flask 예제
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        return do_the_login()
    else:
        return show_the_login_form()

위와 같은 방식은 다음과 동일하다.

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

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

 

Static Files

 동적 웹 어플리케이션들은 정적 파일들이 필요한데, static이라는 이름을 가지는 폴더를 생성하고, 그저 거기에 정적 파일들을 올리면 된다. 이때, 정적 파일을 위한 URL을 생성하려면 엔드포인트로 'static'을 쓰면 된다.

url_for('static', filename='style.css')

 

여기서는 static 폴더에 style.css를 들고오게 된다.

 

Rendering Templates

 파이썬에서 HTML을 다루는 것은 클라이언트의 요청을 포함하기 때문에 이를 다루기 위해 HTML escaping도 해야하는 번거로운 작업이 필요하다. 따라서 Jinja2 템플릿 엔진을 이용해서 자동으로 구성할 수 있다. 이때 템플릿은 텍스트 파일 형태를 생성하기 위해 사용되어 진다(자세하게는 Jinja2는 HTML과 markdown, plain text(email을 위함) 등을 생성할 수 있다). 이런 템플릿을 렌더링 하기 위해서는 render_template() 함수를 사용할 수 있다. 인자로는 템플릿 이름과 keyword arguments로 Jinja2에게 전달하려는 변수를 제공해주기만 하면 된다.

from flask import render_template

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

 그럼 이 템플릿이 저장되는 곳이 어딘지 알아야 하는데, Flask에서는 templates라는 폴더를 보면서 템플릿들에 접근한다. 따라서 해당 파일에서 우리가 쓰는 템플릿들을 넣으면 된다. 이때 두가지 형태의 templates를 보는 방식이 있는데,

 다음과 같다고 한다. 만약에 실행되는 어플리케이션이 module 형식이라면 첫번째, 실행되는 어플리케이션이 package 형식이라면 두번째 파일 구조를 따르면 된다. Jinja2 템플릿은 나중에 따로 다루겠지만, 다음 예제를 보면서 간단하게 살펴보자.

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

템플릿 하나를 가져왔다. 이 템플릿 안에서는 config, request, session, g 의 개체와 url_for(), get_flashed_messages() 함수를 사용할 수 있다. 템플릿은 특히 Inheritance를 사용하면 매우 생산적이고 유용한데, 다음 링크를 참고하자.

https://flask.palletsprojects.com/en/3.0.x/patterns/templateinheritance/

 

Template Inheritance — Flask Documentation (3.0.x)

Template Inheritance The most powerful part of Jinja is template inheritance. Template inheritance allows you to build a base “skeleton” template that contains all the common elements of your site and defines blocks that child templates can override. S

flask.palletsprojects.com

 기본적으로 이 템플릿 상속이라는 것은 특정 페이지 내에서의 elements(header, footer, ...)를 유지시키게 한다. 여기서 Jinja2는 자동적으로 escaping이 활성화되어 있어서 HTML을 렌더링할때, 변수들에게 자동적으로 escape()가 적용되게 된다. 따라서 이를 만약에 적용시키고 싶지 않다면 다음을 사용하여 해제할 수 있다.

# 커맨드
python
>>> from markupsafe import Markup

# Flask 예제
>>> Markup('<strong>Hello %s!</strong>') % '<blink>hacker</blink>'
# Markup('<strong>Hello &lt;blink&gt;hacker&lt;/blink&gt;!</strong>')
>>> Markup.escape('<blink>hacker</blink>')
# Markup('&lt;blink&gt;hacker&lt;/blink&gt;')
>>> Markup('<em>Marked up</em> &raquo; HTML').striptags()
# 'Marked up » HTML'

 

Accessing Request Data

 클라이언트가 서버에게 보내는 데이터들은 웹에 반응하는데 있어 굉장히 중요하다. 이 데이터들은 global request 객체에 의해 제공되게 되되는데, 이는 전역으로 선언되어 있기 때문에 기본적으로 스레드 안전성에 문제가 될 수 있다(동시 요청, ...). 따라서 파이썬은 기본적으로 멀티스레딩을 지원하지만, 이러한 스레드 간의 충돌을 방지하기 위해 적절한 대책이 필요하게 된다.

 Flask에서는 이런 문제를 해결하기 위해 Context Locals이라는 걸 도입했는데, 이는 전역 객체인 것처럼 동작은 하지만, 실제로는 현재 실행 컨텍스트에 로컬한 객체에 대한 프록시로 작동하는 것이다. 즉, 플라스크에서 여러 요청을 처리하면서 데이터의 일관성과 보안을 유지하고, 다수의 사용자 요청을 처리하는데 중요한 역할을 한다.

 이게 의미하는 바가 굉장히 중요한데, 요청 객체(request object)에 의존하는 코드를 작성할 때, 유닛 테스트 중에 문제가 발생할 수 있는데 유닛 테스트 중에는 실제 HTTP 요청이 없을 수도 있고, 이는 요청 객체가 없다는 뜻이므로 코드가 실행되지 않을 수도 있다는 것이다. 그래서 이에 대한 해결책으로 요청 객체를 직접 생성하고 컨텍스트에 바인딩을 하는 것. 여기서 Flask에서는 test_request_context() 컨텍스트 매니저를 사용하는 것이고 with 문과 함께 사용하면 테스트 요청을 바인딩하여 상호 작용할 수 있도록 해준다.

from flask import request

# Flask 예제
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'

 

with app.request_context(environ):
    assert request.method == 'POST'

 

The Request Object

 request 객체는 간단하게만 살펴보고 후에 상세히 다루도록 한다. 우선 클래스를 들고 오는 것 부터

from flask import request

와 같이 들고 올 수 있다.

@app.route('/login', method=['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'
    
    return render_template('login.html', error=error)

위의 예제를 보면 login 엔드 포인트로 post, get 방식의 요청이 들어온 것에서 이 요청의 데이터(POST 또는 PUT 요청에서 전송된 데이터) 에 액세스하기 위해 form의 속성을 사용하여 데이터에 접근이 가능하게 된다. 또한 method를 통해 해당 HTTP 프로토콜의 데이터가 어떤 형식인지 확인을 하고 GET 방식은 따로 빠지는 것을 볼 수 있다.

 만약 form에서 없는 키를 넣으면 KeyError 를 발생시키고, 이 표준 KeyError와 같이 catch 가능하지만, 그렇게 코딩을 안했다면 HTTP 400 Bad Request error page를 띄우게 된다.

 또 request에 url query 데이터들도 접근 할 수 있는데,

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

와 같이 접근이 가능하다. 이때, 클라이언트는 URL을 변경할 수 있고, 이 경우에 get 또는 catch를 사용하여 URL 매개 변수에 접근하도록 코딩하는 것이 좋다.

 

File Upload

 Flask에서 파일들을 업로드를 다루는 것은 매우 쉬운 일인데, HTML 에서 enctype="multipart/form-data" 속성을 쓴다면 브라우저가 파일을 전송할 수 있게 된다. 이 파일들은 메모리나 파일 시스템의 임시 저장소에 저장되게 되는데, 우리 프로그래머 입장에서는 request의 files 만 보면 된다.

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')
        ...

 여기서 더 나아가 파일 이름을 보고 싶을 수 있는데 다음과 같이 Werkzeug가 제공하는 secure_filename()를 사용하면 된다.

from werkzeug.utils import secure_filename

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

 

Cookie

 쿠키도 비슷하다. 전역 객체 request를 통해 cookies attribute를 불러오고 get을 사용하자.

읽기

from flask import request

@app.route('/')
def index():
    username = request.cookies.get('username')

저장

from flask import make_response

@app.route('/')
def index():
    resp = make_response(render_template(...))
    resp.set_cookie('username', 'the username')
    return resp

쿠키는 응답 객체에서 설정이 되는데, 보통 view function(여기서는 render_template)에서 return을 할때 string의 형태로 return을 하게 되고, Flask는 이 문자열을 자동으로 응답 객체로 변환한다. Flask는 그것(리턴된 string)을 응답으로 간주하고 클라이언트에게 반환하게 된다.

 하지만 위의 예제에서는 이를 명시적으로 응답 객체를 생성하고 조작하기 위해 make_response를 사용하였고, 이를 통해 응답 객체를 수정할 수 있게 되기 때문에 더 복잡한 응답을 생성하고 필요한 쿠키를 설정할 수 있게 된다.

 또한 때로는 웹서버 어플리케이션 관리자들이 response object가 존재하기도 전에 cookie를 설정하고 싶을 때가 있는데, 이는 다음을 참고하자.

https://flask.palletsprojects.com/en/3.0.x/patterns/deferredcallbacks/

 

Deferred Request Callbacks — Flask Documentation (3.0.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

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

 

Quickstart — Flask Documentation (3.0.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

 

Redirects and Errors

 유저의 endpoint를 다른 곳으로 redirect 시키고 싶을 때, redirect() 함수를 사용하여 쓸 수 있다.

from flask import abort, redirect, url_for

# Flask 예제
@app.route('/')
def index():
    return redirect(url_for('login'))

@app.route('/login')
def login():
    abort(401)
    this_is_never_executed()

만약 홈페이지가 해당 상태에서 'login'으로 redirect를 하기를 원한다면 위와 같이 index()함수를 '/'에 바인딩 시키면 되고, 이는 다시 /login에 바인딩 된 함수로 가서 실행이 되는데 abort(401)으로 액세스 거부의 오류 페이지를 띄우도록 하고 있다. 이런 오류 페이지 같은 것을 사용자 정의를 하려면 errorhandler() 데코레이터를 사용할 수 있다.

from flask import render_template

@app.errorhandler(404)
def page_not_found(error):
    return render_template('page_not_found.html'), 404

위와 같이 return의 마지막 404는 해당 페이지의 상태 코드가 404임을 Flask에게 알려주기 위한 용도이다(기본적으로는 200으로 되어 있다).

 

Responses

 view function의 return value는 response object의 call에 의해 자동적으로 바뀐다는 것을 안다. 만약 return value가 string이라면 그것은 문자열을 응답 본문으로 가지고, 200(default)의 상태 코드와 text/html MIME 타입을 가진 응답 객체로 변환되게 되고, view function의 return이 dict나 list라면 이를 response로 바꿀 수 있는  jsonify()가 호출되게 된다. 밑을 참고하자.

  1. 올바른 유형의 response object가 반환되면 view에서 직접 반환된다.
  2. string인 경우, 해당 데이터와 기본 매개변수를 사용하여 response object가 생성된다.
  3. string이나 byte를 반환하는 iterator 또는 generator인 경우 스트리밍 응답으로 처리된다.
  4. dict 또는 목록인 경우 응답 객체는 jsonify()를 사용하여 생성된다.
  5. 튜플이 반환되면 튜플의 항목이 추가 정보를 제공할 수 있는데, 이 튜플은 (response, status), (response, headers), (response, status, headers)의 형태로 올 수 있다. 값들은 status 를 재정의 하고 headers는 추가적인 header values이 있는 list 또는 dictionary 형태이다.
  6. 만약 이러한 값들이 아니라면, Flask는 return value가 유효한 WSGI application이라고 가정할 것이고, 이를 응답 객체로 변환시킨다.

 

from flask import render_template

@app.errorhandler(404)
def not_found(error):
    return render_template('error.html'), 404

 

from flask import make_response

@app.errorhandler(404)
def not_found(error):
    resp = make_reponse(render_template('error.html'), 404
    resp.headers['X-Something'] = 'A value'
    return resp

 

APIs with JSON

 API를 작성할 때 흔한 형태는 JSON이다. 이 또한 Flask에서 작성하기 쉽다. 그냥 dict, list로 반환시켜주면 끝이다(jsonify()가 실행되기 때문).

@app.route("/me")
def me_api():
    user = get_current_user()
    return {
        "username": user.username,
        "theme": user.theme,
        "image": url_for("user_image", filename=user.image)
    }

@app.route("/users")
def users_api():
    users = get_all_users()
    return [user.to_json() for user in users]

 이는 직렬화되어 있는 어떤 것이든 jsonify()를 통해 JSON 데이터 타입으로 바뀔 수 있고, 이 말은 JSON serializable하면 JSON 데이터로 넣을 수 있다는 것이다.

 데이터베이스 모델과 같은 복잡한 유형의 경우 먼저 직렬화 라이브러리를 사용하여 데이터를 유효한 JSON 유형으로 변환하는 것이 좋다.

 

Session

 request object 외에도 session object가 있다. 이는 한 요청에서 다음 요청까지의 사용자에 대한 특정 정보를 저장할 수 있는 객체인데, 세션은 쿠키에서 구현이 되어지고, 암호화를 사용하여 저장되어 진다. 이것은 사용자가사용된 비밀 키를 알지 않는 한 쿠키의 내용을 볼 수는 있지만 수정할 수는 없다는 것이다.

from flask import session

app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'

# Flask 예제
@app.route('/')
def index():
    if 'username' in session:
        return f'Logged in as {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():
    session.pop('username', None)
    return redirect(url_for('index'))

 

Message Flashing

 좋은 어플리케이션과 UI는 모든 피드백을 제공한다. 이를 위해서 플라스크는 flashing system을 통해 클라이언트에게 간단한 피드백을 제공한다. 플래싱 시스템을 사용하면 기본적으로 요청이 끝날 때 메시지를 녹음하고 다음(그리고 다음 요청에서만) 메시지에 액세스할 수 있다. 이는 일반적으로 메시지를 노출하기 위해 레이아웃 템플릿과 결합된다. 다음 링크를 확인한다.

https://flask.palletsprojects.com/en/3.0.x/patterns/flashing/

 

Message Flashing — Flask Documentation (3.0.x)

Message Flashing Good applications and user interfaces are all about feedback. If the user does not get enough feedback they will probably end up hating the application. Flask provides a really simple way to give feedback to a user with the flashing system

flask.palletsprojects.com

 

그 외의 Logging, Hooking, Flask Extensions, Deploying to a Web Server 이 있지만, 이들은 실제 프로젝트를 할 때 하는 것이 좋을 것 같다.

'BackEnd > Flask' 카테고리의 다른 글

Flask - 5. Tutorial Blueprint and View  (1) 2024.02.14
Flask - 4. Define and Access the Database  (1) 2024.02.12
Flask - 3. Tutorial Application Setup  (0) 2024.02.12
Flask - 2. Tutorial Project Layout  (0) 2024.02.08
Flask - 0. 환경 세팅  (1) 2024.02.06
Comments