책 '점프 투 플라스크'를 공부하면서 정리한 내용입니다.
출처 : https://wikidocs.net/book/4542
index 함수가 문자열을 반환하던 부분을 질문 데이터를 출력하도록 main_views.py 수정
from flask import Blueprint, render_template
from pybo.models import Question
bp = Blueprint('main', __name__, url_prefix='/')
@bp.route('/hello')
def hello_pybo():
return 'Hello, Pybo!'
@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)
order_by 함수는 조회 결과를 정렬해 준다. 즉, Question.create_date.desc() 코드는 조회된 질문 목록을 '작성일시 기준 역순으로 정렬'이라는 의미이다.
반환문에 호출한 render_template 함수는 템플릿 파일을 화면에 그려주고 조회된 질문 목록을 템플릿으로 전달하면 전달받은 데이터로 화면을 구성한다.
render_template 함수에서 사용할 question/question_list.html 템플릿 파일을 작성해야 한다. 이 파일을 플라스크가 앱으로 지정한 모듈 아래에 templates라는 디렉토리에 저장한다. (별다른 설정 안해도 템플릿 디렉토리로 인식)
템플릿 파일 : 파이썬 문법을 사용할 수 있는 HTML
(myproject) c:\projects\myproject> cd pybo
(myproject) c:\projects\myproject\pybo> mkdir templates
해당 디렉토리에 템플릿 파일을 만든다. 파일명을 아까 question/question_list.html로 지정했으므로 이 이름으로 생성한다. 파이참에서는 파일 생성 시 파일 이름에 /를 붙이면 / 이전에 있는 이름은 디렉토리로 생성해 준다.
즉 question/question_list.html은 question 디렉토리를 만들고 하위에 자동으로 question_list.html를 만들어줌
{% if question_list %}
<ul>
{% for question in question_list %}
<li><a href="/detail/{{ question.id }}/">{{ question.subject }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>질문이 없습니다.</p>
{% endif %}
{% 와 %}로 둘러싸인 문장 : 템플릿 태그
{% if question_list %} : render_template 함수에서 전달받은 질문 목록 데이터 question_list가 있는지 검사
{% for question in question_list %} : question_list에 저장된 데이터를 하나씩 꺼내 question 객체에 대입 (파이썬의 for~in문)
{{ question.id }}
{{ question.subject }}
for문에서 얻은 question 객체의 id, subject 출력
그 후 localhost:5000에 접속하면 다음과 같이 뜬다.

이전에 등록했던 질문 2건이 조회됐다.
플라스크에서 자주 사용하는 템플릿 태그
1. 분기문 태그 : 파이썬의 if, elif, else문. 닫는 태그 {% endif %} 필수{% if 조건문1 %} <p>조건문1에 해당하면 실행</p> {% elif 조건문2 %} <p>조건문2에 해당하면 실행</p> {% else %} <p>조건문1, 2 모두 해당하지 않으면 실행</p> {% endif %}
- 반복문 태그 : 파이썬의 for문. 닫는 태그 {% endfor %} 필수
{% for item in list %} <p>순서: {{ loop.index }} </p> <p>{{ item }}</p> {% endfor %}파이썬 문법과는 별도로 반복문 태그 편의를 위한 loop 속성 제공
loop.index : 반복 순서, 1부터 1씩 증가
loop.index0 : 반복 순서, 0부터 1씩 증가
loop.first : 반복 순서가 첫 번째 순서이면 True 아니면 False
loop.last : 반복 순서가 마지막 순서이면 True 아니면 False
3. 객체 태그
객체
{{ 객체 }}
객체의 속성
{{ 객체.속성 }}
질문 링크를 눌러보면 오류 메시지가 뜬다. 주소 표시줄에 보이는 http://127.0.0.1:5000/detail/2/ 페이지의 URL을 정의하지 않아 발생한 오류이다.
http://127.0.0.1:5000/detail/2/ 이 URL은 Question 모델 데이터 중 id 값이 2인 데이터를 조회하라는 의미이다. 이 요청에 대응하기 위해 main_views.py 파일에 @db.route와 함께 detail 함수를 추가한다.
(...생략...)
@bp.route('/detail/<int:question_id>/')
def detail(question_id):
question = Question.query.get(question_id)
return render_template('question/question_detail.html', question=question)
detail 함수의 매개변수 question_id에는 라우트 매핑 규칙에 사용한 <int:question_id>가 전달된다.
templates/question 디렉터리에 question/question_detail.html 템플릿 파일 작성
<h1>{{ question.subject }}</h1>
<div>
{{ question.content }}
</div>
{{ question.subject }}와 {{ question.content }}의 question은 render_template 함수에서 전달받은 데이터이다.
다시 아까 페이지를 요청해보면 question_id가 2인 질문 데이터의 제목과 내용이 표시된다.

localhost:5000/detail/30/ 페이지를 요청해 보면 빈 페이지가 나온다. 앱이 전달받은 question_id가 30이므로 main_views.py 파일의 detail 함수에서 Question.query.get(30)이 호출되는데 question_id가 30인 질문이 없기 때문이다.
잘못된 URL을 요청받을 때 보통 'Not Found (404)’처럼 오류 페이지를 표시해야 한다. 404는 HTTP 주요 응답 코드의 하나이다.
detail 함수에서 기존 코드 get 함수 대신 해당 데이터를 찾을 수 없을 경우에 404 페이지를 출력해주는 get_or_404 함수를 사용한다.
(...생략...)
@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)

