본문 바로가기

Python

Flask 8 : 블로그 블루프린트

원문링크 : https://flask.palletsprojects.com/en/1.1.x/tutorial/blog/

몇몇 블로그나 책에서는 blueprint를 청사진이라고 번역한 것이 보이지만, 청사진이라는 용어가 아무런 인사이트를 주지 못하는 직역이라 그냥 블루프린트라고 쓰기로 했습니다.


블로그 블루프린트

블로그 블루프린트를 만들기 위해 사용자 인증 섹션에서 사용한 것과 동일한 기술을 이용합니다. 블로그는 전체 포스트 목록 화면을, 그리고 로그인한 유저를 위해 포스트 작성/수정/삭제 화면을 제공합니다.

각 화면을 만드는 동안 개발 서버는 운영(running)상태로 유지해주세요. 코드를 하나씩 수정해가면서 브라우저를 통해 각각의 URL에 대한 접속 테스트를 진행해보세요.


블루프린트

블루프린트를 만들고 어플리케이션 팩토리에 등록합니다.

flaskr/blog.py

from flask import ( Blueprint, flash, g, redirect, render_template, request, url_for ) from werkzeug.exceptions import abort from flaskr.auth import login_required from flaskr.db import get_db bp = Blueprint('blog', __name__)


app.register_blueprint()을 이용해서 블루프린트 모듈을 불러오고 팩토리에 등록합니다. 신규 코드는 팩토리 함수의 마지막부터 앱으로 결과를 리턴헤주는 부분 사이에 추가합니다.

flaskr/__init__.py
def create_app():
    app = ...
    # existing code omitted

    from . import blog
    app.register_blueprint(blog.bp)
    app.add_url_rule('/', endpoint='index')

    return app

블로그 블루프린트는 사용자인증 블루프린트와는 달리 url_prefix를 사용하지 않습니다. 따라서 index(디폴트) 뷰가 / 아래 바로 위치합니다. 마찬가지로 create 뷰는 /create 에 위치합니다. 블로그는 Flaskr 프로젝트의 메인 기능이므로 블로그 전체의 인덱스를 포스트 리스트 화면으로 세팅하는 것입니다.

그럼에도 불구하고, 아래 코드는 디폴트 index뷰의 엔드포인트를 blog.index 로 설정합니다. 사용자인증 뷰 중 일부도 여기서와 같이 간단히 엔드포인트를 지정했었죠. app.add_url_rule() 함수에서는 엔드포인트 이름 'index' 를 / 만으로 간단히 지정할 수 있습니다. 또한 url_for('index') 이나 url_for('blog.index') 역시 마찬가지 기능을 수행합니다.  

다른 앱에서는 블로그 블루프린트에 url_prefix를 주거나 어플리캐이션 팩토리에서 별도로 인덱스를 지정하기도 합니다. hello 뷰를 참고하세요. 그렇게 하면 index  blog.index 는 서로 다른 엔드포인트를 가리키게 됩니다.


인덱스 (디폴트 뷰)

인덱스는 전체 포스트 목록을 최신글부터 보여줍니다. 포스트 목록과 작성자를 함께 표시하기 위해 SQL문을 이용해 DB에서 목록을 불러올 때 JOIN을 이용합니다.

flaskr/blog.py
@bp.route('/')
def index():
    db = get_db()
    posts = db.execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' ORDER BY created DESC'
    ).fetchall()
    return render_template('blog/index.html', posts=posts)
