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문 없이도 객체처럼 데이터를 다룰 수 있어 유지보수가 훨씬 편리합니다.