16. Flask Web Framework Basics

16. Flask Web Framework Basics

Learning Objectives

  • Understand Flask's core concepts and structure
  • Master routing, templates, and form handling
  • Implement database integration and RESTful APIs
  • Structure large-scale applications using blueprints

1. Introduction to Flask

1.1 What is Flask?

Flask is a micro web framework written in Python.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Flask Architecture                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                              β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”‚
β”‚   β”‚ Client  │────▢│  Flask  │────▢│ Responseβ”‚              β”‚
β”‚   β”‚(Browser)β”‚     β”‚   App   β”‚     β”‚  (HTML) β”‚              β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                        β”‚                                     β”‚
β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚         β”‚              β”‚              β”‚                     β”‚
β”‚         β–Ό              β–Ό              β–Ό                     β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚   β”‚ Routing β”‚    β”‚Templatesβ”‚    β”‚Database β”‚               β”‚
β”‚   β”‚  (URL)  β”‚    β”‚ (Jinja2)β”‚    β”‚(SQLAlchemy)β”‚            β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1.2 Installation and Setup

# κ°€μƒν™˜κ²½ 생성
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# Flask μ„€μΉ˜
pip install flask

# μΆ”κ°€ νŒ¨ν‚€μ§€ (ꢌμž₯)
pip install flask-sqlalchemy flask-migrate flask-wtf python-dotenv

1.3 Minimal Flask Application

# app.py
from flask import Flask

app = Flask(__name__)

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

if __name__ == '__main__':
    app.run(debug=True)
# μ‹€ν–‰
python app.py
# λ˜λŠ”
flask run --debug

# λΈŒλΌμš°μ €μ—μ„œ http://127.0.0.1:5000 접속

2. Routing

2.1 Basic Routes

from flask import Flask

app = Flask(__name__)

# 기본 라우트
@app.route('/')
def index():
    return 'Home Page'

# 정적 경둜
@app.route('/about')
def about():
    return 'About Page'

@app.route('/contact')
def contact():
    return 'Contact Page'

2.2 Dynamic URLs

# λ¬Έμžμ—΄ λ³€μˆ˜
@app.route('/user/<username>')
def show_user(username):
    return f'User: {username}'

# μ •μˆ˜ λ³€μˆ˜
@app.route('/post/<int:post_id>')
def show_post(post_id):
    return f'Post #{post_id}'

# 경둜 λ³€μˆ˜ (μŠ¬λž˜μ‹œ 포함)
@app.route('/path/<path:subpath>')
def show_path(subpath):
    return f'Path: {subpath}'

# μ—¬λŸ¬ λ³€μˆ˜
@app.route('/user/<username>/post/<int:post_id>')
def user_post(username, post_id):
    return f'{username}\'s Post #{post_id}'

2.3 HTTP Methods

from flask import request

# GET (κΈ°λ³Έκ°’)
@app.route('/search')
def search():
    query = request.args.get('q', '')
    return f'Search: {query}'

# POST
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        # 둜그인 처리
        return f'Logging in as {username}'
    return '''
        <form method="post">
            <input name="username" placeholder="Username">
            <input name="password" type="password" placeholder="Password">
            <button type="submit">Login</button>
        </form>
    '''

# RESTful μŠ€νƒ€μΌ
@app.route('/api/users', methods=['GET', 'POST'])
def users():
    if request.method == 'GET':
        return {'users': ['Alice', 'Bob']}
    elif request.method == 'POST':
        data = request.json
        return {'created': data['name']}, 201

@app.route('/api/users/<int:user_id>', methods=['GET', 'PUT', 'DELETE'])
def user(user_id):
    if request.method == 'GET':
        return {'id': user_id, 'name': 'Alice'}
    elif request.method == 'PUT':
        data = request.json
        return {'id': user_id, 'name': data['name']}
    elif request.method == 'DELETE':
        return '', 204

2.4 URL Building

from flask import url_for, redirect

@app.route('/admin')
def admin():
    return 'Admin Page'

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