flaskr/templates/blog/index.html
{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Posts{% endblock %}</h1>
  {% if g.user %}
    <a class="action" href="{{ url_for('blog.create') }}">New</a>
  {% endif %}
{% endblock %}

{% block content %}
  {% for post in posts %}
    <article class="post">
      <header>
        <div>
          <h1>{{ post['title'] }}</h1>
          <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
        </div>
        {% if g.user['id'] == post['author_id'] %}
          <a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
        {% endif %}
      </header>
      <p class="body">{{ post['body'] }}</p>
    </article>
    {% if not loop.last %}
      <hr>
    {% endif %}
  {% endfor %}
{% endblock %}

로그인된 사용자라면 header 블록 내에 있는 New 버튼이 활성화되며 create 뷰로의 링크가 생성됩니다. 사용자가 포스트 작성자라면 update 뷰로 연결되는 Edit 버튼이 활성화됩니다. loop.last 는 Jinja for loops에 포함된 특별한 기능입니다. 이 기능을 이용해 마지막 글 까지 루프를 돌며 제목 사이에 줄을 삽입합니다.

글 쓰기 Create

create 뷰는 사용자 등록을 위한 register 뷰와 동일하게 작동합니다. 새 글을 입력할 폼을 표시하고, 입력된 정보의 유효성 체크를 하며, DB에 저장하는 기능 입니다.

@login_required 데코레이터는 로그인된 사용자인지 체크하며, 로그인된 사용자가 아니라면 로그인 페이지로 보냅니다.

flaskr/blog.py
@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'INSERT INTO post (title, body, author_id)'
                ' VALUES (?, ?, ?)',
                (title, body, g.user['id'])
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/create.html')
flaskr/templates/blog/create.html
{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}New Post{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title" value="{{ request.form['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
{% endblock %}

수정 Update

update  delete 뷰는 id 정보를 함께 표출합니다. 코드를 중복해서 작성하지 않도록 get_post 함수로 작성해서 각각의 뷰에서 불러와 사용합니다.

flaskr/blog.py
def get_post(id, check_author=True):
    post = get_db().execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' WHERE p.id = ?',
        (id,)
    ).fetchone()

    if post is None:
        abort(404, "Post id {0} doesn't exist.".format(id))

    if check_author and post['author_id'] != g.user['id']:
        abort(403)

    return post

abort() 는 미리 정의된 예외상황에 따른 HTTP 코드값을 반환합니다. 이 코드에서 사용된 404 는 “Not Found”, 403 은  “Forbidden” 을 의미합니다.

check_author 인자는 익명글을 허용할지에 대한 부분입니다. 

flaskr/blog.py
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
    post = get_post(id)

    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'UPDATE post SET title = ?, body = ?'
                ' WHERE id = ?',
                (title, body, id)
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/update.html', post=post)

지금까지 만들어온 뷰들과는 달리 update 함수는 id라는 인자값을 받아옵니다. 이 인자는 경로값에 있는 <int:id> 값을 사용합니다. 실제 URL을 보면, /1/update 와 같이 되어 있는것을 확인할 수 있습니다. Flask는 경로에서 1 값을 가져와 이 값이 int 인지 확인하고 id 인자로 넘겨줍니다. 만일 int: 를 표시하지 않고 <id> 처럼 쓰게되면, 이 값은 문자로 처리됩니다. 수정화면에 대한 URL을 만들기 위해 url_for() 를 사용하고, 여기에 id값을 함께 포함해서 보냅니다 : url_for('blog.update', id=post['id']). 이 방법은 index.html 에서도 사용되었습니다.


create  update  뷰는 꽤 비슷한데요, 차이점은 update 뷰에서는 내용을 DB에 전달할 때 INSERT 가 아닌 UPDATE 를 사용한다는 것 입니다. 잘만 만들면 템플릿 하나로 양쪽 뷰 모두에 사용할 수 있습니다.

flaskr/templates/blog/update.html
{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title"
      value="{{ request.form['title'] or post['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
  <hr>
  <form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
    <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
  </form>
{% endblock %}

이번 템플릿은 두 개의 폼 태그를 갖고 있습니다. 첫번째는 현재 페이지(/<id>/update)에 수정된 데이터를 보여주기 위한 폼이고, 두번째는 삭제를 위한 버튼 하나 입니다. 버튼은 삭제확인을 위한 자바스크립트 코드를 담고 있습니다.

패턴 {{ request.form['title'] or post['title'] }} 는 폼에 어떤 데이터를 넣을 것인지를 고르기 위해 사용됩니다. 폼의 데이터가 아직 전달되지 않았다면, 수정 전의 데이터가 표출됩니다. 규칙에 맞지 않는 데이터가 입력됐다면, 사용자가 수정할 수 있도록 request.form 를 이용해 수정전 데이터 대신 표출합니다.

삭제 Delete

삭제 뷰는 별도의 템플릿이 없고, 버튼도 update.html 의 일부분으로 포함되어 /<id>/delete URL로 연결됩니다. 별도의 템플릿이 없기 때문에 데이터를 넘겨주는 부분과 인덱스로 화면을 전환하는 부분만 처리합니다.

flaskr/blog.py
@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
    get_post(id)
    db = get_db()
    db.execute('DELETE FROM post WHERE id = ?', (id,))
    db.commit()
    return redirect(url_for('blog.index'))


축하합니다. 이제 앱 개발이 완료되었습다! 브라우저에서 이것저것 해보세요.

개발은 완성됐지만, 조금 더 알아볼게 남아있어요.

다음 글 : Make the Project Installable.


역자 : 웹사이트 제작은 여기까지 입니다. 이 다음 글 부터는 배포파일 작성, 테스트 등과 관련한 내용이라, 관심 없는 분은 여기까지만 읽어도 충분분합니다. ^^



'Python' 카테고리의 다른 글

BeautifulSoup parsers : 소스코드 해석기  (0) 2019.10.04
Flask 7 : 정적 파일  (0) 2019.09.08
Flask 6 : 템플릿  (0) 2019.09.06
Flask 5 : 블루프린트와 뷰  (0) 2019.08.28
Flask 4 : DB 구축  (1) 2019.08.28