모든 질문 목록 조회, 상세 조회 기능을 main_views.py 파일에 구현했는데 각 기능을 블루프린트 파일로 분리해서 관리하연 유지보수하는 데 유리하다.
pybo/views 디렉터리에 question_views.py 파일을 새로 만들고 질문 목록 조회와 질문 상세 조회 기능을 이동한다.
quesiton_views.py 파일에 main_views.py 파일의 내용을 그대로 복사하되, 블루프린트 객체를 생성할 때 question이라는 이름을 사용하고, url_prefix에는 /question을 사용해 main_views.py 파일의 블루프린트와 구별했다. 그리고 index 함수명을 _list로 바꾸고 라우트도 /에서 /list/로바꿨다.
from flask import Blueprint, render_template
from pybo.models import Question
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)
question_views.py 파일에 등록한 블루프린트를 적용하기 위해 pybo/__init__.py 파일도 수정해서 question_views의 bp 객체를 등록한다.
(...생략...)
def create_app():
(...생략...)
# 블루프린트
from .views import main_views, question_views
app.register_blueprint(main_views.bp)
app.register_blueprint(question_views.bp)
(...생략...)
question_views.py 파일로 질문 목록과 질문 상세 조회 기능을 분리했으므로 main_views.py 파일에서 해당 기능을 제거한다.
from flask import Blueprint, url_for
from werkzeug.utils import redirect
bp = Blueprint('main', __name__, url_prefix='/')
@bp.route('/hello')
def hello_pybo():
return 'Hello, Pybo!'
@bp.route('/')
def index():
return redirect(url_for('question._list'))
detail 함수는 제거하고 index 함수를 question._list에 해당하는 URL로 리다이렉트할 수 있도록 수정한다.
redirect 함수 : 입력받은 URL을 리다이렉트
url_for 함수 : 라우트가 설정된 함수명으로 URL을 역으로 찾아줌
url_for 함수에 전달된 question._list는 question, _list 순서로 해석되어 함수명을 찾아준다. question은 등록된 블루프린트 이름, _list는 블루프린트에 등록된 함수명이다. 현재 _list 함수에 등록된 라우트는 @bp.route('/list/')이므로 url_for('question._list')는 bp의 접두어인 /question/과 /list/가 더해진 /question/list/ URL을 반환한다.
이제 localhost:5000에 접속하면 리다이렉트 기능 덕분에 localhost:5000/question/list/ 페이지가 호출된다.
<li><a href="{{ url_for('question.detail', question_id=question.id) }}">{{ question.subject }}</a></li>
기존에는 a 엘리먼트에 설정한 링크 URL이 /detail/처럼 하드 코딩되어 있었는데 이 부분을 url_for 함수를 이용해 question.detail 라우트 함수로 URL을 찾도록 변경했다. 이때 question.detail 함수는 question_id 매개변수가 필요하므로 question_id를 전달해야 한다.
url_for 함수를 사용하면 유지보수가 쉬워진다
URL을 하드 코딩하면 URL 구성 방식 자체가 변경될 경우 대응하기 어렵다. URL 구성 방식을 자주 변경하면 템플릿에서 사용한 모든 URL을 일일이 찾아가며 수정해야 하기 때문에 url_for 함수를 사용해야 한다.