Flask는 왜 모든 기능을 기본으로 넣지 않을까
Flask를 처음 쓰면 이런 의문이 생길 수 있습니다.
"왜 데이터베이스 기능을 직접 연결해야 하지?"
"왜 폼 검증을 함수로 따로 만들어야 하지?"
"왜 로그인 기능이 처음부터 들어 있지 않지?"
이것은 Flask가 부족해서라기보다, 작게 시작하고 필요한 것만 붙이는 방식을 선택했기 때문입니다. Flask 공식 문서도 Flask를 가볍게 시작할 수 있지만 복잡한 애플리케이션으로 확장할 수 있는 프레임워크라고 설명합니다. 또한 필요한 기능은 커뮤니티 확장 기능으로 더할 수 있다고 안내합니다.1
비유하자면 Flask는 "완성된 대형 주방"보다 "기본 도구가 잘 갖춰진 작은 주방"에 가깝습니다. 처음부터 제빵기, 에스프레소 머신, 대형 오븐을 모두 들여놓지 않습니다. 빵을 만들 일이 많아지면 제빵기를 들이고, 커피를 팔게 되면 에스프레소 머신을 추가합니다.
웹 애플리케이션도 마찬가지입니다. 어떤 프로젝트는 로그인 없이 게시글만 보여 주면 됩니다. 어떤 프로젝트는 데이터베이스가 필요하지만 복잡한 ORM은 필요 없습니다. 또 어떤 프로젝트는 폼이 많아져서 검증 규칙을 체계적으로 관리해야 합니다.
Flask는 이런 선택을 개발자에게 맡깁니다. 초보자에게는 처음에 조금 불친절해 보일 수 있지만, 익숙해지면 이것이 큰 장점이 됩니다.
AI 생성 이미지
확장 라이브러리란 무엇인가
Flask 확장 라이브러리는 Flask에 특정 기능을 덧붙이는 파이썬 패키지입니다. 보통 다른 패키지와 마찬가지로 pip로 설치합니다.
python -m pip install Flask-SQLAlchemy
python -m pip install Flask-WTF
python -m pip install Flask-Login확장 라이브러리는 대체로 다음 흐름으로 사용합니다.
# board/extensions.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()# board/__init__.py
from flask import Flask
from .extensions import db
def create_app():
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///board.sqlite3"
db.init_app(app)
return app여기서 중요한 점은 create_app() 안에서 앱을 만든 뒤 확장 기능을 연결한다는 것입니다. 앞에서 배운 애플리케이션 팩토리 구조와 잘 어울립니다.
db = SQLAlchemy()라고 쓰면 처음에는 "여기서 바로 데이터베이스에 연결되는 건가?" 하고 생각할 수 있습니다. 하지만 이 객체는 데이터베이스 연결 그 자체라기보다, Flask 앱과 SQLAlchemy를 이어 주는 확장 객체에 가깝습니다. 실제 연결 정보는 app.config["SQLALCHEMY_DATABASE_URI"]에 넣고, db.init_app(app)을 호출하면서 현재 Flask 앱과 연결됩니다.
즉, 흐름을 간단히 말하면 이렇습니다.
extensions.py에서 확장 객체를 미리 만든다.create_app()에서 Flask 앱을 만든다.설정값을 넣는다.
init_app()으로 앱과 확장 객체를 연결한다.
이 방식은 확장 라이브러리가 많아졌을 때도 프로젝트 구조를 깔끔하게 유지하는 데 도움이 됩니다.
Flask-SQLAlchemy: SQL을 직접 쓰는 방식과 ORM 방식
이전에는 SQLite를 사용할 때 SQL을 직접 작성했습니다.
db.execute(
"SELECT id, title, author, content FROM posts WHERE id = ?",
(post_id,)
).fetchone()이 방식은 SQL이 눈에 그대로 보입니다. 초보자가 데이터베이스의 기본 원리를 배우기 좋습니다. SELECT, INSERT, WHERE, commit()이 어떤 역할을 하는지 명확히 알 수 있습니다.
반면 Flask-SQLAlchemy는 SQLAlchemy라는 데이터베이스 도구를 Flask에서 편하게 쓰도록 연결해 주는 확장입니다. SQLAlchemy의 ORM 기능을 사용하면 테이블을 파이썬 클래스로 표현할 수 있습니다.2
ORM은 Object Relational Mapping의 줄임말입니다. 말이 어렵지만 핵심은 간단합니다.
데이터베이스 테이블의 한 행을 파이썬 객체처럼 다루게 해 주는 방식입니다.
예를 들어 게시글 테이블을 다음처럼 표현할 수 있습니다.
# board/models.py
from .extensions import db
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
author = db.Column(db.String(50), nullable=False)
content = db.Column(db.Text, nullable=False)이 코드에서 Post 클래스는 게시글 테이블을 표현합니다. id, title, author, content는 테이블의 열에 해당합니다. 다만 모델 클래스를 작성했다고 해서 데이터베이스 테이블이 저절로 만들어지는 것은 아닙니다. 실제 프로젝트에서는 테이블을 만드는 작업이나 마이그레이션 도구를 함께 사용해야 합니다. 여기서는 "테이블을 파이썬 클래스로 표현할 수 있다"는 감각만 먼저 잡으면 충분합니다.
이제 새 게시글을 저장할 때 SQL 문자열을 직접 쓰지 않고 객체를 만들 수 있습니다.
post = Post(
title="첫 번째 글",
author="지민",
content="Flask 확장 기능을 배우는 중입니다."
)
db.session.add(post)
db.session.commit()직접 SQL 방식은 다음과 비슷했습니다.
db.execute(
"INSERT INTO posts (title, author, content) VALUES (?, ?, ?)",
(title, author, content)
)
db.commit()두 방식의 차이를 정리하면 이렇습니다.
| 방식 | 장점 | 주의할 점 |
|---|---|---|
| 직접 SQL | SQL이 명확히 보인다. 동작 원리를 배우기 좋다. | 쿼리가 많아지면 문자열 관리가 복잡해진다. |
| ORM | 파이썬 객체처럼 데이터를 다룰 수 있다. 반복 코드가 줄어든다. | 실제 SQL이 바로 보이지 않아 데이터베이스 이해가 약해질 수 있다. |
ORM이 항상 더 좋은 것은 아닙니다. 지금처럼 작은 게시판에서는 직접 SQL도 충분히 좋은 선택입니다. 다만 회원, 게시글, 댓글, 좋아요처럼 테이블이 늘고 관계가 복잡해지면 ORM이 코드를 정리하는 데 도움이 됩니다.
초보자에게 중요한 기준은 이것입니다.
SQL을 모른 채 ORM으로 넘어가지 말고, SQL의 기본 흐름을 이해한 뒤 ORM을 도구로 사용하세요.
Flask-WTF: 폼 검증과 CSRF 보호를 체계적으로 다루기
앞에서는 폼 데이터를 직접 읽고 validate_post() 함수를 만들어 검증했습니다.
title = request.form.get("title", "").strip()
content = request.form.get("content", "").strip()
errors = validate_post(title, content)폼이 한두 개일 때는 이 방식이 단순합니다. 하지만 회원가입, 로그인, 게시글 작성, 댓글 작성, 프로필 수정 폼이 생기면 검증 코드가 여러 라우트에 흩어집니다.
Flask-WTF는 Flask와 WTForms를 연결해 폼 클래스를 만들고, 검증 규칙과 CSRF 보호를 함께 관리할 수 있게 도와주는 확장입니다. Flask-WTF는 WTForms 통합, CSRF 토큰을 이용한 안전한 폼, 파일 업로드, reCAPTCHA 같은 기능을 제공합니다.3
예를 들어 게시글 작성 폼을 클래스로 표현하면 다음처럼 됩니다.
# board/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
class PostForm(FlaskForm):
title = StringField(
"제목",
validators=[DataRequired(), Length(max=100)]
)
content = TextAreaField(
"내용",
validators=[DataRequired()]
)
submit = SubmitField("저장")라우트에서는 폼 객체를 만들고 검증 결과만 확인합니다.
@bp.route("/create", methods=["GET", "POST"])
def create():
form = PostForm()
if form.validate_on_submit():
# form.title.data, form.content.data를 사용해 저장
flash("게시글이 저장되었습니다.")
return redirect(url_for("posts.list"))
return render_template("posts/create.html", form=form)validate_on_submit()은 현재 요청이 폼 제출 요청인지 확인하고, 필드별 검증 규칙도 함께 실행합니다. 덕분에 라우트 함수가 조금 더 읽기 쉬워집니다.
템플릿에서는 CSRF 토큰을 함께 넣습니다.
<form method="post">
{{ form.csrf_token }}
{{ form.title.label }}
{{ form.title() }}
{{ form.content.label }}
{{ form.content() }}
{{ form.submit() }}
</form>CSRF는 Cross-Site Request Forgery의 줄임말입니다. 한국어로는 사이트 간 요청 위조라고 부릅니다. 사용자가 로그인한 상태를 악용해, 사용자가 원하지 않은 요청을 몰래 보내는 공격입니다.
쉽게 말하면 누군가 사용자의 도장을 몰래 찍어 서류를 제출하는 상황과 비슷합니다. CSRF 토큰은 "이 폼이 우리 사이트에서 정상적으로 만들어진 폼인가?"를 확인하는 임시 확인표 역할을 합니다.
Flask-WTF를 쓰려면 SECRET_KEY 설정도 필요합니다. CSRF 토큰을 안전하게 만들기 위해 비밀 키가 사용되기 때문입니다.
app.config["SECRET_KEY"] = "dev-secret-key"실제 운영 환경에서는 이 값을 코드에 직접 적지 말고 환경 변수나 별도 설정 파일로 관리해야 합니다. 예제에서는 이해를 돕기 위해 문자열을 바로 적었지만, 실제 서비스의 비밀 키는 외부에 노출되면 안 됩니다.
Flask-Login: 세션 기반 로그인 관리를 단순화하기
로그인은 생각보다 반복 작업이 많습니다.
사용자가 로그인했는지 확인하기
로그인한 사용자 정보를 템플릿에서 사용하기
로그아웃 처리하기
로그인하지 않은 사용자를 로그인 페이지로 보내기
필요한 경우 "로그인 상태 유지" 기능 제공하기
Flask-Login은 이런 세션 기반 로그인 관리를 도와주는 확장입니다. 공식 문서에 따르면 Flask-Login은 로그인, 로그아웃, 사용자 세션 기억 기능을 제공하지만 비밀번호 확인, 회원가입, 사용자 저장 방식까지 대신 처리하지는 않습니다.4
즉, Flask-Login은 "사용자가 누구인지 기억하는 일"을 도와줍니다. "비밀번호가 맞는지 검사하는 일"과 "사용자 정보를 데이터베이스에 저장하는 일"은 여전히 우리 애플리케이션의 책임입니다.
기본 흐름은 다음과 같습니다.
# board/extensions.py
from flask_login import LoginManager
login_manager = LoginManager()# board/__init__.py
from flask import Flask
from .extensions import login_manager
def create_app():
app = Flask(__name__)
app.config["SECRET_KEY"] = "dev-secret-key"
login_manager.init_app(app)
login_manager.login_view = "auth.login"
return applogin_manager.login_view에는 로그인하지 않은 사용자가 @login_required 페이지에 접근했을 때 이동할 로그인 라우트 이름을 넣습니다. 위 예시에서는 인증 기능을 auth 블루프린트로 나누고, 그 안에 login 라우트가 있다고 가정했습니다.
사용자 모델도 Flask-Login이 알아볼 수 있는 형태여야 합니다. 보통은 UserMixin을 함께 상속해서 필요한 기본 속성과 메서드를 쉽게 갖추게 합니다.
# board/models.py
from flask_login import UserMixin
from .extensions import db
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False)
username = db.Column(db.String(50), nullable=False)그리고 로그인한 사용자를 다시 불러오는 함수가 필요합니다.
# board/auth.py 또는 사용자 로딩 코드를 모아 둔 파일
from .extensions import db, login_manager
from .models import User
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))이 함수는 세션에 저장된 사용자 id를 보고 실제 사용자 객체를 찾아옵니다. Flask-Login은 세션에 "현재 로그인한 사용자의 id"를 저장하고, 요청이 들어올 때마다 이 id로 사용자를 복원합니다.
로그인 성공 시에는 login_user()를 호출합니다.
from flask import flash, redirect, render_template, request, url_for
from flask_login import login_user, logout_user, login_required, current_user
@bp.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form.get("email", "").strip()
password = request.form.get("password", "")
# 이메일과 비밀번호 확인은 직접 구현해야 함
user = find_user_by_email(email)
if user and user.check_password(password):
login_user(user)
return redirect(url_for("posts.list"))
flash("이메일 또는 비밀번호가 올바르지 않습니다.")
return render_template("auth/login.html")여기서 find_user_by_email()과 user.check_password()는 예시 이름입니다. 실제 프로젝트에서는 데이터베이스에서 사용자를 찾고, 저장된 비밀번호 해시와 사용자가 입력한 비밀번호를 비교하는 코드를 직접 작성해야 합니다.
로그아웃은 간단합니다.
@bp.route("/logout")
def logout():
logout_user()
return redirect(url_for("posts.list"))예제에서는 흐름을 단순하게 보여 주기 위해 GET /logout 형태로 작성했습니다. 실제 서비스에서는 CSRF를 함께 고려해 로그아웃도 POST 요청으로 처리하는 방식을 선택할 수 있습니다.
로그인한 사용자만 접근할 수 있는 페이지는 @login_required를 붙입니다.
@bp.route("/create", methods=["GET", "POST"])
@login_required
def create():
return render_template("posts/create.html")템플릿에서는 current_user로 현재 사용자를 확인할 수 있습니다.
{% if current_user.is_authenticated %}
<p>{{ current_user.username }}님, 환영합니다.</p>
{% else %}
<a href="{{ url_for('auth.login') }}">로그인</a>
{% endif %}사용자가 "로그인 상태 유지"를 선택했을 때는 login_user(user, remember=True)처럼 기억하기 옵션을 켤 수 있습니다. 이 기능은 사용자가 브라우저를 닫았다가 다시 열어도 일정 기간 로그인 상태를 복원하는 데 쓰입니다. 다만 항상 자동으로 켜지는 기능은 아니므로, 서비스 정책에 맞게 선택해야 합니다.
이처럼 Flask-Login은 로그인 상태를 다루는 반복 코드를 줄여 줍니다. 하지만 보안의 전부를 맡겨 주지는 않습니다. 비밀번호는 반드시 해시해서 저장해야 하고, 사용자 권한 검사는 애플리케이션 요구사항에 맞게 따로 설계해야 합니다.
확장 라이브러리를 도입하기 전 확인할 기준
확장 라이브러리는 편리하지만, 많이 설치한다고 좋은 프로젝트가 되는 것은 아닙니다. 오히려 초보 단계에서는 확장 기능이 너무 많으면 어디서 문제가 생겼는지 찾기 어려워집니다.
도입 전에 다음 질문을 해 보세요.
지금 실제로 반복되는 문제가 있는가?
폼이 하나뿐이라면 Flask-WTF가 아직 필요 없을 수 있습니다.직접 작성한 코드가 너무 길어졌는가?
검증 함수가 여러 파일에 흩어지고 같은 코드가 반복된다면 확장을 고려할 만합니다.보안과 관련된 기능인가?
로그인, CSRF 보호, 비밀번호 처리처럼 보안이 중요한 기능은 검증된 도구를 쓰는 편이 안전합니다.공식 문서와 유지보수가 활발한가?
오래 업데이트되지 않은 확장은 Flask 최신 버전과 맞지 않을 수 있습니다.팀이나 내가 이해할 수 있는가?
확장은 마법이 아닙니다. 문제가 생겼을 때 설정과 동작 흐름을 읽을 수 있어야 합니다.
현재 게시판 프로젝트라면 다음 순서가 현실적입니다.
게시글 CRUD만 있다면 지금의 SQLite 직접 SQL 방식도 충분합니다.
회원가입과 로그인 페이지가 생기면 Flask-Login을 고려합니다.
폼이 많아지고 검증 규칙이 반복되면 Flask-WTF를 고려합니다.
테이블 관계가 많아지고 SQL 문자열 관리가 부담되면 Flask-SQLAlchemy를 고려합니다.
핵심은 "유명하니까 설치"가 아니라 "현재 문제를 줄여 주니까 설치"입니다.
작게 시작하고, 필요할 때 붙이기
Flask의 장점은 처음부터 큰 구조를 강요하지 않는다는 점입니다. 우리는 이미 라우팅, 요청과 응답, 템플릿, 폼 검증, SQLite, Blueprint, JSON API를 직접 다뤄 보았습니다. 이 경험이 있어야 확장 라이브러리를 써도 무엇이 편해졌는지 알 수 있습니다.
확장은 기본기를 대신하지 않습니다. 기본기를 바탕으로 반복 작업을 줄여 주는 도구입니다.
앞으로 프로젝트가 커질수록 모든 기능을 직접 만들 필요는 없습니다. 다만 확장을 붙일 때마다 스스로 물어보면 됩니다.
이 확장은 지금 내 프로젝트의 어떤 문제를 해결하는가?
이 질문에 답할 수 있다면 확장 라이브러리는 Flask 프로젝트를 더 단단하게 만들어 줄 좋은 도구가 됩니다.
참고
1Welcome to Flask — Flask Documentation
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.