간단히 정리한 Flask 강좌

간단히 정리한 Flask 강좌

flask quickstart 문서를 기반으로 실행가능한 예제코드와 개념 설명을 보완하여 작성했는데 40페이지 정도 됩니다.
https://flask.palletsprojects.com/en/stable/quickstart/

2021.10. 09. 최초작성
2025.07. 13. 다시 새로 작성

Flask 설치

Flask는 Python 3.9 이상을 지원하고 있습니다. miniconda 또는 uv를 사용하여 Python 가상 환경을 생성한 후 진행하는 것을 권장합니다.

miniconda에서 python 가상환경을 생성 후, flask 패키지를 설치하면 다른 개발 환경과 별도로 파이썬 패키지를 관리할 수 있어 좋습니다.

$ conda create -n flask python=3.13
$ conda activate flask
$ pip install Flask

miniconda 설치 방법은 다음 포스트를 참고하세요. miniconda 설치 후, Visual Studio Code에서 연계하여 사용하는 방법을 다루고 있습니다.

Visual Studio Code와 Miniconda를 사용한 Python 개발 환경 만들기( Windows, Ubuntu, WSL2)
https://webnautes.kr/visual-studio-codewa-minicondareul-sayonghan-python-gaebal-hwangyeong-mandeulgi-windows-ubuntu-wsl2/

최소한의 애플리케이션

최소한의 flask 애플리케이션은 다음과 같습니다.

# Flask 클래스를 임포트합니다.
from flask import Flask

# Flask 클래스의 인스턴스를 생성합니다. 이 인스턴스가 WSGI 애플리케이션이 됩니다. (1)
app = Flask(__name__)

# route 데코레이터를 사용하여 어떤 URL로 접속했을 때 hello_world 함수를 실행해야 한다는 걸 Flask에게 알려줍니다.
@app.route("/")
def hello_world():

    # 사용자가 웹브라우저에서 볼 수 있는 내용을 리턴합니다. 
    # 문자열 안에 HTML 코드가 있으면 웹브라우저가 그것을 웹페이지로 보여줍니다.
    return "<p>Hello, World!</p>"

(1) Flask 애플리케이션을 생성할 때 첫 번째 인수로 애플리케이션의 모듈 또는 패키지 이름을 전달해야 하며, 대부분의 경우 __name__ 변수를 사용하는 것이 편리합니다. 이 인수는 Flask가 템플릿과 정적 파일 같은 리소스들의 위치를 찾기 위해 필요한 정보로, Flask는 이 모듈의 위치를 기준점으로 삼아 templates/ 폴더와 static/ 폴더를 자동으로 찾아 HTML 템플릿 파일과 CSS, JavaScript 등의 정적 파일에 접근할 수 있게 됩니다.

위 코드를 확장자가 py인 파일로 저장합니다. 여기에선 hello.py 이름으로 저장합니다. Flask 자체와 충돌할 수 있으므로 파일 이름을 flask.py로 하면 안됩니다.

다음처럼 터미널에서 실행할 수 있습니다. Visual Studio Code에서 실행할 경우 메뉴에서 View > Terminal을 선택하면 터미널이 열리는데 miniconda로 생성한 가상환경이 터미널에서 보여야합니다. 보이지 않는 경우 다시 시도해보면 보입니다.

flask --app hello run

실행 결과입니다. 출력된 로그에 보이는 http://127.0.0.1:5000를 클릭하거나 웹브라우저의 주소창에 붙여넣어 접속하면 웹페이지에 Hello, World! 문자열이 출력됩니다.

$ flask --app hello run
* Serving Flask app 'hello'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit

flask를 실행했던 터미널에서 Ctrl + C 를 눌러서 실행을 중지할 수 있습니다.

파일 이름이 app.py 이거나 wsgi.py인 경우에는 다음 명령으로 실행할 수 있습니다. --app를 사용하여 실행할 파이썬 코드의 이름을 flask에게 알려줄 필요가 없기 때문입니다.

flask run

$ flask run
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit

해당 포트를 다른 프로그램에서 이미 실행중이라면 다음과 같은 메시지가 보입니다. 5000번 포트를 다른곳에서 사용중이라는 메시지가 보이고 있습니다. 이 경우 해당 포트를 사용하는 프로그램을 종료하거나 flask에서 다른 포트를 사용해야 합니다.

$ flask run
* Debug mode: off
Address already in use
Port 5000 is in use by another program. Either identify and stop that program, or start the server with a different port.

다음처럼 python을 사용하여 실행할 수도 있습니다.

$ python hello.py

다음처럼 코드를 수정해야 합니다. C언어의 main 함수에 해당하는 코드가 추가되었습니다.

from flask import Flask

app = Flask(__name__)

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

if __name__ == '__main__':
    app.run()

flask run 으로 실행해도 되지만 python으로 실행하면 애플리케이션 시작 과정을 직접 제어할 수 있고 커스텀 설정이나 초기화 로직을 추가하기 쉽습니다.

주의할 점은 이렇게 수정한 후 main에 초기화 함수를 추가해놓았을 경우 flask run으로 실행하면 main이 실행되지 않기 때문에 초기화 함수가 실행되지 않습니다.

외부에서 접속이 가능하게 하려면 다음처럼 수정하면 됩니다.

from flask import Flask

app = Flask(__name__)

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

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

외부에서 접속 가능한 주소인 http://192.168.0.47:5000이 더 추가된 것을 볼 수 있습니다. PC와 스마트폰이 같은 공유기에 연결되어 있다면 스마트폰에서 위 주소에 접속하여 테스트해볼 수 있습니다.

$ python hello.py
 * Serving Flask app 'hello'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://192.168.0.47:5000
Press CTRL+C to quit

디버그 모드(Debug Mode)

디버그 모드를 활성화하면 코드가 변경되면 서버가 자동으로 다시 로드되고 요청 중에 오류가 발생하면 브라우저에 대화형 디버거를 표시합니다.

디버거를 사용하면 웹브라우저에서 임의의 파이썬 코드를 실행할 수 있습니다. 핀(pin)으로 보호되지만 여전히 주요 보안 위험이 있습니다. 프로덕션 환경에서 개발 서버나 디버거를 실행하지 마세요.

디버그 모드를 사용하려면 --debug 옵션을 사용하세요.

flask --app hello run --debug

$ flask --app hello run --debug
* Serving Flask app 'hello'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with watchdog (inotify)
* Debugger is active!
* Debugger PIN: 979-726-227
* Detected change in '/media/webnautes/Windows-SSD/Users/jeong/Desktop/CODE_next/web/online_metric_learning.py', reloading
* Restarting with watchdog (inotify)
* Debugger is active!
* Debugger PIN: 979-726-227

실행후 코드에서 출력되는 문자열을 Hello, World!에서 안녕, 세상아!로 수정후 저장해봅니다. 서버는 다시 실행되지만 바로 바뀐 문자열이 웹브라우저에 반영되지는 않아서 F5를 눌러줘야 했습니다.

다시 파이썬 코드에서 오류 코드를 만들고 저장한 후, f5를 누르면 디버거가 웹브라우저에 에러를 표시됩니다.

앞에서 살펴본 python을 사용하여 실행하려면 다음처럼 코드에 디버그 옵션을 추가하면 됩니다.

app.py

from flask import Flask

app = Flask(__name__)

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

if __name__ == '__main__':
    app.run(debug=True)

다음 명령으로 실행하면 디버그 모드로 실행됩니다.

python app.py

HTML 이스케이핑(HTML Escaping)

Flask에서 기본적으로 반환하는 응답은 HTML 형식입니다. HTML을 반환할 때, 사용자가 입력한 값이 그대로 HTML에 포함되면 스크립트가 실행되는 XSS(교차 사이트 스크립팅) 공격에 노출될 수 있습니다. 이를 방지하기 위해 사용자 입력은 반드시 HTML 이스케이프 처리해야 합니다. 뒤에서 소개할 진자(Jinja)로 렌더링된 HTML 템플릿은 이 작업을 자동으로 수행합니다. 이스케이브 처리하면 스크립트 코드가 입력되는 경우 실행이 되지 않고 해당 코드가 문자열처럼 처리됩니다.

escape() 함수를 사용해 이스케이프 처리를 할 수 있습니다. 아래 예제에서는 사용자가 입력한 name 값을 escape() 함수로 감싸서 HTML 특수문자를 안전하게 처리합니다.

from flask import Flask, request
from markupsafe import escape

app = Flask(__name__)