@app.route('/redirect-example')
def redirect_example():
    # url_for둜 URL 생성
    home_url = url_for('index')  # '/'
    admin_url = url_for('admin')  # '/admin'
    profile_url = url_for('profile', username='alice')  # '/user/alice'

    # λ¦¬λ‹€μ΄λ ‰νŠΈ
    return redirect(url_for('profile', username='guest'))

3. Templates (Jinja2)

3.1 Basic Templates

ν”„λ‘œμ νŠΈ ꡬ쑰:
my_app/
β”œβ”€β”€ app.py
β”œβ”€β”€ templates/
β”‚   β”œβ”€β”€ base.html
β”‚   β”œβ”€β”€ index.html
β”‚   └── user.html
└── static/
    β”œβ”€β”€ css/
    β”‚   └── style.css
    └── js/
        └── main.js
# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html', title='Home')

@app.route('/user/<name>')
def user(name):
    posts = [
        {'title': 'First Post', 'date': '2024-01-01'},
        {'title': 'Second Post', 'date': '2024-01-15'},
    ]
    return render_template('user.html', username=name, posts=posts)
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}{% endblock %} - My App</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <nav>
        <a href="{{ url_for('index') }}">Home</a>
        <a href="{{ url_for('about') }}">About</a>
    </nav>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        <p>&copy; 2024 My App</p>
    </footer>

    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
