Why Not SW CAMP 5기/수업 기록
[4월 1주차-4/1]🧠 Flask로 나만의 Q&A 게시판 만들기 (SQLAlchemy & Migrate 활용)
rubii
2025. 4. 1. 14:30
Flask를 공부하며 실습할 수 있는 좋은 예제가 바로 Q&A 게시판 만들기입니다. 이번 글에서는 ORM(Object Relational Mapping)을 적용하여 SQL 없이도 데이터베이스를 다루는 방법과, Flask의 구조적인 개발 방식을 함께 정리해보겠습니다.
Q&A 게시판 기능
- 질문 목록 보기
- 질문 상세 보기
- 질문 등록하기
- 답변 달기
🗂️ 프로젝트 구조
pybo/
├── __init__.py # Flask 앱 생성 및 초기 설정
├── models.py # SQLAlchemy ORM 모델 정의
├── forms.py # 폼 처리 (WTForms)
├── views/ # 블루프린트 기반 라우팅 관리
│ ├── main_views.py
│ ├── question_views.py
│ └── answer_views.py
├── static/ # 정적 파일 (CSS, JS, 이미지 등)
│ └── style.css
├── templates/ # 템플릿 (HTML)
│ └── question/ # 질문 관련 페이지
│ ├── question_list.html
│ ├── question_detail.html
│ └── question_form.html
config.py # 앱 설정 파일
✅ Flask + SQLAlchemy + Flask-Migrate 세팅 순서
1. 필요한 라이브러리 설치
pip install flask flask-sqlalchemy flask-migrate
2. config.py 설정
import os
BASE_DIR = os.path.dirname(__file__)
SQLALCHEMY_DATABASE_URI = 'sqlite:///{}'.format(os.path.join(BASE_DIR, 'pybo.db'))
SQLALCHEMY_TRACK_MODIFICATIONS = False
3. __init__.py 초기화
from flask import Flask
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
import config
# 전역 변수로 객체 생성
db = SQLAlchemy()
migrate = Migrate()
def create_app(): # Application Factory
app = Flask(__name__)
app.config.from_object(config)
app.secret_key = 'ddsf-df-df-df'
# ORM
db.init_app(app)
migrate.init_app(app, db)
from . import models
# 블루프린트
from .views import main_views, question_views, answer_views
app.register_blueprint(main_views.bp)
app.register_blueprint(question_views.bp)
app.register_blueprint(answer_views.bp)
return app
🧱 모델 정의: models.py
from pybo import db
class Question(db.Model):
id = db.Column(db.Integer, primary_key=True)
subject = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text(), nullable=False)
create_date = db.Column(db.DateTime(), nullable=False)
class Answer(db.Model):
id = db.Column(db.Integer, primary_key=True)
question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete='CASCADE'))
question = db.relationship('Question', backref=db.backref('answer_set'))
content = db.Column(db.Text(), nullable=False)
create_date = db.Column(db.DateTime(), nullable=False)
🛠️ DB 초기화 및 마이그레이션
flask db init # 최초 초기화
flask db migrate # 마이그레이션 파일 생성
flask db upgrade # 실제 DB에 반영
📝폼(form) 정의
forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField
from wtforms.validators import DataRequired
class QuestionForm(FlaskForm):
subject = StringField('제목', validators=[DataRequired()])
content = TextAreaField('내용', validators=[DataRequired()])
🔍 뷰(View) 구성
main_views.py
from flask import Blueprint, url_for
from werkzeug.utils import redirect
bp = Blueprint('main', __name__, url_prefix='/')
@bp.route('/')
def index():
return redirect(url_for('question._list'))
question_views.py
from flask import Blueprint, render_template, request, redirect, url_for
from pybo import db
from pybo.models import Question
from datetime import datetime
from pybo.forms import QuestionForm
bp = Blueprint('question', __name__, url_prefix='/question')
@bp.route('/list/')
def _list():
question_list = Question.query.order_by(Question.create_date.desc())
return render_template('question/question_list.html', question_list=question_list)
@bp.route('/detail/<int:question_id>/')
def detail(question_id):
question = Question.query.get_or_404(question_id)
return render_template('question/question_detail.html', question=question)
@bp.route('/create/', methods=('GET', 'POST'))
def create():
form = QuestionForm()
if request.method == 'POST' and form.validate_on_submit():
question = Question(
subject=form.subject.data,
content=form.content.data,
create_date=datetime.now()
)
db.session.add(question)
db.session.commit()
return redirect(url_for('question._list'))
return render_template('question/question_form.html', form=form)
answer_views.py
from datetime import datetime
from flask import Blueprint, url_for, request
from werkzeug.utils import redirect
from pybo import db
from pybo.models import Question, Answer
bp = Blueprint('answer', __name__, url_prefix='/answer')
@bp.route('/create/<int:question_id>', methods=('POST',))
def create(question_id):
question = Question.query.get_or_404(question_id)
content = request.form['content']
answer = Answer(content=content, create_date=datetime.now())
question.answer_set.append(answer)
db.session.commit()
return redirect(url_for('question.detail', question_id=question_id))
✍️ Flask 쉘에서 데이터 추가, 수정, 삭제
flask shell
📌 질문 데이터 추가
from pybo.models import Question
from datetime import datetime
from pybo import db
q1 = Question(subject='Flask ORM이 뭔가요?', content='SQL 없이도 가능한가요?', create_date=datetime.now())
q2 = Question(subject='플라스크 모델 질문입니다.', content='id는 자동으로 생성되나요?', create_date=datetime.now())
db.session.add(q1)
db.session.add(q2)
db.session.commit()
📌 질문 조회
Question.query.all()
Question.query.filter(Question.id==1).all()
Question.query.get(1)
Question.query.filter(Question.subject.like('%플라스크%')).all()
📌 질문 수정 & 삭제
q = Question.query.get(2)
q.subject = 'Flask Model Question'
db.session.commit()
db.session.delete(q)
db.session.commit()
📌 답변 추가
from pybo.models import Answer
q = Question.query.get(1)
a = Answer(content='네, 가능합니다!', create_date=datetime.now(), question=q)
db.session.add(a)
db.session.commit()
🖥️ HTML 템플릿 구성
base.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Pybo</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<h1><a href="{{ url_for('main.index') }}">📘 Pybo</a></h1>
<hr>
{% block content %}
{% endblock %}
</div>
</body>
</html>
question_list.html
{% extends 'base.html' %}
{% block content %}
<p style="text-align: right;">
<a href="{{ url_for('question.create') }}" class="btn">🖋 질문 등록</a>
</p>
{% if question_list %}
<ul>
{% for question in question_list %}
<li>
<a href="{{ url_for('question.detail', question_id=question.id) }}">
{{ question.subject }}
</a>
<span class="date-right">{{ question.create_date.strftime('%Y-%m-%d %H:%M') }}</span>
</li>
{% endfor %}
</ul>
{% else %}
<p>질문이 없습니다.</p>
{% endif %}
{% endblock %}
question_form.html
{% extends 'base.html' %}
{% block content %}
<h1>질문 등록</h1>
<form method="post">
{{ form.hidden_tag() }}
<div>
{{ form.subject.label }}<br>
{{ form.subject(size=80) }}
{% if form.subject.errors %}
<ul>
{% for error in form.subject.errors %}
<li style="color: red;">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div style="margin-top: 10px;">
{{ form.content.label }}<br>
{{ form.content(rows=10) }}
{% if form.content.errors %}
<ul>
{% for error in form.content.errors %}
<li style="color: red;">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div style="margin-top: 15px;">
<input type="submit" value="질문 등록">
</div>
</form>
{% endblock %}
question_detail.html
{% extends 'base.html' %}
{% block content %}
<h1>{{ question.subject }}</h1>
<div class="question-content">
{{ question.content }}
</div>
<div class="date-right">
{{ question.create_date.strftime('%Y-%m-%d %H:%M') }}
</div>
<h5>{{ question.answer_set|length }}개의 답변이 있습니다.</h5>
<ul>
{% for answer in question.answer_set %}
<li>
<div class="answer-content">{{ answer.content }}</div>
<div class="date-right">{{ answer.create_date.strftime('%Y-%m-%d %H:%M') }}</div>
</li>
{% endfor %}
</ul>
<form action="{{ url_for('answer.create', question_id=question.id) }}" method="post">
<textarea name="content" id="content" rows="10" placeholder="답변을 입력하세요."></textarea>
<input type="submit" value="답변 등록">
</form>
{% endblock %}
🎨 CSS로 예쁘게 꾸미기
✅ 마무리
지금까지 Flask, SQLAlchemy, Flask-Migrate를 활용하여 간단한 Q&A 게시판을 만들어봤습니다.
ORM을 사용하면 SQL문 없이도 객체처럼 데이터를 다룰 수 있어 유지보수가 훨씬 편리합니다.