# 사용자가 입력한 값이 저장되있는 name변수를 함수 escape로 감싸고 있습니다. 
@app.route('/', methods=['GET', 'POST'])
def hello():
    if request.method == 'POST':
        name = request.form.get('name', '')
        return f"Hello, {escape(name)}!"

    return '''
        <h2>이름을 입력하세요</h2>
        <form method="post">
            <input type="text" name="name" placeholder="이름 입력">
            <input type="submit" value="전송">
        </form>
    '''

if __name__ == '__main__':
    app.run()

실행합니다.

python app.py

다음 과정으로 테스트를 진행합니다.

1.웹브라우저에서 http://127.0.0.1:5000/로 접속합니다.
2.이름 입력란에 webnautes를 입력후 전송을 클릭하면 Hello, webnautes!가 출력됩니다. 입력한 이름이 출력되는 것을 볼 수 있습니다.
3.이름 입력란에 <script>alert("bad")</script>라는 코드를 제출한 경우 스크립트 코드인 Hello, <script>alert("bad")</script>!가 그대로 출력됩니다.

코드에서 escape을 제거한 다음 코드를 실행해봅니다.

app.py

from flask import Flask, request
from markupsafe import escape

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def hello():
    if request.method == 'POST':
        name = request.form.get('name', '')
        return f"Hello, {name}!"

    return '''
        <h2>이름을 입력하세요</h2>
        <form method="post">
            <input type="text" name="name" placeholder="이름 입력">
            <input type="submit" value="전송">
        </form>
    '''

if __name__ == '__main__':
    app.run()

실행합니다.

python app.py

1.웹브라우저에서 http://127.0.0.1:5000/로 접속합니다.
2.이름으로 <script>alert("bad")</script>라는 문자열을 제출한 경우 자바스크립트 경고창(alert)이 웹브라우저에서 실행됩니다.

이미지

라우팅(Routing)

사용자가 기억하고 직접 페이지를 방문하는 데 사용할 수 있는 의미 있는 URL을 사용하는 것이 좋습니다.

함수를 URL에 바인딩하려면 route 데코레이터를 사용합니다. 특정 주소에 접속하면 실행해야 하는 함수와 연결합니다.

from flask import Flask


app = Flask(__name__)

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

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


if __name__ == '__main__':
    app.run()

http://127.0.0.1:5000/로 접속하면 index 함수가 실행되어 'Index Page'가 출력되고 http://127.0.0.1:5000/hello로 접속하면 hello 함수가 실행되어 'Hello, World'가 출력됩니다.

변수 규칙(Variable Rules)

<variable_name>으로 섹션을 표시하여 URL에 변수 섹션을 추가할 수 있습니다. 그러면 함수가 <variable_name>을 키워드 인수로 받습니다. 선택적으로 컨버터(converter)를 사용하여 <converter:variable_name>과 같이 인수의 데이터 타입을 지정할 수 있습니다.

from flask import Flask
from markupsafe import escape

app = Flask(__name__)

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

@app.route('/user/<username>')
def show_user_profile(username):
    # show the user profile for that user
    return f'User {escape(username)}'

@app.route('/post/<int:post_id>')
def show_post(post_id):
    # show the post with the given id, the id is an integer
    return f'Post {post_id}'

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


if __name__ == '__main__':
    app.run()

http://127.0.0.1:5000/user/webnautes로 접속하면 함수가 슬래시가 없는 문자열을 전달받아 User webnautes가 출력됩니다.

http://127.0.0.1:5000/post/42로 접속하면 정수를 함수가 전달받아 Post 42가 출력됩니다.

http://127.0.0.1:5000/path/home/webnautes로 접속하면 함수가 슬래시가 있는 문자열을 전달받아 Subpath home/webnautes가 출력됩니다.

사용 가능한 컨버터 타입(Converter types)은 다음과 같습니다.

string(기본값) 슬래시 없는 모든 텍스트를 허용합니다.
int양의 정수를 허용합니다.
float양의 실수값을 허용합니다.
path슬래시를 포함하는 모든 텍스트를 허용합니다.
uuidUUID 문자열을 허용합니다.

고유 URL/ 리다이렉션 동작(Unique URLs / Redirection Behavior)

route 데코레이터 사용시 두가지 후행 슬래시 사용 방식이 있습니다.

from flask import Flask

app = Flask(__name__)

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

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

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


if __name__ == '__main__':
    app.run()
@app.route('/projects/')

projects 엔드포인트의 표준 URL에는 후행 슬래시가 있습니다. 이는 파일 시스템의 폴더와 유사합니다. 후행 슬래시(/projects)가 없는 URL에 액세스하면 Flask는 후행 슬래시(/projects/)가 있는 표준 URL로 리디렉션합니다.

@app.route('/about')

about 엔드포인트의 표준 URL에는 후행 슬래시가 없습니다. 이는 파일의 경로 이름과 비슷합니다. 후행 슬래시(/about/)가 있는 URL에 액세스하면 404 “찾을 수 없음” 오류가 발생합니다. 이렇게 하면 이러한 리소스에 대해 고유한 URL을 유지하여 검색 엔진이 같은 페이지를 두 번 색인하지 않도록 할 수 있습니다.

URL Building(URL 생성하기)

Flask에서는 특정 라우트(예: 로그인 페이지 등)의 URL을 직접 문자열로 작성하는 대신, url_for() 함수를 사용해 자동으로 URL을 생성할 수 있습니다.

url_for()는 실제 경로가 아니라 함수 이름을 기준으로 URL을 생성하기 때문에, 라우트 경로(/login)를 바꾸더라도 함수 이름만 유지하면 링크는 자동으로 반영됩니다.

url_for()를 사용하면 좋은 점은 다음과 같습니다.
- 라우트 이름만 바꾸면 관련 링크도 자동으로 반영됨 (유지보수 쉬움)
- URL에 포함되는 특수 문자나 공백도 자동 인코딩
- 앱이 /가 아닌 하위 경로(/myapp)에 설치된 경우도 자동 처리
절대 경로로 URL을 생성하므로, 상대 경로로 인한 오류 방지

간단히 테스트해볼 수 있는 코드입니다.

from flask import Flask, url_for, redirect

app = Flask(__name__)

@app.route('/')
def index():
    # 로그인 페이지로 이동하는 링크 생성
    login_url = url_for('login')
    return f'<p>Welcome!</p><p><a href="{login_url}">Go to Login</a></p>'

@app.route('/login')
def login():
    return 'This is the login page.'


if __name__ == '__main__':
    app.run(debug=True)

실행합니다.

python app.py

http://127.0.0.1:5000/에 접속하여 링크를 클릭하면 url_for('login') 코드가 실행되어 /login이라는 URL 문자를 생성하여 http://127.0.0.1:5000/login로 접속하게 됩니다.

url_for 함수는 라우트 함수 이름을 기반으로 Flask가 자동으로 경로를 만들어줍니다.

HTTP Methods

웹 애플리케이션은 브라우저와 서버가 서로 통신할 때 여러 가지 HTTP 메서드를 사용합니다.
가장 자주 사용되는 HTTP 메서드는 다음과 같습니다:

메서드용도
GET데이터를 요청 (폼, 페이지 보기 등)
POST데이터를 서버에 제출 (로그인, 글쓰기 등)
PUT기존 데이터를 수정
DELETE데이터를 삭제

Flask에서 라우트는 기본적으로 GET 요청만 처리합니다.
하지만 @app.route()에 methods 인자를 설정하면 다른 메서드도 처리할 수 있습니다.

아래 코드는 GET 요청으로 로그인 폼을 보여주고, POST 요청으로 로그인 데이터를 처리하는 예제입니다.

app.py

from flask import Flask, request, redirect, url_for

app = Flask(__name__)

# GET과 POST를 하나의 뷰 함수에서 처리
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if username == 'admin' and password == '1234':
            return f'환영합니다, {username}님!'
        else:
            return '로그인 실패', 401
    return '''
        <form method="post">
            아이디: <input type="text" name="username"><br>
            비밀번호: <input type="password" name="password"><br>
            <input type="submit" value="로그인">
        </form>
    '''

# 실행
if __name__ == '__main__':
    app.run(debug=True)

실행합니다.

python app.py

다음 과정으로 테스트합니다.

1.http://127.0.0.1:5000/login에 접속하면 로그인 폼을 보여줍니다. 사용자가 브라우저에서 http://127.0.0.1:5000/login에 접속하면, 브라우저는 서버에 GET 방식으로 요청을 보냅니다.
서버에서는 이 요청을 받아 로그인 폼이 포함된 HTML 페이지를 생성해 응답으로 보내고,
사용자는 화면에서 로그인 폼을 볼 수 있게 됩니다.