<!-- templates/index.html -->
{% extends 'base.html' %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
<h1>Welcome to {{ title }}</h1>
<p>This is the home page.</p>
{% endblock %}
<!-- templates/user.html -->
{% extends 'base.html' %}

{% block title %}{{ username }}'s Profile{% endblock %}

{% block content %}
<h1>{{ username }}'s Profile</h1>

{% if posts %}
<h2>Posts</h2>
<ul>
    {% for post in posts %}
    <li>
        <strong>{{ post.title }}</strong>
        <span>{{ post.date }}</span>
    </li>
    {% endfor %}
</ul>
{% else %}
<p>No posts yet.</p>
{% endif %}
{% endblock %}

3.2 Jinja2 Syntax

<!-- λ³€μˆ˜ 좜λ ₯ -->
<p>{{ variable }}</p>
<p>{{ user.name }}</p>
<p>{{ items[0] }}</p>

<!-- ν•„ν„° -->
<p>{{ name|upper }}</p>
<p>{{ text|truncate(100) }}</p>
<p>{{ date|datetime('%Y-%m-%d') }}</p>
<p>{{ html_content|safe }}</p>

<!-- 쑰건문 -->
{% if user.is_admin %}
    <p>Admin Panel</p>
{% elif user.is_staff %}
    <p>Staff Panel</p>
{% else %}
    <p>User Panel</p>
{% endif %}

<!-- 반볡문 -->
{% for item in items %}
    <p>{{ loop.index }}. {{ item }}</p>
{% endfor %}

<!-- loop λ³€μˆ˜ -->
{% for user in users %}
    {{ loop.index }}     {# 1λΆ€ν„° μ‹œμž‘ν•˜λŠ” 인덱슀 #}
    {{ loop.index0 }}    {# 0λΆ€ν„° μ‹œμž‘ν•˜λŠ” 인덱슀 #}
    {{ loop.first }}     {# 첫 번째 반볡이면 True #}
    {{ loop.last }}      {# λ§ˆμ§€λ§‰ 반볡이면 True #}
    {{ loop.length }}    {# 전체 ν•­λͺ© 수 #}
{% endfor %}

<!-- 맀크둜 (μž¬μ‚¬μš© κ°€λŠ₯ν•œ ν…œν”Œλ¦Ώ ν•¨μˆ˜) -->
{% macro input(name, type='text', value='') %}
    <input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}

{{ input('username') }}
{{ input('password', type='password') }}

<!-- 맀크둜 κ°€μ Έμ˜€κΈ° -->
{% from 'macros.html' import input %}

<!-- 블둝 -->
{% block sidebar %}
    <aside>Default Sidebar</aside>
{% endblock %}

<!-- λΆ€λͺ¨ 블둝 λ‚΄μš© 포함 -->
{% block content %}
    {{ super() }}
    <p>Additional content</p>
{% endblock %}

3.3 Custom Filters

# app.py
from flask import Flask
from datetime import datetime

app = Flask(__name__)

@app.template_filter('datetime')
def format_datetime(value, format='%Y-%m-%d %H:%M'):
    if isinstance(value, str):
        value = datetime.fromisoformat(value)
    return value.strftime(format)

@app.template_filter('money')
def format_money(value):
    return f'{value:,.0f}원'

# ν…œν”Œλ¦Ώμ—μ„œ μ‚¬μš©: {{ post.created_at|datetime('%Yλ…„ %mμ›” %d일') }}
# ν…œν”Œλ¦Ώμ—μ„œ μ‚¬μš©: {{ price|money }}

4. Form Handling

4.1 Basic Form Handling

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

app = Flask(__name__)
app.secret_key = 'your-secret-key'  # flash λ©”μ‹œμ§€μš©

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    if request.method == 'POST':
        name = request.form.get('name')
        email = request.form.get('email')
        message = request.form.get('message')

        # κ°„λ‹¨ν•œ μœ νš¨μ„± 검사
        if not name or not email:
            flash('이름과 이메일은 ν•„μˆ˜μž…λ‹ˆλ‹€.', 'error')
            return redirect(url_for('contact'))

        # 데이터 처리 (예: 이메일 전솑, DB μ €μž₯)
        flash('λ©”μ‹œμ§€κ°€ μ „μ†‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 'success')
        return redirect(url_for('index'))

    return render_template('contact.html')
<!-- templates/contact.html -->
{% extends 'base.html' %}

{% block content %}
<h1>Contact Us</h1>

{% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
        {% for category, message in messages %}
            <div class="alert alert-{{ category }}">{{ message }}</div>
        {% endfor %}
    {% endif %}
{% endwith %}

<form method="POST">
    <div>
        <label for="name">Name:</label>
        <input type="text" id="name" name="name" required>
    </div>
    <div>
        <label for="email">Email:</label>
        <input type="email" id="email" name="email" required>
    </div>
    <div>
        <label for="message">Message:</label>
        <textarea id="message" name="message" rows="5"></textarea>
    </div>
    <button type="submit">Send</button>
</form>
{% endblock %}

4.2 Form Handling with Flask-WTF

pip install flask-wtf email-validator
# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextAreaField, SelectField, BooleanField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(message='μ‚¬μš©μžλͺ…을 μž…λ ₯ν•˜μ„Έμš”.'),
        Length(min=3, max=20, message='3-20자 μ‚¬μ΄λ‘œ μž…λ ₯ν•˜μ„Έμš”.')
    ])
    password = PasswordField('Password', validators=[
        DataRequired(message='λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜μ„Έμš”.')
    ])
    remember = BooleanField('Remember Me')

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(),
        Length(min=3, max=20)
    ])
    email = StringField('Email', validators=[
        DataRequired(),
        Email(message='μœ νš¨ν•œ 이메일 μ£Όμ†Œλ₯Ό μž…λ ₯ν•˜μ„Έμš”.')
    ])
    password = PasswordField('Password', validators=[
        DataRequired(),
        Length(min=8, message='λΉ„λ°€λ²ˆν˜ΈλŠ” μ΅œμ†Œ 8자 이상이어야 ν•©λ‹ˆλ‹€.')
    ])
    confirm_password = PasswordField('Confirm Password', validators=[
        DataRequired(),
        EqualTo('password', message='λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.')
    ])

    def validate_username(self, field):
        # μ»€μŠ€ν…€ μœ νš¨μ„± 검사
        if field.data.lower() in ['admin', 'root', 'administrator']:
            raise ValidationError('μ‚¬μš©ν•  수 μ—†λŠ” μ‚¬μš©μžλͺ…μž…λ‹ˆλ‹€.')

class ContactForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    category = SelectField('Category', choices=[
        ('general', '일반 문의'),
        ('support', '기술 지원'),
        ('feedback', 'ν”Όλ“œλ°±')
    ])
    message = TextAreaField('Message', validators=[
        DataRequired(),
        Length(min=10, max=1000)
    ])
# app.py
from flask import Flask, render_template, redirect, url_for, flash
from forms import LoginForm, RegistrationForm

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'

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

    if form.validate_on_submit():
        # form.username.data, form.password.data μ‚¬μš©
        username = form.username.data
        # 둜그인 둜직...
        flash(f'{username}λ‹˜, ν™˜μ˜ν•©λ‹ˆλ‹€!', 'success')
        return redirect(url_for('index'))

    return render_template('login.html', form=form)

@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()

    if form.validate_on_submit():
        # νšŒμ›κ°€μž… 둜직...
        flash('νšŒμ›κ°€μž…μ΄ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 'success')
        return redirect(url_for('login'))

    return render_template('register.html', form=form)
<!-- templates/login.html -->
{% extends 'base.html' %}

{% block content %}
<h1>Login</h1>

<form method="POST" novalidate>
    {{ form.hidden_tag() }}

    <div class="form-group">
        {{ form.username.label }}
        {{ form.username(class="form-control") }}
        {% for error in form.username.errors %}
            <span class="error">{{ error }}</span>
        {% endfor %}
    </div>

    <div class="form-group">
        {{ form.password.label }}
        {{ form.password(class="form-control") }}
        {% for error in form.password.errors %}
            <span class="error">{{ error }}</span>
        {% endfor %}
    </div>

    <div class="form-check">
        {{ form.remember(class="form-check-input") }}
        {{ form.remember.label(class="form-check-label") }}
    </div>

    <button type="submit" class="btn btn-primary">Login</button>
</form>
{% endblock %}

4.3 File Upload

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

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB μ œν•œ
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}

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

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    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)
            filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
            file.save(filepath)
            flash(f'{filename} μ—…λ‘œλ“œ μ™„λ£Œ!')
            return redirect(url_for('index'))
        else:
            flash('ν—ˆμš©λ˜μ§€ μ•ŠλŠ” 파일 ν˜•μ‹μž…λ‹ˆλ‹€.')
            return redirect(request.url)

    return '''
    <form method="post" enctype="multipart/form-data">
        <input type="file" name="file">
        <input type="submit" value="Upload">
    </form>
    '''

5. Database (Flask-SQLAlchemy)

5.1 Configuration and Model Definition

# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# λͺ¨λΈ μ •μ˜
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    # 관계: 1:N
    posts = db.relationship('Post', backref='author', lazy='dynamic')

    def __repr__(self):
        return f'<User {self.username}>'

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    # μ™Έλž˜ ν‚€
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    # 관계: N:M (νƒœκ·Έ)
    tags = db.relationship('Tag', secondary='post_tags', backref='posts')

    def __repr__(self):
        return f'<Post {self.title}>'

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)

# λ‹€λŒ€λ‹€ 관계λ₯Ό μœ„ν•œ μ—°κ²° ν…Œμ΄λΈ”
post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)

