728x90
SMALL
- 오늘은 Flask를 활용해 ORM 설정부터 질문, 답변의 CRUD 기능 구현, 스타일링, 템플릿 상속, 폼 검증까지 포함한 웹 서비스의 핵심 흐름을 실습했고, 백엔드와 프론트엔드를 아우르는 풀스택 개발의 기초를 또다시 경험해보았습니다.
🌱 Flask ORM으로 데이터 다루기
- Flask 웹 서비스에서 데이터를 저장하고 불러오는 기능을 구현할 땐 보통 데이터베이스를 사용합니다.
- 하지만 직접 SQL을 작성하는 건 번거롭고 실수도 잦기 때문에 등장한 게 바로 ORM(Object Relational Mapping)입니다.
- 그래서 오늘 실습한 내용 중 Flask에서 SQLAlchemy와 Flask-Migrate를 사용해 ORM을 적용하고, 데이터를 다루는 기본 흐름을 정리해 보겠습니다.
📌 ORM은?
- SQL 없이 파이썬 코드로 DB를 조작할 수 있는 기술입니다.
- 데이터베이스 테이블을 파이썬 클래스(모델)로 표현합니다.
- insert, select, update, delete 같은 작업을 파이썬 방식으로 처리할 수 있습니다.
# SQL 대신 이런 식으로 데이터 삽입
q = Question(subject='안녕하세요', content='가입 인사드립니다')
db.session.add(q)
db.session.commit()
💡 ORM은 어떻게 설치하고 어떻게 설정할까?
- 우선 라이브러리를 설치하고, (pip install flask-migrate)
- DB 설정 파일을 추가해줍니다. (config.py)
- 여기서 sqlite:///pybo.db 형식으로 DB 파일 경로를 설정해줍니다.
- 그리고 __init__.py 파일에서 앱 초기화 코드를 수정해서 등록해주면 끝!
- SQLAlchemy, Migrate 객체를 등록하고,
- 설정 파일을 적용하는 과정이 포함되어 있습니다.
🧱 그럼 모델은 뭘까?
- 모델은 데이터베이스의 테이블을 코드로 표현한 클래스라고 보시면 됩니다.
- 예를 들어 질문 모델은 제목, 내용, 작성 일시, 고유 번호로 이루어져있고,
- 답변 모델은 내용, 작성 일시, 어떤 질문에 달린 답변인지를 확인할 수 있는 id로 이루어져 있는 식입니다!
- 관계 설정도 가능! 하나의 질문에 여러 개의 답변이 달릴 수 있도록 설정할 수 있습니다.
💡 테이블 자동 생성
- Flask-Migrate를 사용하면 모델을 기반으로 테이블을 자동으로 생성할 수 있습니다.
- flask db init으로 초기 설정한 뒤, (한 번만)
- 모델 변경을 하고 나서는 flask db migrate을 실행해주고!
- 실제 DB에 반영할 때에는 flask db upgrade를 실행해주면 됩니다.
💡 모델을 사용해 CRUD 해보기
- Flask Shell에서 직접 데이터 다루기를 해보면 감이 확 옵니다!
- 저장할 때에는객체 생성 후 add + commit,
- 조회할 때에는 Question.query.all(), get(id), filter(),
- 수정할 때에는 속성 변경 후 commit,
- 삭제할 때에는 delete + commit을 사용하면 되겠습니다!
💡 질문과 답변 간의 관계를 활용할 때는?
- answer.question를 이용하면, 답변에 연결된 질문을 찾을 수 있고,
- question.answer_set을 사용하게 되면, 질문에 달린 답변 목록을 불러올 수도 있습니다.
📋 질문 목록과 상세 페이지 만들어보기
- 그럼 이제 질문 게시판의 핵심 기능인 질문 목록과 질문 상세 페이지를 구현해보겠습니다.
- 그래서 데이터베이스에 저장된 질문들을 리스트로 보여주고, 각 질문의 내용을 클릭해서 자세히 볼 수도 있게 되었습니다.
📌 먼저 질문 목록 페이지 !
💡 라우팅 함수 수정해주기
- 기존의 / 경로에서 'Pybo index' 문구만 보여주던 부분을 질문 목록을 보여주도록 변경해주었습니다.
@bp.route('/')
def index():
question_list = Question.query.order_by(Question.create_date.desc())
return render_template('question/question_list.html', question_list=question_list)
💡 템플릿 작성은 question_list.html에서
{% if question_list %}
<ul>
{% for question in question_list %}
<li><a href="{{ url_for('question.detail', question_id=question.id) }}">{{ question.subject }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>질문이 없습니다.</p>
{% endif %}
- 여기서 url_for를 사용하면 하드코딩된 URL을 줄이고 유지보수에 유리합니다.
📌 이번엔 질문 상세 페이지 !
💡 상세적인 라우팅 추가해주기
- URL에서 질문 id를 받아 해당 질문을 보여주는 기능을 작성해보았습니다.
@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)
- get_or_404를 사용하면 존재하지 않는 질문을 요청했을 때 자동으로 404 오류 페이지가 표시됩니다.
💡 상세 페이지 템플릿은 question_detail.html에서
<h1>{{ question.subject }}</h1>
<div>
{{ question.content }}
</div>
📦 블루프린트로 기능 분리하기
- 모든 기능을 한 파일에 작성하면 코드가 복잡해질 수 있으므로, 질문 관련 기능은 question_views.py로 분리해서 따로 작성해보았습니다.
- 그래서 pybo/views/question_views.py에 목록과 상세 라우팅을 작성해주고,
- __init__.py 파일에서 블루프린트를 등록해주었습니다.
from .views import main_views, question_views
app.register_blueprint(main_views.bp)
app.register_blueprint(question_views.bp)
💡 경로 구조도 변경해주기!
- 질문 목록은 /question/list/ 질문 상세는 /question/detail/<id>/로 변경하고
- 메인의 / 페이지에서 질문 목록으로 바로 리다이렉트되도록 변경해주었습니다.
@bp.route('/')
def index():
return redirect(url_for('question._list'))
🧠 템플릿 태그를 간단히 설명해주자면...
- Flask(Jinja2) 템플릿에서 자주 사용하는 문법 중에
- 조건문은 {% if %} ... {% elif %} ... {% else %} {% endif %},
- 반복문은 {% for item in list %} {{ item }} {% endfor %},
- 출력을 할 경우에는 {{ 변수 }}, {{ 객체.속성 }}와 같이
- 마지막으로 URL을 연결할 때에는 {{ url_for('라우팅함수명', 파라미터=값) }}와 같이 작성해주면 됩니다.
🗨️ 답변 등록 기능도 만들어보자!
- 요번에는 질문에 대한 답변을 등록하고, 해당 질문 아래에 답변 목록을 출력하는 기능을 만들어보았습니다.
📝 답변 등록할 때는 어떤 식으로 구현했냐면...
💡 질문 상세 페이지에 답변 폼을 먼저 추가
- question_detail.html 파일에 아래 코드를 추가해주었습니다.
<form action="{{ url_for('answer.create', question_id=question.id) }}" method="post">
<textarea name="content" rows="15"></textarea>
<input type="submit" value="답변등록">
</form>
- url_for('answer.create', question_id=...)라고 작성된 부분은 POST 요청이 전송될 URL을 뜻하고,
- <textarea>는 사용자 입력을 받는 부분을 나타냅니다.
💡 답변 저장해주는 라우팅 함수 만들기
- views에다가 answer_views.py 파일을 새로 만들고 아래 코드를 추가해주었습니다.
from flask import Blueprint, request, url_for
from werkzeug.utils import redirect
from datetime import datetime
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(question=question, content=content, create_date=datetime.now())
db.session.add(answer)
db.session.commit()
return redirect(url_for('question.detail', question_id=question_id))
- POST 방식만 허용한다고 표시해주고, (methods=('POST',))
- request.form['content']를 통해 textarea의 내용을 받아와주는 방식입니다.
- 답변 등록 후에는 다시 질문 상세 페이지로 리다이렉트해줍니다.
💡 등록된 답변 화면에 출력해주기
- 다시 question_detail.html로 돌아가서, 답변 출력 영역을 아래처럼 추가해주면! 출력이 됩니다.
<h5>{{ question.answer_set|length }}개의 답변이 있습니다.</h5>
<ul>
{% for answer in question.answer_set %}
<li>{{ answer.content }}</li>
{% endfor %}
</ul>
- question.answer_set은 해당 질문에 달린 모든 답변 리스트이고,
- |length는 템플릿 필터로, 답변 개수를 출력해주는 역할을 합니다.
🎨 CSS로 화면 좀 꾸며보까
- 실제 웹 페이지가 너무 밋밋했기 때문에 이번에는 CSS 스타일시트를 적용해서 화면을 조금 더 예쁘게 만들어보았습니다.
- CSS 파일은 Flask에서 정적(static) 파일로 처리됩니다.
- 그래서 앱 내부에 static 폴더를 만들어서 CSS 파일을 저장해야 합니다.
🖌️ 어떤 내용이 들어가야할까?
- 일단 간단하게 스타일을 적용하려고 합니다! 아래 코드는 텍스트 창의 너비를 100%로 만들고, 답변 등록 버튼 위에 여백을 주는 코드입니다.
/* pybo/static/style.css */
textarea {
width: 100%;
}
input[type=submit] {
margin-top: 10px;
}
🧩 질문 상세 템플릿에 스타일 적용해보기
- 위에서 만든 스타일을 질문 상세 페이지에 연결하기 위해서 question_detail.html 상단에 아래 코드를 추가해줍니다.
<!-- question_detail.html -->
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<h1>{{ question.subject }}</h1>
<div>
{{ question.content }}
</div>
- 여기서 url_for('static', filename=...)는 정적 파일의 경로를 자동으로 생성해주고,
- 이 한 줄만으로 외부 스타일시트가 연결되어서 작성한 CSS가 적용됩니다.
✨ 이번에는 Bootstrap 적용해보기 !
🎀 Bootstrap이란?
- Twitter에서 시작한 오픈소스 프론트엔드 프레임워크입니다.
- 다양한 스타일, 버튼, 카드, 테이블 컴포넌트 등을 손쉽게 적용할 수 있어서 디자이너 없이도 빠르게 그럴싸한 화면을 만들 수 있습니다.
📥 Bootstrap을 다운로드하고 적용해보자!
Download
Download Bootstrap to get the compiled CSS and JavaScript, source code, or include it with your favorite package managers like npm, RubyGems, and more.
getbootstrap.com
- 위 링크에서 부트스트랩 파일을 다운로드합니다.
- 압축을 풀고 아래 경로에 위치하는 bootstrap.min.css 파일을 복사해줍니다.
- bootstrap-5.1.3-dist/css/bootstrap.min.css
- 그리고는 Flask의 정적 파일 경로에 붙여넣습니다.
- projects/myproject/pybo/static/bootstrap.min.css 요렇게!
📄 질문 목록에다가 이 Bootstrap 적용해보자
- 기존의 <ul> 대신 <table>을 사용하고, 부트스트랩 클래스들을 적용해주었습니다.
💡 적용된 주요 클래스
클래스명 | 설명 |
container | 가운데 정렬 컨테이너 |
my-3 | 위아래 공백 |
table, table-dark | 테이블 스타일 지정 |
💡 적용된 코드 예시
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}">
<div class="container my-3">
<table class="table">
<thead>
<tr class="table-dark">
<th>번호</th><th>제목</th><th>작성일시</th>
</tr>
</thead>
<tbody>
{% for question in question_list %}
<tr>
<td>{{ loop.index }}</td>
<td><a href="{{ url_for('question.detail', question_id=question.id) }}">{{ question.subject }}</a></td>
<td>{{ question.create_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
📑 이번엔 상세 페이지에 !
질문과 답변을 카드(card) 컴포넌트로 예쁘게 꾸미고, 버튼과 입력 창(form)에도 스타일을 적용했습니다.
💡 적용된 주요 부트스트랩 클래스
클래스명 | 설명 |
card, card-body, card-text | 카드 UI 구성 |
badge, bg-light, text-dark | 배지와 색상 |
form-control, btn btn-primary | 입력창 및 버튼 |
d-flex justify-content-end | 우측 정렬 |
white-space: pre-line; | 줄바꿈 적용 인라인 스타일 |
🧱 표준 HTML 구조와 템플릿의 상속도 사용해보기
- 그동안 질문 목록과 질문 상세 페이지를 만들면서 템플릿을 직접 구성해왔는데,
- 이 템플릿들은 표준 HTML 구조를 따르지도 않았고, 모든 템플릿에서 중복된 코드가 많았습니다.
- 그래서 이번에는 HTML 표준 구조를 지키면서, Flask의 강력한 기능인 템플릿 상속을 도입해 코드를 효율적으로 개선해보았습니다.
✅ 표준 HTML 구조는?
- HTML 문서는 다음과 같은 구조를 가져야 웹 브라우저에서 일관된 결과를 보여줄 수 있습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<title>Hello, pybo!</title>
</head>
<body>
... 실제 콘텐츠 ...
</body>
</html>
🧩 그럼 템플릿 상속은?
- Flask의 Jinja 템플릿 엔진은 extends, block 문법을 사용해서 공통 템플릿(base.html)을 상속받을 수 있습니다.
📄 기본 템플릿인 base.html 만들기
- 먼저 모든 페이지의 뼈대가 될 base.html을 만들어주었습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<title>Hello, pybo!</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
- 이렇게 하면 공통된 <head> 내용을 한 번만 작성할 수 있습니다.
- 그래서 각 페이지는 {% block content %} 안에 들어갈 콘텐츠만 작성하면 됩니다.
📋 이에 맞춰서 질문 목록, 상세 페이지 템플릿도 수정해주기
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
<table class="table">
<thead class="table-dark">
<tr><th>번호</th><th>제목</th><th>작성일시</th></tr>
</thead>
<tbody>
{% for question in question_list %}
<tr>
<td>{{ loop.index }}</td>
<td><a href="{{ url_for('question.detail', question_id=question.id) }}">{{ question.subject }}</a></td>
<td>{{ question.create_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
- 이 구조 덕분에 스타일 파일이나 타이틀을 수정할 때에는 base.html만 수정하면 됩니다.
🧾 Flask-WTF로 폼 검증 더 쉽게 하기
- 폼에 사용자가 입력한 데이터를 저장하기 전, 이 값들이 유효한지 검증해야 합니다.
- Flask에서는 이런 과정을 더 쉽고 안전하게 처리하기 위해 Flask-WTF (WTForms) 모듈을 사용합니다.
- 사용하기 전, config.py에 SECRET_KEY 설정 필수!
SECRET_KEY = "dev" # 배포 시에는 안전한 값으로 변경 필요
📝 질문을 등록할 수 있는 기능 만들기
🧩 Flask-WTF 폼 정의하기
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('내용은 필수입력 항목입니다.')])
🌐 라우팅 함수 작성하기
@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('main.index'))
return render_template('question/question_form.html', form=form)
📄 질문 등록 템플릿 만들기
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h5 class="my-3 border-bottom pb-2">질문 등록</h5>
<form method="post" class="my-3">
{{ form.csrf_token }}
{% if form.errors %}
<div class="alert alert-danger">
{% for field, errors in form.errors.items() %}
<strong>{{ form[field].label }}</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endfor %}
</div>
{% endif %}
<div class="mb-3">
<label for="subject">제목</label>
<input type="text" name="subject" id="subject" class="form-control"
value="{{ form.subject.data or '' }}">
</div>
<div class="mb-3">
<label for="content">내용</label>
<textarea name="content" id="content" class="form-control" rows="10">
{{ form.content.data or '' }}</textarea>
</div>
<button type="submit" class="btn btn-primary">저장하기</button>
</form>
</div>
{% endblock %}
- CSRF 토큰은 필수이기 때문에 {{ form.csrf_token }}를 추가해주고,
- 오류를 출력할 때에는 form.errors를, 입력값을 유지할 때에는 form.subject.data 등을 사용하시면 됩니다.
- 위와 같은 과정으로 답변 등록 기능도 추가할 수 있었습니다.
🤔 45일차 회고
- 오늘은 Flask로 게시판의 간단한 구조를 구현해보며, Flask의 여러 기능들을 익히고 만들어보았습니다.
- 뭔가 아직은 구조 설계 단계라 프론트엔드의 느낌보다는 백엔드의 느낌이 강한데, 내일부터 있을 JS를 배우면 더 프론트 느낌이 살 것 같습니다.
- 큰 프레임워크에서나 보안 관련 모듈이 있을 줄 알았는데, Flask에도 폼 검증하기 쉽게 해주는 모듈이 있다는 것도 너무 신기했습니다. (Flask 나름 잘 갖춰져있구나...)
- 사실 프론트엔드를 React + TS로만 배우고 구현해보아서 Flask는 잘 안 쓸 듯 싶지만, 그래도 구조적으로 익히는 것이 나중에도 도움이 많이 될 것 같습니다.
728x90
LIST
'부트캠프 > LG U+' 카테고리의 다른 글
🤔 해도 해도 끝이 없는 AWS (0) | 2025.04.18 |
---|---|
🤔 Flask를 어떻게 배포할까? (0) | 2025.04.07 |
🤔 이게 Flask였던가... (0) | 2025.03.31 |
🤔 타이타닉에서 살아남기 (0) | 2025.03.27 |
🤔 요즘 어떤 강의가 가장 핫해...? (0) | 2025.03.19 |