2.아이디와 패스워드에 admin과 1234를 각각 입력 후, 로그인을 클릭하면 "환영합니다, admin님!"가 출력됩니다. 로그인 버튼을 클릭하면 입력한 아이디와 비밀번호가 POST 방식으로 서버에 전송되고,
서버에서는 이를 검사하여 로그인 성공 여부에 따라 다른 응답을 보여줍니다.

GET 방식은 입력값이 URL에 포함되어 브라우저 주소창에 노출됩니다. 반면 POST 방식은 데이터가 HTTP 본문(body)에 담겨 전송되므로 더 안전하게 정보를 전달할 수 있습니다. 따라서 비밀번호나 개인 정보처럼 민감한 데이터는 반드시 POST 방식으로 전송해야 합니다.

정적 파일(Static Files)

동적 웹 애플리케이션에도 정적 파일이 필요합니다. 일반적으로 CSS와 JavaScript 파일이 여기에 있습니다. 웹 서버가 이를 제공하도록 구성하는 것이 가장 이상적이지만, 개발 중에도 Flask가 이를 수행할 수 있습니다. 패키지 또는 모듈 옆에 static이라는 폴더를 생성하면 애플리케이션의 /static에서 사용할 수 있습니다.

정적 파일의 URL을 생성하려면 특별한 static 엔드포인트 이름을 사용하세요:

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

웹 애플리케이션에서 정적 파일이란, 서버에서 실행 시 매번 변경되지 않고 항상 동일하게 제공되는 파일을 말합니다. 예를들어 CSS (스타일시트), JavaScript (클라이언트 스크립트), 이미지 (PNG, JPG 등), 글꼴 파일등이 될 수 있습니다.

Flask에서는 프로젝트 디렉터리 내에 static/ 폴더를 생성하면, 이 안의 파일들을 자동으로 /static/파일명 URL 경로를 통해 접근할 수 있습니다.

예를 들어 파일의 경로가 static/style.css이라면 접근할 수 있는 URL은 http://localhost:5000/static/style.css입니다.

Flask의 url_for() 함수로 정적 파일의 경로를 동적으로 생성할 수 있습니다:

url_for('static', filename='style.css')  # → "/static/style.css"

정적 css 파일을 사용하는 예제 코드를 테스트해봅니다.

1.app.py

from flask import Flask, render_template_string, url_for

app = Flask(__name__)

@app.route('/')
def index():
    css_url = url_for('static', filename='style.css')
    return render_template_string(f'''
        <!doctype html>
        <html>
            <head>
                <link rel="stylesheet" href="{css_url}">
                <title>정적 파일 예제</title>
            </head>
            <body>
                <h1>Flask 정적 파일 예제</h1>
                <p>이 페이지는 외부 CSS 파일을 사용합니다.</p>
            </body>
        </html>
    ''')

if __name__ == '__main__':
    app.run(debug=True)

2.static/style.css

body {
    background-color: #f0f0f0;
    font-family: sans-serif;
}

h1 {
    color: darkblue;
}

p {
    font-size: 1.2em;
}

실행합니다.

python app.py

웹브라우저에서 http://127.0.0.1:5000/에 접속하면 CSS가 적용된 웹페이지가 보입니다.

이미지

style.css 파일을 지우고 웹브라우저에서 f5를 눌러보면 CSS 효과가 사라집니다.

이미지

렌더링 템플릿(Rendering Templates)

파이썬에서 직접 HTML을 생성하는 것은 매우 번거로운 작업입니다. 코드 안에 HTML 태그를 일일이 작성해야 하기 때문입니다. 특히 사용자 입력을 HTML에 포함시킬 때는 XSS 공격 등을 방지하기 위해 특수 문자들을 이스케이프 처리해야 하는 보안 작업을 신경써야 합니다.

Flask는 이런 불편함을 해결하기 위해 Jinja2라는 템플릿 엔진을 기본으로 제공합니다. Jinja2를 사용하면 HTML 템플릿 파일을 별도로 만들어서 파이썬 코드와 분리할 수 있고, 변수나 데이터를 템플릿에 전달할 때 자동으로 HTML 이스케이프 처리가 되므로 보안 문제를 걱정하지 않아도 됩니다. 또한 템플릿 안에서 조건문이나 반복문을 사용할 수 있어서 동적인 웹 페이지를 쉽게 만들 수 있습니다.

Flask의 템플릿 시스템은 매우 유연하고 다양한 용도로 활용할 수 있는 강력한 도구입니다. 대부분의 웹 애플리케이션에서는 사용자에게 보여줄 HTML 페이지를 동적으로 생성하는 것이 주된 목적이지만, 템플릿의 활용 범위는 그보다 훨씬 넓습니다.

예를 들어, 블로그나 문서 사이트에서 사용하는 마크다운 파일을 생성할 수 있고, 사용자에게 발송할 이메일의 본문을 일반 텍스트 형태로 만들 수도 있습니다. 또한 CSV 파일, JSON 응답, XML 문서, 심지어 설정 파일이나 스크립트 파일까지도 템플릿을 통해 동적으로 생성할 수 있습니다.

이는 Flask가 웹 프레임워크이지만 단순히 웹 페이지만을 위한 것이 아니라, 다양한 형태의 텍스트 기반 콘텐츠를 필요로 하는 모든 애플리케이션에서 활용할 수 있음을 의미합니다. 템플릿에 데이터를 전달하고 조건문이나 반복문을 사용하여 원하는 형태의 텍스트 파일을 생성하는 방식은 동일하므로, 개발자는 일관된 방법으로 다양한 출력 형태를 다룰 수 있습니다.

템플릿을 렌더링하려면 render_template() 메서드를 사용할 수 있습니다. 템플릿 파일의 이름과 템플릿 엔진에 전달할 변수를 키워드 인수로 제공하기만 하면 됩니다.

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

다음은 템플릿을 렌더링하는 방법에 대한 간단한 예시입니다.

테스트에 사용할 폴더의 구성은 다음과 같이 해야합니다.

hello.py 파일을 생성한 후, flask 코드를 저장합니다.
templates 폴더를 생성한 후, 템플릿을 저장할 hello.html 파일을 생성합니다.

이미지

hello.py : flask 코드입니다.

from flask import Flask, render_template


app = Flask(__name__)


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

hello.html : 템플릿입니다.

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

다음처럼 실행합니다.

flask --app hello run

http://127.0.0.1:5000/hello/webnautes에 접속하면 route 데코레이터의 /hello/<name> 경로에서 <name> 부분이 "webnautes"로 매칭되어 함수의 name 매개변수에 전달됩니다. 그리고 render_template 함수를 호출할 때 템플릿 파일인 hello.html에 person이라는 변수명으로 name 값을 전달합니다.

템플릿 hello.html에서 person 변수 값이 존재하므로 웹브라우저에 Hello webnautes!가 출력됩니다.

http://127.0.0.1:5000/hello/에 접속하면 route 데코레이터의 /hello/에 대응되며, 따라서 person 변수 값이 존재하지 않게 됩니다.

템플릿 hello.html에서 person 값이 존재하지 않으므로 웹브라우저에 Hello, World!가 출력됩니다.

플라스크는 templates 폴더에서 템플릿을 찾습니다. 따라서 애플리케이션이 모듈인 경우 templates 폴더는 모듈과 같은 폴더에 위치하며, 애플리케이션이 패키지인 경우에는 패키지 내부에 위치하게 됩니다.

애플리케이션이 모듈인 경우

/application.py
/templates
    /hello.html

애플리케이션이 패키지인 경우

/application
    /__init__.py
    /templates
        /hello.html

템플릿에서 진자2(Jinja2) 템플릿의 모든 기능을 사용할 수 있습니다. 자세한 내용은 공식 Jinja2 템플릿 문서를 참조하세요.

템플릿 내부에서는 configrequest session 및 g 객체와 url_for() 및 get_flashed_messages() 함수를 사용할 수 있습니다.

템플릿은 상속을 사용하는 경우 특히 유용합니다. 어떻게 작동하는지 알고 싶으시면 템플릿 상속을 참조하세요. 기본적으로 템플릿 상속을 사용하면 각 페이지의 특정 요소(헤더, 탐색, 바닥글 등)를 유지할 수 있습니다.