# λ°μ΄ν„°λ² μ΄μŠ€ 생성
with app.app_context():
    db.create_all()

5.2 CRUD Operations

# Create
user = User(username='alice', email='alice@example.com')
db.session.add(user)
db.session.commit()

# μ—¬λŸ¬ 개 μΆ”κ°€
users = [
    User(username='bob', email='bob@example.com'),
    User(username='charlie', email='charlie@example.com')
]
db.session.add_all(users)
db.session.commit()

# Read
# 전체 쑰회
all_users = User.query.all()

# ID둜 쑰회
user = User.query.get(1)  # deprecated in SQLAlchemy 2.0
user = db.session.get(User, 1)  # SQLAlchemy 2.0+

# 쑰건 쑰회
user = User.query.filter_by(username='alice').first()
users = User.query.filter(User.email.like('%@example.com')).all()

# μ •λ ¬
users = User.query.order_by(User.created_at.desc()).all()

# νŽ˜μ΄μ§€λ„€μ΄μ…˜
page = User.query.paginate(page=1, per_page=10)
# page.items, page.has_next, page.has_prev, page.pages

# Update
user = User.query.filter_by(username='alice').first()
user.email = 'newemail@example.com'
db.session.commit()

# λ˜λŠ” bulk update
User.query.filter_by(username='alice').update({'email': 'new@example.com'})
db.session.commit()