자동 이스케이프가 활성화되어 있으므로 person에 HTML이 포함되어 있으면 자동으로 이스케이프됩니다. 변수를 신뢰할 수 있고 안전한 HTML이라는 것을 알고 있는 경우(예: 위키 마크업을 HTML로 변환하는 모듈에서 가져온 것이므로) Markup 클래스를 사용하거나 템플릿에서 |safe 필터를 사용하여 변수를 안전하다고 표시할 수 있습니다. 더 많은 예제는 진자 2 문서를 참조하세요.

다음은 Markup 클래스의 기본적인 작동 방식에 대한 소개입니다:

>>> from markupsafe import Markup

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

웹 애플리케이션에서는 클라이언트가 서버에 보내는 데이터를 처리하는 것이 중요합니다. Flask에서는 이 데이터를 전역 request 객체를 통해 제공합니다. 이 객체는 컨텍스트 로컬(context local)이라는 방식으로 구현되어 있어, 전역처럼 보이지만 각 요청마다 독립적으로 동작합니다. 이를 통해 Flask는 동시에 여러 요청을 처리할 때도 스레드 안전성을 유지할 수 있습니다.

컨텍스트 로컬(Context Locals)

Flask에서는 자주 사용하는 객체들인 requestsessiong 등이 전역 변수처럼 보이지만 실제로는 전역이 아닙니다. 이들은 werkzeug.local.LocalProxy를 기반으로 한 컨텍스트 로컬 객체(context-local object)입니다. 즉, 각 요청(request)마다 독립적으로 값을 유지합니다.

컨텍스트 로컬이 왜 필요할까요.

웹 애플리케이션은 여러 사용자의 요청을 동시에 병렬적으로 처리합니다. 만약 request가 단순한 전역 변수라면 다음과 같은 문제가 생깁니다:

사용자 A의 요청 정보를 처리 중인 도중,
사용자 B의 요청이 들어와 request 값을 덮어쓰면
사용자 A는 B의 요청 데이터를 참조하게 되어 데이터 충돌이 발생합니다.

이를 해결하기 위해 Flask는 요청마다 고유한 컨텍스트(context)를 만들고,
request 등은 현재 실행 중인 컨텍스트에서만 유효한 값을 자동으로 반환하게 되어 있습니다.

테스트나 셸 환경에서는 HTTP 요청이 실제로 존재하지 않기 때문에 request 객체를 바로 사용할 수 없습니다.

이럴 때는 Flask에서 제공하는 test_request_context() 또는 app.request_context()를 사용하여
가짜 요청 컨텍스트(fake request context)를 만들어야 합니다.

간단한 예제 코드로 살펴봅니다.

from flask import Flask, request

app = Flask(__name__)

# 실제 요청 핸들러 예시
@app.route('/user/<username>')
def show_user(username):
    print("요청 경로:", request.path)
    print("요청 메서드:", request.method)
    return f"Hello, {username}!"

# 요청 컨텍스트 없이 테스트하는 방법
def test_request_simulation():
    # GET 요청을 흉내 내는 컨텍스트 생성
    with app.test_request_context('/user/alice', method='GET'):
        assert request.path == '/user/alice'
        assert request.method == 'GET'
        print("✔ 컨텍스트 내에서 request 객체 사용 가능")
        print("요청 경로:", request.path)
        print("요청 메서드:", request.method)

if __name__ == '__main__':
    test_request_simulation()
  1. 위 코드를 test_context.py 같은 파일에 저장합니다.
  2. 터미널 또는 명령 프롬프트에서 해당 파일이 있는 디렉터리로 이동합니다.
  3. 아래 명령어를 실행합니다:

python test_context.py

  1. 정상 실행되면 다음과 같은 출력이 보입니다:

✔ 컨텍스트 내에서 request 객체 사용 가능
요청 경로: /user/alice
요청 메서드: GET

이처럼 실제 HTTP 요청 없이도 Flask 내부 객체(requestsessiong 등)를 테스트할 수 있습니다.

이번엔 실제 사용자 요청으로 컨텍스트 로컬(Context Locals)의 동작을 확인해 봅니다. 요청이 처리되는 동안에만 해당 컨텍스트가 유효하다는 점이 중요합니다.

다음 코드를 실행 후, 웹브라우저에서 요청을 보내면,
Flask가 자동으로 해당 요청에 대한 컨텍스트를 생성하고 requestsessiong 등을 사용할 수 있게 됩니다.

app.py

from flask import Flask, request

app = Flask(__name__)

@app.route('/check')
def check_context_local():
    print("request.path:", request.path)
    print("request.method:", request.method)
    return f"요청 경로는 {request.path}, 메서드는 {request.method} 입니다."

if __name__ == '__main__':
    app.run(debug=True)

실행해봅니다.

python app.py

웹브라우저에서 http://127.0.0.1:5000/check 주소에 접속합니다.

웹브라우저에 "요청 경로는 /check, 메서드는 GET 입니다." 메시지가 보이고 터미널에 다음 메시지가 보입니다. "

request.path: /check
request.method: GET

요청 객체(The Request Object)

Flask에서 request 객체는 클라이언트가 보낸 요청 정보를 담고 있는 핵심 객체입니다.
폼 데이터, URL 파라미터, 요청 방식 등 다양한 정보를 이 객체를 통해 얻을 수 있습니다.

여기에선 몇가지를 살펴봅니다.

1.request.method
사용자가 어떤 방식(GET, POST 등)으로 서버에 요청했는지를 알려줍니다.

예를 들어 사용자가 /login 주소로 접속하여 웹페이지를 열었는지, 웹페이지에 있는 버튼을 눌렀는지를 구분해줍니다.

app.py

from flask import Flask, request, render_template_string

app = Flask(__name__)

# 간단한 로그인 폼 템플릿
login_form_html = """
<!doctype html>
<html>
  <body>
    <h2>로그인 페이지</h2>
    <form method="post">
      아이디: <input name="username"><br>
      비밀번호: <input type="password" name="password"><br>
      <button type="submit">로그인</button>
    </form>
  </body>
</html>
"""

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        return "사용자가 로그인 버튼을 눌렀어요!"
    else:
        return render_template_string(login_form_html)

if __name__ == '__main__':
    app.run(debug=True)

실행해보면 http://127.0.0.1:5000/login 접속시 GET 요청으로 인식하여 웹브라우저에 로그인 창을 보여주며 사용자가 아이디와 패스워드 입력 후, 로그인 버튼을 클릭하면 화면에 "사용자가 로그인 버튼을 눌렀어요!" 메시지가 보이게 됩니다.

2.request.form
사용자가 폼(form)에 입력한 값을 가져올 수 있습니다. 보통 POST 요청입니다.

간단한 코드로 살펴봅니다. request.form를 사용하여 input 태그의 변수값을 가져오는 것을 볼 수 있습니다.

from flask import Flask, request, render_template_string

app = Flask(__name__)

# 간단한 로그인 폼 템플릿
login_form_html = """
<!doctype html>
<html>
  <body>
    <h2>로그인 페이지</h2>
    <form method="post">
      아이디: <input name="username"><br>
      비밀번호: <input type="password" name="password"><br>
      <button type="submit">로그인</button>
    </form>
  </body>
</html>
"""

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':

        username = request.form['username']
        password = request.form['password']

        return f"입력한 아이디: {username}, 비밀번호: {password}"
    else:
        return render_template_string(login_form_html)

if __name__ == '__main__':
    app.run(debug=True)

다음 과정으로 테스트합니다.

1.http://127.0.0.1:5000/login 주소로 접속하면 로그인 화면이 보입니다. 아이디에 webnautes, 비밀번호에 42를 입력 후 로그인 버튼을 클릭합니다.

2.웹페이지에 앞에서 입력한 아이디와 비밀번호가 보이게 됩니다.
입력한 아이디: webnautes, 비밀번호: 42

3.request.args
주소창에 붙은 쿼리 문자열(?key=value)에서 값을 가져올 수 있습니다. 보통 GET 요청에 사용됩니다.

다음 코드를 사용하여 테스트해봅니다.

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/search')
def search():
    keyword = request.args.get('keyword', '')  # 없으면 빈 문자열 반환
    return f"검색어는: {keyword}"

if __name__ == '__main__':
    app.run(debug=True)

다음 과정으로 테스트합니다.

1.http://127.0.0.1:5000/search?keyword=webnautes 주소로 접속합니다.
2.웹페이지에 검색어는: webnautes 메시지가 보입니다. request.args.get을 사용하여 주소에 보이는 keyword의 키값을 가져옵니다.

File Uploads

Flask에서 파일 업로드는 다음과 같은 단계로 진행됩니다. 먼저 사용자가 HTML 폼을 통해 파일을 선택하면, 브라우저는 multipart/form-data 형식으로 파일 데이터를 서버에 전송합니다. Flask 서버는 request.files 객체를 통해 업로드된 파일을 받아서 메모리나 임시 디렉토리에 저장합니다. 이후 개발자가 작성한 검증 로직을 통해 파일의 타입, 크기, 이름 등을 확인하고, 문제가 없으면 save() 메서드를 사용하여 서버의 지정된 위치에 영구적으로 저장합니다. 마지막으로 업로드 결과를 사용자에게 응답으로 전달합니다.

업로드 구현시 고려해야하는 사항입니다.

enctype="multipart/form-data" 을 설정하지 않으면 파일 업로드가 되지 않습니다.
클라이언트의 파일명을 사용하여 업로드하려면 secure_filename 함수를 사용하여 파일명을 안전하게 처리해야 합니다. 왜냐하면 악의적인 파일명을 통한 디렉토리 탐색 공격이 발생할 수 있습니다. 사용자가 ../../../etc/passwd 같은 경로를 포함한 파일명을 업로드하여 시스템 파일에 접근하려 할 수 있습니다.

또한 실행 가능한 파일이나 스크립트 파일의 업로드를 방지하기 위해 허용되는 파일 확장자를 제한하고, 서버 리소스 고갈을 막기 위해 파일 크기 제한을 설정해야 합니다.

추가적으로 업로드된 파일을 웹에서 직접 접근할 수 없는 디렉토리에 저장하고, 파일 내용을 검증하여 악성 코드가 포함되지 않았는지 확인하는 것이 중요합니다. 파일 업로드 횟수나 용량에 대한 제한을 두어 DoS 공격을 방지하고, 업로드된 파일의 메타데이터를 로깅하여 추적 가능성을 확보하는 것도 필요합니다.

테스트를 하기 위해 app.py 파일을 저장한 후 templates 폴더를 생성하여 upload.html 파일을 저장합니다.

이미지

app.py

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

app = Flask(__name__)
app.secret_key = 'your-secret-key-here'

# 업로드 설정
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB 제한

# 업로드 폴더가 없으면 생성
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)

def allowed_file(filename):
    """허용된 파일 확장자인지 확인"""
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def index():
    """메인 페이지 - 업로드 폼 및 파일 업로드 처리"""
    if request.method == 'POST':
        # 파일이 포함되어 있는지 확인
        if 'file' not in request.files:
            flash('파일을 선택해주세요')
            return redirect(request.url)

        file = request.files['file']

        # 파일이 선택되었는지 확인
        if file.filename == '':
            flash('파일을 선택해주세요')
            return redirect(request.url)

        # 파일이 유효하고 허용된 확장자인지 확인
        if file and allowed_file(file.filename):
            # 안전한 파일명으로 변경
            filename = secure_filename(file.filename)

            # 파일 저장
            file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
            file.save(file_path)

            flash(f'파일 "{filename}"이 성공적으로 업로드되었습니다!')
            return redirect(url_for('index'))
        else:
            flash('허용되지 않는 파일 형식입니다')
            return redirect(request.url)

    return render_template('upload.html')

if __name__ == '__main__':
    app.run(debug=True)