# Delete
user = User.query.filter_by(username='alice').first()
db.session.delete(user)
db.session.commit()

# λ˜λŠ” bulk delete
User.query.filter(User.created_at < some_date).delete()
db.session.commit()

5.3 Using in Routes

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

@app.route('/users')
def user_list():
    page = request.args.get('page', 1, type=int)
    users = User.query.order_by(User.created_at.desc()).paginate(
        page=page, per_page=20, error_out=False
    )
    return render_template('users/list.html', users=users)

@app.route('/users/<int:user_id>')
def user_detail(user_id):
    user = db.session.get(User, user_id)
    if user is None:
        abort(404)
    return render_template('users/detail.html', user=user)

@app.route('/users/create', methods=['GET', 'POST'])
def user_create():
    if request.method == 'POST':
        username = request.form['username']
        email = request.form['email']

        # 쀑볡 확인
        if User.query.filter_by(username=username).first():
            flash('이미 μ‘΄μž¬ν•˜λŠ” μ‚¬μš©μžλͺ…μž…λ‹ˆλ‹€.', 'error')
            return redirect(url_for('user_create'))

        user = User(username=username, email=email)
        db.session.add(user)
        db.session.commit()

        flash('μ‚¬μš©μžκ°€ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 'success')
        return redirect(url_for('user_detail', user_id=user.id))

    return render_template('users/create.html')

@app.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
def user_edit(user_id):
    user = db.session.get(User, user_id)
    if user is None:
        abort(404)

    if request.method == 'POST':
        user.email = request.form['email']
        db.session.commit()
        flash('μ‚¬μš©μž 정보가 μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 'success')
        return redirect(url_for('user_detail', user_id=user.id))

    return render_template('users/edit.html', user=user)

@app.route('/users/<int:user_id>/delete', methods=['POST'])
def user_delete(user_id):
    user = db.session.get(User, user_id)
    if user is None:
        abort(404)

    db.session.delete(user)
    db.session.commit()
    flash('μ‚¬μš©μžκ°€ μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 'success')
    return redirect(url_for('user_list'))

5.4 Migrations (Flask-Migrate)

pip install flask-migrate
# app.py
from flask_migrate import Migrate

app = Flask(__name__)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ΄ˆκΈ°ν™”
flask db init

# λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 생성
flask db migrate -m "Initial migration"

# λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 적용
flask db upgrade

# λ‘€λ°±
flask db downgrade

6. Sessions and Cookies

6.1 Using Sessions

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

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

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    # 인증 둜직...
    session['user_id'] = user.id
    session['username'] = user.username
    session.permanent = True  # μ„Έμ…˜ μœ μ§€ (κΈ°λ³Έ 31일)
    return redirect(url_for('dashboard'))

@app.route('/logout')
def logout():
    session.clear()
    # λ˜λŠ” νŠΉμ • ν‚€λ§Œ μ‚­μ œ
    # session.pop('user_id', None)
    return redirect(url_for('index'))

@app.route('/dashboard')
def dashboard():
    if 'user_id' not in session:
        return redirect(url_for('login'))
    return f'Welcome, {session["username"]}!'

6.2 Using Cookies

from flask import make_response, request

@app.route('/set-cookie')
def set_cookie():
    response = make_response('Cookie set!')
    response.set_cookie(
        'theme',
        'dark',
        max_age=60*60*24*30,  # 30일
        httponly=True,
        secure=True,  # HTTPSμ—μ„œλ§Œ
        samesite='Lax'
    )
    return response

@app.route('/get-cookie')
def get_cookie():
    theme = request.cookies.get('theme', 'light')
    return f'Current theme: {theme}'

@app.route('/delete-cookie')
def delete_cookie():
    response = make_response('Cookie deleted!')
    response.delete_cookie('theme')
    return response

7. Blueprints

7.1 Blueprint Structure

my_app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ models.py
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ routes.py
β”‚   β”‚   β”œβ”€β”€ forms.py
β”‚   β”‚   └── templates/
β”‚   β”‚       └── auth/
β”‚   β”‚           β”œβ”€β”€ login.html
β”‚   β”‚           └── register.html
β”‚   β”œβ”€β”€ main/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ routes.py
β”‚   β”‚   └── templates/
β”‚   β”‚       └── main/
β”‚   β”‚           └── index.html
β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   └── routes.py
β”‚   β”œβ”€β”€ templates/
β”‚   β”‚   └── base.html
β”‚   └── static/
β”‚       β”œβ”€β”€ css/
β”‚       └── js/
β”œβ”€β”€ config.py
β”œβ”€β”€ requirements.txt
└── run.py

7.2 Defining Blueprints

# app/auth/__init__.py
from flask import Blueprint

auth_bp = Blueprint('auth', __name__,
                    template_folder='templates',
                    url_prefix='/auth')

from . import routes
# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from . import auth_bp
from .forms import LoginForm, RegistrationForm

@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # 둜그인 둜직
        return redirect(url_for('main.index'))
    return render_template('auth/login.html', form=form)

@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        # νšŒμ›κ°€μž… 둜직
        flash('νšŒμ›κ°€μž…μ΄ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', form=form)

@auth_bp.route('/logout')
def logout():
    # λ‘œκ·Έμ•„μ›ƒ 둜직
    return redirect(url_for('main.index'))
# app/main/__init__.py
from flask import Blueprint

main_bp = Blueprint('main', __name__,
                    template_folder='templates')

from . import routes
# app/main/routes.py
from flask import render_template
from . import main_bp

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

@main_bp.route('/about')
def about():
    return render_template('main/about.html')

7.3 Application Factory

# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config

db = SQLAlchemy()
migrate = Migrate()

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    # ν™•μž₯ μ΄ˆκΈ°ν™”
    db.init_app(app)
    migrate.init_app(app, db)

    # λΈ”λ£¨ν”„λ¦°νŠΈ 등둝
    from app.auth import auth_bp
    from app.main import main_bp
    from app.api import api_bp

    app.register_blueprint(auth_bp)
    app.register_blueprint(main_bp)
    app.register_blueprint(api_bp, url_prefix='/api')

    # μ—λŸ¬ ν•Έλ“€λŸ¬ 등둝
    from app.errors import register_error_handlers
    register_error_handlers(app)

    return app
# config.py
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevelopmentConfig(Config):
    DEBUG = True

class ProductionConfig(Config):
    DEBUG = False

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
# run.py
from app import create_app

app = create_app()

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

8. RESTful API

8.1 JSON Responses

from flask import Flask, jsonify, request, abort

app = Flask(__name__)

# μƒ˜ν”Œ 데이터
books = [
    {'id': 1, 'title': 'Flask Web Development', 'author': 'Miguel Grinberg'},
    {'id': 2, 'title': 'Python Crash Course', 'author': 'Eric Matthes'},
]

@app.route('/api/books', methods=['GET'])
def get_books():
    return jsonify({'books': books})

@app.route('/api/books/<int:book_id>', methods=['GET'])
def get_book(book_id):
    book = next((b for b in books if b['id'] == book_id), None)
    if book is None:
        abort(404)
    return jsonify(book)

@app.route('/api/books', methods=['POST'])
def create_book():
    if not request.json or 'title' not in request.json:
        abort(400)

    book = {
        'id': books[-1]['id'] + 1 if books else 1,
        'title': request.json['title'],
        'author': request.json.get('author', '')
    }
    books.append(book)
    return jsonify(book), 201

@app.route('/api/books/<int:book_id>', methods=['PUT'])
def update_book(book_id):
    book = next((b for b in books if b['id'] == book_id), None)
    if book is None:
        abort(404)
    if not request.json:
        abort(400)

    book['title'] = request.json.get('title', book['title'])
    book['author'] = request.json.get('author', book['author'])
    return jsonify(book)

@app.route('/api/books/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
    book = next((b for b in books if b['id'] == book_id), None)
    if book is None:
        abort(404)
    books.remove(book)
    return '', 204

# μ—λŸ¬ ν•Έλ“€λŸ¬
@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not found'}), 404

@app.errorhandler(400)
def bad_request(error):
    return jsonify({'error': 'Bad request'}), 400

8.2 API Authentication (JWT)

pip install flask-jwt-extended
from flask import Flask, jsonify, request
from flask_jwt_extended import (
    JWTManager, create_access_token, create_refresh_token,
    jwt_required, get_jwt_identity, get_jwt
)
from datetime import timedelta

app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'your-jwt-secret'
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1)
app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=30)

jwt = JWTManager(app)

# 토큰 λΈ”λž™λ¦¬μŠ€νŠΈ (λ‘œκ·Έμ•„μ›ƒλœ 토큰)
blacklist = set()

@jwt.token_in_blocklist_loader
def check_if_token_in_blocklist(jwt_header, jwt_payload):
    return jwt_payload['jti'] in blacklist

@app.route('/api/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')

    # μ‚¬μš©μž 인증 둜직
    user = authenticate(username, password)
    if not user:
        return jsonify({'error': 'Invalid credentials'}), 401

    access_token = create_access_token(identity=user.id)
    refresh_token = create_refresh_token(identity=user.id)

    return jsonify({
        'access_token': access_token,
        'refresh_token': refresh_token
    })

@app.route('/api/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refresh():
    identity = get_jwt_identity()
    access_token = create_access_token(identity=identity)
    return jsonify({'access_token': access_token})

@app.route('/api/logout', methods=['POST'])
@jwt_required()
def logout():
    jti = get_jwt()['jti']
    blacklist.add(jti)
    return jsonify({'message': 'Logged out'})

@app.route('/api/protected', methods=['GET'])
@jwt_required()
def protected():
    current_user_id = get_jwt_identity()
    return jsonify({'user_id': current_user_id})

8.3 API Documentation (Flask-RESTX)

pip install flask-restx
from flask import Flask
from flask_restx import Api, Resource, fields

app = Flask(__name__)
api = Api(app, version='1.0', title='Book API',
          description='A simple Book API')

ns = api.namespace('books', description='Book operations')

book_model = api.model('Book', {
    'id': fields.Integer(readonly=True, description='Unique identifier'),
    'title': fields.String(required=True, description='Book title'),
    'author': fields.String(required=True, description='Book author'),
})

books = []

@ns.route('/')
class BookList(Resource):
    @ns.doc('list_books')
    @ns.marshal_list_with(book_model)
    def get(self):
        '''List all books'''
        return books

    @ns.doc('create_book')
    @ns.expect(book_model)
    @ns.marshal_with(book_model, code=201)
    def post(self):
        '''Create a new book'''
        book = api.payload
        book['id'] = len(books) + 1
        books.append(book)
        return book, 201

@ns.route('/<int:id>')
@ns.response(404, 'Book not found')
@ns.param('id', 'The book identifier')
class Book(Resource):
    @ns.doc('get_book')
    @ns.marshal_with(book_model)
    def get(self, id):
        '''Get a book by ID'''
        book = next((b for b in books if b['id'] == id), None)
        if book is None:
            api.abort(404, 'Book not found')
        return book

    @ns.doc('delete_book')
    @ns.response(204, 'Book deleted')
    def delete(self, id):
        '''Delete a book'''
        global books
        books = [b for b in books if b['id'] != id]
        return '', 204

if __name__ == '__main__':
    app.run(debug=True)
    # Swagger UI: http://localhost:5000/

9. Error Handling

9.1 Error Handlers

from flask import Flask, render_template, jsonify, request

app = Flask(__name__)

@app.errorhandler(404)
def not_found(error):
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Not found'}), 404
    return render_template('errors/404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()  # νŠΈλžœμž­μ…˜ λ‘€λ°±
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Internal server error'}), 500
    return render_template('errors/500.html'), 500

@app.errorhandler(403)
def forbidden(error):
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Forbidden'}), 403
    return render_template('errors/403.html'), 403

# μ»€μŠ€ν…€ μ˜ˆμ™Έ
class ValidationError(Exception):
    def __init__(self, message, status_code=400):
        super().__init__()
        self.message = message
        self.status_code = status_code

@app.errorhandler(ValidationError)
def handle_validation_error(error):
    response = jsonify({'error': error.message})
    response.status_code = error.status_code
    return response

# μ‚¬μš©
@app.route('/api/validate', methods=['POST'])
def validate():
    if 'name' not in request.json:
        raise ValidationError('Name is required')
    return jsonify({'valid': True})

9.2 Logging

import logging
from logging.handlers import RotatingFileHandler
import os

def setup_logging(app):
    if not app.debug:
        if not os.path.exists('logs'):
            os.mkdir('logs')

        file_handler = RotatingFileHandler(
            'logs/app.log',
            maxBytes=10240000,  # 10MB
            backupCount=10
        )
        file_handler.setFormatter(logging.Formatter(
            '%(asctime)s %(levelname)s: %(message)s '
            '[in %(pathname)s:%(lineno)d]'
        ))
        file_handler.setLevel(logging.INFO)
        app.logger.addHandler(file_handler)

        app.logger.setLevel(logging.INFO)
        app.logger.info('Application startup')

# μ‚¬μš©
@app.route('/api/action')
def action():
    app.logger.info('Action performed by user')
    try:
        # 둜직
        pass
    except Exception as e:
        app.logger.error(f'Error occurred: {e}')
        raise

10. Testing

10.1 Testing with pytest

# tests/conftest.py
import pytest
from app import create_app, db

@pytest.fixture
def app():
    app = create_app('config.TestingConfig')

    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

@pytest.fixture
def client(app):
    return app.test_client()

@pytest.fixture
def runner(app):
    return app.test_cli_runner()
# tests/test_routes.py
def test_index(client):
    response = client.get('/')
    assert response.status_code == 200
    assert b'Welcome' in response.data

def test_login_page(client):
    response = client.get('/auth/login')
    assert response.status_code == 200

def test_login(client):
    response = client.post('/auth/login', data={
        'username': 'testuser',
        'password': 'testpass'
    }, follow_redirects=True)
    assert response.status_code == 200

def test_api_get_books(client):
    response = client.get('/api/books')
    assert response.status_code == 200
    assert response.content_type == 'application/json'

def test_api_create_book(client):
    response = client.post('/api/books',
        json={'title': 'Test Book', 'author': 'Test Author'}
    )
    assert response.status_code == 201
    data = response.get_json()
    assert data['title'] == 'Test Book'

10.2 Running Tests

# ν…ŒμŠ€νŠΈ μ‹€ν–‰
pytest

# 상세 좜λ ₯
pytest -v

# 컀버리지
pip install pytest-cov
pytest --cov=app --cov-report=html

11. Deployment

11.1 Production Configuration

# config.py
import os

class ProductionConfig:
    SECRET_KEY = os.environ['SECRET_KEY']
    SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    # λ³΄μ•ˆ μ„€μ •
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = 'Lax'

11.2 Running with Gunicorn

pip install gunicorn

# μ‹€ν–‰
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"

# λ˜λŠ” gunicorn.conf.py μ‚¬μš©
gunicorn -c gunicorn.conf.py "app:create_app()"
# gunicorn.conf.py
bind = '0.0.0.0:8000'
workers = 4
threads = 2
worker_class = 'gthread'
timeout = 120
keepalive = 5
errorlog = 'logs/gunicorn-error.log'
accesslog = 'logs/gunicorn-access.log'
loglevel = 'info'

11.3 Docker Deployment

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV FLASK_APP=run.py
ENV FLASK_ENV=production

EXPOSE 8000

CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:create_app()"]
# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - DATABASE_URL=postgresql://user:pass@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=app
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

12. Practical Example: Blog Application

For a complete blog application example with full structure and key files, refer to the examples/Web_Development/flask_blog/ folder.


Practice Problems

Exercise 1: Basic CRUD App

Create a simple Todo list application: - View list of todos - Add new todo - Mark as completed - Delete

Exercise 2: User Authentication

Add user authentication to the above app: - Registration/login/logout - Each user can only see their own todos

Exercise 3: REST API

Create a Todo list API: - CRUD endpoints - JWT authentication - Swagger documentation


References

to navigate between lessons