templates/upload.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>파일 업로드</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        .upload-container {
            border: 2px dashed #ccc;
            padding: 20px;
            text-align: center;
            border-radius: 10px;
        }
        .upload-form {
            margin: 20px 0;
        }
        .file-input {
            margin: 10px 0;
            padding: 10px;
        }
        .upload-btn {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .upload-btn:hover {
            background-color: #45a049;
        }
        .flash-messages {
            margin: 20px 0;
        }
        .flash-message {
            padding: 10px;
            margin: 5px 0;
            border-radius: 4px;
        }
        .flash-success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .flash-error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
    </style>
</head>
<body>
    <h1>파일 업로드</h1>

    <!-- 플래시 메시지 표시 -->
    {% with messages = get_flashed_messages() %}
        {% if messages %}
            <div class="flash-messages">
                {% for message in messages %}
                    <div class="flash-message flash-success">{{ message }}</div>
                {% endfor %}
            </div>
        {% endif %}
    {% endwith %}

    <div class="upload-container">
        <h2>파일을 선택하세요</h2>
        <p>허용되는 파일: PNG, JPG, JPEG, GIF</p>
        <p>최대 파일 크기: 16MB</p>

        <form method="POST" enctype="multipart/form-data" class="upload-form">
            <div>
                <input type="file" name="file" class="file-input" accept=".png,.jpg,.jpeg,.gif" required>
            </div>
            <div>
                <button type="submit" class="upload-btn">업로드</button>
            </div>
        </form>
    </div>
</body>
</html>

실행해봅니다.

python app.py

127.0.0.1:5000에 접속한 후 파일 선택 버튼을 클릭하고 이미지 파일을 선택합니다.

이미지

이제 업로드 버튼을 클릭합니다.

이미지

파일 이름과 함께 업로드가 성공적이었다는 메시지가 보입니다.

이미지

uploads 폴더에 업로드된 cat.jpg 파일이 있는 것을 확인할 수 있습니다.

이미지

쿠키(Cookies)

쿠키는 웹사이트가 사용자의 브라우저에 저장하는 작은 텍스트 파일로, 사용자의 정보나 설정을 기억하기 위해 사용됩니다. 쿠키는 사용자 ID, 로그인 상태, 장바구니 내용, 언어 설정 등의 데이터를 포함할 수 있습니다.

쿠키의 동작원리는 다음과 같습니다. 사용자가 웹사이트에 처음 방문하면 서버는 HTTP 응답 헤더에 Set-Cookie를 포함하여 쿠키를 생성하고 브라우저에 저장하도록 지시합니다. 이후 사용자가 같은 웹사이트를 재방문할 때마다 브라우저는 자동으로 저장된 쿠키를 HTTP 요청 헤더에 포함하여 서버로 전송합니다. 서버는 이 쿠키 정보를 읽어 사용자를 식별하고 개인화된 서비스를 제공할 수 있습니다.

쿠키의 주요 문제점들이 있습니다. 개인정보 보호 측면에서 쿠키는 사용자의 웹 활동을 추적하고 개인정보를 수집하는 데 사용될 수 있어 프라이버시 침해 우려가 있습니다. 특히 서드파티 쿠키는 여러 웹사이트에서 사용자를 추적하여 광고 타겟팅에 활용됩니다. 또한 쿠키는 보안 취약점을 가지고 있어 세션 하이재킹, XSS 공격, CSRF 공격 등에 악용될 수 있습니다. 쿠키는 브라우저별로 저장 용량 제한이 있고, 사용자가 쿠키를 삭제하거나 비활성화할 수 있어 데이터 손실 가능성도 있습니다.

Flask에서 쿠키를 다루는 코드를 구현해봅니다.

from flask import Flask, request, make_response

app = Flask(__name__)

@app.route('/')
def home():
    # 쿠키에서 이름 가져오기
    name = request.cookies.get('name')

    if name:
        return f'안녕하세요, {name}님! <a href="/logout">로그아웃</a>'
    else:
        return '''
        <form action="/login" method="post">
            <input type="text" name="name" placeholder="이름을 입력하세요">
            <button type="submit">로그인</button>
        </form>
        '''

@app.route('/login', methods=['POST'])
def login():
    name = request.form['name']

    # 응답 만들기
    response = make_response(f'{name}님 환영합니다! <a href="/">홈으로</a>')

    # 쿠키 설정
    response.set_cookie('name', name)

    return response

@app.route('/logout')
def logout():
    response = make_response('로그아웃 완료! <a href="/">홈으로</a>')

    # 쿠키 삭제
    response.set_cookie('name', '', expires=0)

    return response

if __name__ == '__main__':
    app.run(debug=True)

http://127.0.0.1:5000/로 접속하면 로그인한 적이 없기 때문에 로그인을 하라는 페이지가 보입니다.

이미지

이름을 입력하고 로그인을 클릭합니다.

이미지

http://127.0.0.1:5000/login로 이동하면서 로그인이 되었다고 보입니다. 홈 링크를 클릭하거나 http://127.0.0.1:5000/로 접속합니다. 이제 로그아웃을 클릭해봅니다.

이미지


http://127.0.0.1:5000/logout로 이동하면서 로그아웃이 완료되었다는 메시지가 보입니다. 홈으로를 클릭합니다.

이미지

다시 로그인하라는 페이지가 보입니다.

이미지

리다이렉트와 에러(Redirects and Errors)

웹 애플리케이션에서 사용자의 요청을 다른 페이지로 안내하거나 오류 상황을 처리하는 것은 매우 중요한 기능입니다. Flask는 이러한 작업을 위해 redirect()와 abort() 함수를 제공합니다.

redirect() 함수

사용자를 다른 URL로 자동으로 이동시키는 함수입니다. 주로 로그인 후 메인 페이지로 이동하거나, 폼 제출 후 결과 페이지로 이동할 때 사용합니다.

abort() 함수

HTTP 오류 코드와 함께 요청을 즉시 중단하는 함수입니다. 권한이 없거나 잘못된 접근을 차단할 때 사용합니다.

다음은 로그인 페이지에서 redirect()와 abort() 함수를 테스트할 수 있는 간단한 예제입니다:

from flask import Flask, render_template, request, redirect, url_for, abort

app = Flask(__name__)

# 간단한 사용자 정보 (실제로는 데이터베이스 사용)
users = {
    'user': '1234'
}

@app.route('/')
def home():
    return '<h1>메인 페이지</h1><a href="/login">로그인</a>'

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']

        # 사용자명이 존재하지 않으면 404 에러 발생 (abort 테스트)
        if username not in users:
            abort(404)  # 사용자를 찾을 수 없음

        # 비밀번호가 틀리면 403 에러 발생 (abort 테스트)
        if users[username] != password:
            abort(403)  # 접근 권한 없음

        # 로그인 성공 시 메인 페이지로 리다이렉트 (redirect 테스트)
        return redirect(url_for('success'))

    # GET 요청 시 로그인 폼 표시
    return '''
    <form method="post">
        <h2>로그인</h2>
        <p>사용자명: <input type="text" name="username" required></p>
        <p>비밀번호: <input type="password" name="password" required></p>
        <p><input type="submit" value="로그인"></p>
    </form>
    <p>테스트 계정: user/1234</p>
    '''

@app.route('/success')
def success():
    return '<h1>로그인 성공!</h1><a href="/">홈으로</a>'

if __name__ == '__main__':
    app.run()

http://127.0.0.1:5000/으로 접속한 후, 로그인을 클릭하면 로그인 페이지인 http://127.0.0.1:5000/login으로 이동합니다.

이미지

다른 주소로 이동시키는 redirect 함수를 테스트합니다. 올바른 계정 정보인 user/1234를 입력 후 로그인을 클릭합니다.

이미지

http://127.0.0.1:5000/success으로 이동하면서 로그인 성공 ! 메시지가 보입니다. 홈으로 눌러 초기화면으로 돌아옵니다.

이미지

HTTP 오류 코드와 함께 요청을 즉시 중단하는abort 함수를 를 테스트합니다. 로그인 페이지에서 존재하지 않는 계정 정보인 1/1을 입력합니다.

이미지

사용자를 찾을 수 없어 404 에러가 발생합니다. 뒤로 이동합니다.

이미지


abort 함수를 다시 한번 더 테스트합니다. 이번엔 사용자명은 제대로 입력하고 비밀번호를 틀리게 입력한 후, 로그인을 클릭합니다.

이미지

패스워드가 틀려서 403 에러가 발생합니다.

이미지

응답(Responses)

Flask에서 뷰 함수의 반환값은 자동으로 응답 객체로 변환됩니다. 변환 규칙은 다음과 같습니다:

1.응답 객체 → 그대로 반환

뷰 함수에서 이미 완성된 응답 객체를 반환하는 경우에는 Flask가 별도의 변환 작업을 수행하지 않고 해당 객체를 그대로 클라이언트에게 전달합니다. 이는 개발자가 완전한 제어권을 가지고 응답을 구성했다는 것을 의미합니다.

2.문자열 → 200 OK 상태코드와 text/html 타입으로 응답 생성
문자열을 반환하는 경우 Flask는 이를 HTML 응답으로 간주하여 자동으로 200 OK 상태코드와 text/html MIME 타입을 가진 응답 객체를 생성합니다. 이는 가장 간단한 형태의 웹 페이지나 HTML 컨텐츠를 반환할 때 매우 편리한 방식입니다.

3.반복자/생성자 → 스트리밍 응답으로 처리
반복자나 생성자 객체를 반환하는 상황입니다. 이때 Flask는 해당 객체가 문자열이나 바이트를 생성한다고 가정하고 스트리밍 응답으로 처리합니다. 이는 대용량 데이터를 메모리 효율적으로 전송할 때 유용한 방식입니다.

4.딕셔너리/리스트 → jsonify()로 JSON 응답 생성
딕셔너리나 리스트를 반환하면 Flask는 자동으로 jsonify() 함수를 호출하여 JSON 형태의 응답을 생성합니다. 이는 REST API 개발에서 매우 자주 사용되는 패턴으로, 별도의 JSON 변환 작업 없이도 구조화된 데이터를 클라이언트에게 전달할 수 있습니다.

5.튜플 → (응답, 상태코드)(응답, 헤더)(응답, 상태코드, 헤더) 형태로 추가 정보 제공
튜플을 반환하는 경우입니다. 튜플의 형태에 따라 추가적인 HTTP 정보를 제공할 수 있으며, (응답, 상태코드)(응답, 헤더), 또는 (응답, 상태코드, 헤더) 형태로 구성할 수 있습니다. 이를 통해 개발자는 응답 내용과 함께 특정 상태코드나 커스텀 헤더를 쉽게 설정할 수 있습니다.

6.기타 → WSGI 애플리케이션으로 간주하여 변환
마지막으로, 위의 모든 경우에 해당하지 않는 반환값은 Flask가 유효한 WSGI 애플리케이션으로 간주하여 응답 객체로 변환합니다.

뷰 함수 내에서 응답 객체를 직접 조작해야 하는 경우에는 make_response() 함수를 사용할 수 있습니다. 이 함수를 통해 Flask의 자동 변환 과정을 거친 응답 객체를 얻어서 헤더 추가, 쿠키 설정, 상태코드 변경 등의 추가적인 작업을 수행할 수 있습니다. 이는 복잡한 응답 구성이 필요한 경우에 매우 유용한 도구입니다.

간단한 예제 입니다.

from flask import Flask, jsonify, make_response, render_template_string

app = Flask(__name__)

# 1. 문자열 반환 (200 OK, text/html)
@app.route('/hello')
def hello():
    return "<h1>안녕하세요!</h1>"

# 2. JSON 반환 (딕셔너리)
@app.route('/api/user')
def get_user():
    return {
        'name': '홍길동',
        'age': 30,
        'city': '서울'
    }

# 3. 튜플로 상태코드 지정
@app.route('/api/error')
def api_error():
    return {'error': '데이터를 찾을 수 없습니다'}, 404

# 4. 튜플로 헤더 추가
@app.route('/api/data')
def api_data():
    return {'data': 'success'}, 200, {'X-Custom-Header': 'MyValue'}


# 5. 에러 핸들러 예제
@app.errorhandler(404)
def not_found(error):
    resp = make_response(
        render_template_string('''
        <html>
        <body>
            <h1>404 - 페이지를 찾을 수 없습니다</h1>
            <p>요청하신 페이지가 존재하지 않습니다.</p>
        </body>
        </html>
        '''), 404
    )
    resp.headers['X-Error-Type'] = 'Not Found'
    return resp

if __name__ == '__main__':
    app.run(debug=True)

실행 결과입니다.

http://127.0.0.1:5000/로 접속한 경우입니다. 존재하지 않는 페이지 접속시 errorhandler(404)에서 처리합니다.

이미지

http://localhost:5000/hello로 접속한 경우입니다. 문자열을 응답한 경우입니다.

이미지

http://localhost:5000/api/user로 접속한 경우입니다. JSON을 반환한 경우입니다.

이미지

http://localhost:5000/api/error로 접속한 경우입니다. 튜플로 상태 코드를 반환한 경우입니다.

이미지


http://localhost:5000/api/data로 접속한 경우입니다. 튜플로 헤더를 반환한 경우입니다.

이미지

JSON 응답하는 API 구현

Flask 1.1 이상에서는 딕셔너리나 리스트를 뷰 함수에서 직접 반환하면 Flask가 이를 자동으로 JSON 응답으로 변환해줍니다. 내부적으로 jsonify()와 비슷한 방식으로 처리되며, Content-Type: application/json 헤더도 자동으로 추가됩니다.

예를 들어 다음처럼 home 함수에서 딕셔너리를 리턴하면

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():

    # 딕셔너리
    return {"message": "Hello, world!"}


if __name__ == '__main__':
    app.run()

Flask는 자동으로 아래와 같은 HTTP 응답을 생성하면서 json이라고 명시해줍니다.

  • Content-Type: application/json
  • 응답 본문: {"message": "Hello, world!"}

웹 브라우저에 {"message": "Hello, world!"} JSON 형식의 메시지가 보입니다.

JSON 형식의 응답으로 자동으로 변환해주긴 하지만 jsonify() 함수를 사용하여 명시적으로 JSON 응답을 바꾸는 것이 더 낫습니다. 왜냐하면 유니코드가 깨지지 않도록 처리해주며 HTTP 상태 코드와 헤더를 명시적으로 지정할 수도 있습니다.

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():

    # 딕셔너리
    return jsonify({"message": "Hello, world!"})


if __name__ == '__main__':
    app.run()

실행결과는 jsonify 함수를 사용하지 않을 때와 동일합니다.

이제 간단한 JSON API를 살펴보겠습니다. 사용자의 요청에 대한 응답을 JSON 형식으로 반환해줍니다.

from flask import Flask, url_for, jsonify

app = Flask(__name__)

# 딕셔너리를 반환하는 함수입니다. 
def get_current_user():
    return {
        "username": "jeongjoo",
        "theme": "dark",
        "image": url_for('static', filename='avatar.png')
    }

def get_all_users():
    return [
        {
            "username": "jeongjoo",
            "theme": "dark",
            "image": url_for('static', filename='avatar.png')
        },
        {
            "username": "mina",
            "theme": "light",
            "image": url_for('static', filename='mina.png')
        }
    ]

# 딕셔너리 응답을 테스트합니다.
@app.route("/me")
def me_api():
    user = get_current_user()
    return jsonify(user)

# 리스트 응답을 테스트합니다. 
@app.route("/users")
def users_api():
    users = get_all_users()
    return jsonify(users)

if __name__ == '__main__':
    app.run(debug=True)

위 코드를 실행한 후, 웹브라우저에 다음 두개의 주소에 각각 접속합니다. 실행 결과를 보면 url_for 함수가 정적인 경로로 변환해준 것을 볼 수 있습니다.

http://localhost:5000/me

웹브라우저에 {"image":"/static/avatar.png","theme":"dark","username":"jeongjoo"}를 JSON 형식으로 출력해줍니다. 딕셔너리 응답에 대한 테스트입니다.

http://localhost:5000/users

웹브라우저에  [{"image":"/static/avatar.png","theme":"dark","username":"jeongjoo"},{"image":"/static/mina.png","theme":"light","username":"mina"}]를 JSON 형식으로 출력해줍니다. 리스트 응답에 대한 테스트입니다.

세션(Sessions)

Flask의 세션은 사용자별 데이터를 유지하는 데 사용됩니다.
왜냐하면 웹 애플리케이션은 기본적으로 기억력이 없는 시스템이기 때문입니다.

예를 들어 사용자가 로그인 요청을 할 때마다 서버는 처음 로그인을 시도하는 사람처럼 생각합니다.
이를 해결하기 위해 사용되는 것이 바로 세션(session)입니다.

세션을 사용하여 사용자를 기억합니다. 예를 들어 사용자가 로그인을 하면 그 정보를 서버에 기억해서 다음에 사용자가 요청시 이미 로그인한 사용자라는 것을 인식합니다.

서버에 기억하는 정보가 세션 ID로 쿠키에 저장됩니다. 세션 ID를 확인하여 이미 로그인한 사용자라는 것을 서버가 알게 되는 것입니다.

로그인 상태를 유지하는 예시입니다.

/로 접속시 로그인 화면이 보입니다.

사용자가 webnautes로 로그인을 하면 session['username'] = 'webnautes' 처럼 세션에 저장합니다.

동일한 사용자가 다시 /로 접속시 flask가 세션을 보고 로그인 화면 대신에 webnautes님 로그인 중이라고 화면에 표시해줍니다.

Flask의 세션은 서버에 따로 저장소를 두지 않고, 세션 데이터를 클라이언트의 쿠키 안에 직접 저장하며 Flask는 이를 서명(signing)해서 위변조를 방지합니다. 사용자가 쿠키 내용을 볼 수는 있지만, 조작은 불가능합니다. 비밀 키로 서명되어 있기 때문입니다. 서명이란, 쿠키에 담긴 정보가 원래 Flask에서 만든 것인지 확인하기 위한 검증 방식입니다.

Flask는 쿠키 위변조를 막기 위해 데이터를 서명하는데 이때 사용하는 비밀 키(secret_key)는 반드시 안전하게 관리해야 합니다. 너무 단순하거나 노출된 키는 보안 취약점을 초래할 수 있습니다.

비밀키를 생성하는 방법은 여러가지가 있는데 그중 하나는 다음처럼 터미널에서 다음 명령으로 생성하는 것입니다.

$ python -c 'import secrets; print(secrets.token_hex())'

세션을 사용하여 간단한 로그인/로그아웃을 구현한 코드입니다.

from flask import Flask, session, redirect, url_for, request

app = Flask(__name__)

# 실제 서비스에서는 단순한 키(b'42') 대신 보안성이 높은 비밀 키를 사용해야 합니다.
app.secret_key = b'42'

@app.route('/')
def index():
    if 'username' in session:
        return f"Logged in as {session['username']}. <a href="/logout">Logout</a>"
    return 'You are not logged in. <a href="/login">Login</a>'

@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 placeholder="Enter username">
            <p><input type=submit value=Login>
        </form>
    '''

@app.route('/logout')
def logout():
    # 세션에서 사용자 정보 제거
    session.pop('username', None)
    return redirect(url_for('index'))

if __name__ == '__main__':
    app.run(debug=True)

실행합니다.

$ python app.py

다음 주소에 접속하여 웹브라우저에서 다음처럼 테스트합니다.

1.http://127.0.0.1:5000/ 접속시 로그인 상태가 아니라는 다음 메시지가 보입니다.
You are not logged in. Login

2.Login 링크를 클릭합니다.

3.http://127.0.0.1:5000/login 주소로 이동합니다. 사용자 이름 webnautes를 입력한 후, Login 버튼을 클릭합니다.

4.http://127.0.0.1:5000/ 주소로 이동하면서 다음 메시지가 보입니다. webnautes 사용자로 로그인했다며 Logout 링크가 보입니다. 지금 시점부턴 http://127.0.0.1:5000/ 로 접속시 다음 메시지가 계속 보이며 로그인 상태라는 것을 표시합니다.
Logged in as webnautes. Logout

5.Logout 링크를 클릭합니다.

6.http://127.0.0.1:5000/로 이동하며 로그인 상태가 아니라는 다음 메시지가 다시 보입니다.
You are not logged in. Login

지금 예제는 기본 Flask 방식으로 모든 세션 데이터를 클라이언트의 쿠키에 저장합니다. Flask-Session 확장 기능을 사용하면 세션 데이터를 서버에 저장할 수 도 있습니다.

메시지 플래싱(Message Flashing)

메시지 플래싱(Message flashing)은 Flask가 사용자에게 일회성 메시지를 보여주는 기능입니다.
즉, "한 번만 보여주고 사라지는 메시지"입니다. 로그인/로그아웃 후 사용자에게 알림 메시지를 보여주거나 폼 전송 후 메시지를 보여줄 때 유용합니다.

flash 함수를 사용하여 메시지를 세션에 임시 저장한 후, 사용자 요청시 get_flashed_messages 함수를 사용해 메시지를 꺼내서 보여줄 수 있습니다. 메시지는 한번 보여지고 사라집니다.

flash() 함수에 두 번째 인자로 'success', 'error' 같은 카테고리를 전달하면, 템플릿에서 이 값을 활용해 각 메시지를 다르게 보여줄 수 있습니다. (예: 색깔, 아이콘 등)

간단한 예제를 사용하여 테스트해봅니다.

1.app.py

from flask import Flask, render_template, request, redirect, url_for, flash, session

app = Flask(__name__)
app.secret_key = 'supersecretkey123'  # flash는 세션을 사용하므로 필수

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        if username == 'admin':
            session['username'] = username
            flash('로그인에 성공했습니다!', 'success')
            return redirect(url_for('index'))
        else:
            flash('로그인 실패. 사용자 이름이 잘못되었습니다.', 'error')
            return redirect(url_for('login'))
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.pop('username', None)
    flash('로그아웃되었습니다.', 'info')
    return redirect(url_for('index'))

if __name__ == '__main__':
    app.run(debug=True)

2.templates/layout.html

<!doctype html>
<html>
<head>
    <title>Flask Flash 예제</title>
</head>
<body>
    {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
        <ul class="flashes">
          {% for category, message in messages %}
            <li class="{{ category }}">{{ message }}</li>
          {% endfor %}
        </ul>
      {% endif %}
    {% endwith %}

    {% block content %}{% endblock %}
</body>
</html>

3.templates/index.html

{% extends "layout.html" %}
{% block content %}
<h1>홈 페이지</h1>
{% if 'username' in session %}
  <p>{{ session.username }}님, 환영합니다!</p>
  <a href="{{ url_for('logout') }}">로그아웃</a>
{% else %}
  <a href="{{ url_for('login') }}">로그인</a>
{% endif %}
{% endblock %}

4.templates/login.html

{% extends "layout.html" %}
{% block content %}
<h1>로그인</h1>
<form method="post">
    <input type="text" name="username" placeholder="사용자 이름">
    <input type="submit" value="로그인">
</form>
{% endblock %}

예제를 실행합니다.

python app.py

다음 과정을 통해 테스트합니다.

1.http://127.0.0.1:5000/에 접속합니다.
홈 페이지
로그인

2.로그인 링크를 클릭합니다.

3.사용자 이름 webnautes를 입력 후, 로그인을 클릭하면 "로그인 실패. 사용자 이름이 잘못되었습니다." 메시지가 보입니다. 메시지 플래싱으로 보여진 것입니다.

이미지


4.다시 사용자 이름 admin을 입력한 후 로그인을 클릭하면 "로그인에 성공했습니다!" 메시지가 보입니다. 메시지 플래싱으로 보여진 것입니다.

이미지


5.로그아웃 링크를 클릭하면 "로그아웃되었습니다." 메시지가 보입니다. 메시지 플래싱으로 보여진 것입니다.

이미지

로깅(Logging)

로깅(logging)은 프로그램이 실행되는 동안 일어나는 중요한 정보나 이상 징후, 에러 등을 로그로 남겨 기록하는 일입니다.

Flask는 내부적으로 Python 표준 모듈인 logging을 사용합니다.
Flask 앱 객체에는 기본적으로 logger가 포함되어 있으며, 다음처럼 바로 사용할 수 있습니다:

app.logger.debug('디버깅 메시지')
app.logger.info('일반 정보 메시지')
app.logger.warning('경고 메시지')
app.logger.error('오류 메시지')
app.logger.critical('치명적 오류 메시지')

간단한 예제를 통해 로그를 살펴봅니다.

from flask import Flask, request, abort
import logging

app = Flask(__name__)

@app.route('/submit', methods=['POST'])
def submit():
    data = request.form.get('data')

    if not data:
        app.logger.warning('비어 있는 data 필드가 제출되었습니다.')
        abort(400, 'data 필드는 필수입니다.')

    if not data.isalnum():
        app.logger.error('잘못된 형식의 data 입력: %s', data)
        return '잘못된 입력입니다.', 400

    app.logger.info('정상 입력 처리됨: %s', data)
    return f'입력받은 데이터: {data}'

@app.route('/')
def index():
    return '''
        <form method="post" action="/submit">
            <input type="text" name="data" placeholder="데이터 입력">
            <input type="submit" value="제출">
        </form>
    '''

if __name__ == '__main__':
    # 기본 로그 레벨을 DEBUG로 설정 (개발 중 디버깅용)
    app.logger.setLevel(logging.DEBUG)
    app.run(debug=True)

실행합니다.

python app.py

다음처럼 테스트를 진행합니다.

1.http://127.0.0.1:5000/에 접속합니다.

2.abc를 입력 후 제출을 클릭하면 "웹브라우저에 입력받은 데이터: abc"가 표시됩니다. 숫자나 알파벳 입력시엔 똑같이 표시됩니다. 메시지 앞에 INFO가 표시된 것을 볼 수 있습니다.

로그 메시지는 Flask가 실행되고 있는 터미널(또는 명령 프롬프트)에 출력됩니다.
[2025-07-10 19:33:08,690] INFO in app: 정상 입력 처리됨: abc

3.다시 http://127.0.0.1:5000/에 접속합니다.

4.!!을 입력 후 제출을 클릭하면 "잘못된 입력입니다."가 표시됩니다. 숫자나 알파벳이 아닌 문자 입력시 출력됩니다. 메시지 앞에 ERROR이 표시된 것을 볼 수 있습니다.

로그 메시지는 Flask가 실행되고 있는 터미널(또는 명령 프롬프트)에 출력됩니다.
[2025-07-10 19:34:56,691] ERROR in app: 잘못된 형식의 data 입력: !!

5.다시 http://127.0.0.1:5000/에 접속합니다.

6.아무것도 입력하지 않은 후 제출을 클릭하면 "Bad Request data 필드는 필수입니다."가 표시됩니다.

로그 메시지는 Flask가 실행되고 있는 터미널(또는 명령 프롬프트)에 출력됩니다.
[2025-07-10 21:25:42,308] WARNING in app: 비어 있는 data 필드가 제출되었습니다.

각 로그 레벨을 언제 사용하지에 대한 일반적인 설명은 다음과 같습니다.

레벨설명
DEBUG디버깅용 상세 정보 (예: 변수 값 확인용)
INFO일반적인 실행 흐름 정보 (예: 로그인 성공)
WARNING문제가 될 수 있는 상황 (예: 누락된 입력 등)
ERROR에러 발생했지만 실행은 계속됨 (예: 잘못된 입력)
CRITICAL프로그램이 멈출 수 있는 심각한 오류 (예: DB 연결 실패)

로그 파일로 저장하려면 다음처럼 수정하면 됩니다.

import logging
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.WARNING)
app.logger.addHandler(file_handler)

WSGI 미들웨어에 후킹(Hooking in WSGI Middleware)

WSGI는 파이썬 웹 애플리케이션(Flask, Django 등)과 웹 서버(gunicorn, uWSGI, Nginx 등) 사이에서 요청과 응답을 주고받는 표준 인터페이스입니다.
이 덕분에 웹 프레임워크와 서버가 서로 독립적으로 개발되면서도 함께 작동할 수 있습니다.

WSGI 미들웨어는 이 요청과 응답 사이에 끼어들어, 공통 기능을 자동으로 처리하거나 환경을 조정하는 역할을 합니다.
예를 들어, 프록시 뒤에서 클라이언트의 실제 IP 주소나 HTTPS 정보를 복원하거나, 요청을 기록하거나 변형하는 기능을 추가할 수 있습니다.

WSGI 미들웨어에 후킹하는 이유는, 웹 서버와 애플리케이션 사이에서 자동으로 처리해야 할 공통 작업을 끼워 넣기 위해서입니다.

예를 들어:

Nginx 같은 프록시 서버 뒤에서 요청이 들어올 때, 실제 클라이언트의 IP 주소나 HTTPS 여부를 복원하려면 ProxyFix 같은 미들웨어가 필요합니다.

gzip 압축을 적용해 응답 크기를 줄이거나, 요청/응답 로깅, CORS 처리, 세션 공유 설정 등을 미들웨어로 공통 적용할 수 있습니다.

이처럼 미들웨어는 Flask 코드 안에서 일일이 처리하지 않아도 되는 기능들을 외부에서 자동으로 적용할 수 있게 해줍니다.

Flask 확장(Extensions) 사용하기

Flask 확장(Extensions)은 Flask의 기본 기능을 필요에 따라 확장할 수 있도록 도와주는 외부 라이브러리입니다.

예를 들어, 데이터베이스 연동(SQLAlchemy), 사용자 인증(Login), 폼 처리(WTF), 마이그레이션 등 복잡한 기능들을 쉽게 추가할 수 있게 해줍니다.

자세한 내용은 다음 문서를 참고하세요.
https://flask.palletsprojects.com/en/stable/extensions/

Flask 앱 프로덕션(deploy to production) 배포

Flask 앱을 프로덕션에 배포한다는 것은, 개발이 끝난 앱을 실제 사용자들이 접속할 수 있는 서버에 안정적으로 올리는 것을 의미합니다.

Flask의 기본 서버(app.run(debug=True))는 개발자 편의를 위한 간단한 테스트용 서버라서 느리고 많은 수의 요청을 동시에 처리하기에 부족하고 보안 설정도 부족합니다.

따라서 실제 서비스용으로는 gunicorn, uWSGI 같은 WSGI 서버를 사용하고, 필요에 따라 Nginx와 같은 웹 서버를 앞단에 붙여야 합니다.

자세한 내용은 다음 문서를 참고하세요.
https://flask.palletsprojects.com/en/stable/deploying/

참고

Installation — Flask Documentation (3.1.x)
Quickstart — Flask Documentation (3.1.x)