블루프린트 — 애플리케이션을 모듈로 나누는 Flask의 방식
블루프린트 — 애플리케이션을 모듈로 나누는 Flask의 방식
애플리케이션이 커지면 라우트와 뷰가 한곳에 쌓이면서 관리가 어려워져요. Flask는 이를 **블루프린트(blueprint)**라는 개념으로 풀어요. 블루프린트는 애플리케이션 안에서, 또는 애플리케이션들 사이에서 흔한 패턴을 지원하며 애플리케이션 컴포넌트를 만드는 방식이에요.
블루프린트는 대규모 애플리케이션을 아주 단순하게 만들고, Flask 확장이 애플리케이션에 작업을
등록하는 중앙 수단을 제공해요. :class:Blueprint 객체는 :class:Flask 애플리케이션 객체와
비슷하게 동작하지만 실제 애플리케이션은 아니에요. 애플리케이션을 어떻게 생성·확장할지에 대한
*설계도(blueprint)*예요.
본문
왜 블루프린트인가?
Flask에서 블루프린트는 이런 경우를 위해 만들어졌어요.
- 애플리케이션을 블루프린트 집합으로 분해해요. 큰 애플리케이션에 이상적이에요. 프로젝트가 애플리케이션 객체를 인스턴스화하고, 여러 확장을 초기화하고, 블루프린트 모음을 등록할 수 있어요.
- 애플리케이션에 URL 접두사 또는 서브도메인으로 블루프린트를 등록해요. URL 접두사/서브도메인의 파라미터는 블루프린트의 모든 뷰 함수에서 공통 뷰 인자(기본값 포함)가 돼요.
- 서로 다른 URL 규칙으로 블루프린트를 애플리케이션에 여러 번 등록해요.
- 블루프린트를 통해 템플릿 필터·정적 파일·템플릿·기타 유틸리티를 제공해요. 블루프린트가 애플리케이션이나 뷰 함수를 구현하지 않아도 돼요.
- Flask 확장을 초기화할 때 위 사례 중 어떤 것이든 애플리케이션에 블루프린트를 등록해요.
Flask의 블루프린트는 플러그형 앱(pluggable app)이 아니에요. 실제 애플리케이션이 아니라 애플리케이션에 등록할 수 있는(여러 번도 가능한) 작업의 집합이니까요. 여러 애플리케이션 객체를 쓰는 건 안 되나요? 쓸 수 있어요(앱 디스패치 패턴 참고). 다만 그러면 애플리케이션들이 별도 설정을 갖고 WSGI 계층에서 관리돼요.
블루프린트는 대신 Flask 레벨에서 분리를 제공하고, 애플리케이션 설정을 공유하며, 등록되는 동안 필요에 따라 애플리케이션 객체를 바꿀 수 있어요. 단점은 애플리케이션 객체 전체를 파괴하지 않고는 일단 애플리케이션이 생성된 뒤 블루프린트를 등록 해제할 수 없다는 점이에요.
블루프린트의 개념
블루프린트의 기본 개념은 애플리케이션에 등록될 때 실행할 작업을 기록한다는 거예요. Flask는 요청을 디스패치하고 한 엔드포인트에서 다른 엔드포인트로 URL을 만들 때 뷰 함수를 블루프린트와 연관 지어요.
첫 번째 블루프린트
아주 기본적인 블루프린트는 이렇게 생겼어요. 이 경우 정적 템플릿을 간단히 렌더링하는 블루프린트를 구현해 볼게요.
from flask import Blueprint, render_template, abort
from jinja2 import TemplateNotFound
simple_page = Blueprint('simple_page', __name__,
template_folder='templates')
@simple_page.route('/', defaults={'page': 'index'})
@simple_page.route('/<page>')
def show(page):
try:
return render_template(f'pages/{page}.html')
except TemplateNotFound:
abort(404)
:class:Blueprint 생성자에 준 이름(여기서는 simple_page)으로 함수를 묶습니다. 그 파이썬 모듈
또는 패키지에 대응합니다.
@simple_page.route 데코레이터로 함수를 묶으면, 나중에 등록될 때 애플리케이션에 show 함수를
등록하겠다는 의도를 블루프린트가 기록해요. 추가로 함수의 엔드포인트 앞에 :class:Blueprint
생성자에 준 블루프린트 이름(여기서도 simple_page)을 접두사로 붙여요. 블루프린트의 이름은
URL을 바꾸지 않고 엔드포인트만 바꿔요.
블루프린트 등록
그 블루프린트를 어떻게 등록할까요? 이렇게 해요.
from flask import Flask
from yourapplication.simple_page import simple_page
app = Flask(__name__)
app.register_blueprint(simple_page)
애플리케이션에 등록된 규칙을 확인하면 이것들이 보여요.
>>> app.url_map
Map([<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
<Rule '/<page>' (HEAD, OPTIONS, GET) -> simple_page.show>,
<Rule '/' (HEAD, OPTIONS, GET) -> simple_page.show>])
첫 번째는 분명히 애플리케이션 자체의 정적 파일 규칙이에요. 나머지 두 개는 simple_page
블루프린트의 show 함수용이에요. 보시다시피 블루프린트 이름과 점(.)으로 접두사가 붙어 있어요.
블루프린트는 서로 다른 위치에 마운트할 수도 있어요.
app.register_blueprint(simple_page, url_prefix='/pages')
그럼 생성된 규칙들은 이렇게 돼요.
>>> app.url_map
Map([<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
<Rule '/pages/<page>' (HEAD, OPTIONS, GET) -> simple_page.show>,
<Rule '/pages/' (HEAD, OPTIONS, GET) -> simple_page.show>])
그 위에 블루프린트를 여러 번 등록할 수도 있지만, 모든 블루프린트가 그에 적절히 응답하진 않을 수 있어요. 실제로 두 번 이상 마운트할 수 있는지는 블루프린트가 어떻게 구현됐는지에 달려 있어요.
블루프린트 중첩
다른 블루프린트에 블루프린트를 등록하는 것도 가능해요.
parent = Blueprint('parent', __name__, url_prefix='/parent')
child = Blueprint('child', __name__, url_prefix='/child')
parent.register_blueprint(child)
app.register_blueprint(parent)
자식 블루프린트는 이름에 부모의 이름이 접두사로, 자식 URL은 부모의 URL 접두사가 접두사로 붙어요.
url_for('parent.child.create')
/parent/child/create
추가로 자식 블루프린트는 부모의 서브도메인을 얻고, 서브도메인이 있으면 자기 서브도메인을 접두사로 붙여요.
parent = Blueprint('parent', __name__, subdomain='parent')
child = Blueprint('child', __name__, subdomain='child')
parent.register_blueprint(child)
app.register_blueprint(parent)
url_for('parent.child.create', _external=True)
"child.parent.domain.tld"
부모로 등록한 블루프린트 특정 before request 함수 등은 자식에서도 발동돼요. 자식이 주어진 예외를 처리할 오류 핸들러가 없으면 부모의 것이 시도돼요.
블루프린트 리소스
블루프린트는 리소스도 제공할 수 있어요. 때로는 제공하는 리소스만을 위해 블루프린트를 만들고 싶을 때가 있어요.
블루프린트 리소스 폴더
일반 애플리케이션과 마찬가지로 블루프린트는 폴더에 담겨 있다고 간주돼요. 여러 블루프린트가 같은 폴더에서 시작할 수 있지만, 반드시 그럴 필요는 없고 일반적으로 권장되지도 않아요.
폴더는 :class:Blueprint의 두 번째 인자(보통 __name__)로 유추돼요. 이 인자는 블루프린트에
대응하는 논리적 파이썬 모듈 또는 패키지를 지정해요. 실제 파이썬 패키지를 가리키면 그 패키지
(파일시스템의 폴더)가 리소스 폴더예요. 모듈이면 그 모듈이 담긴 패키지가 리소스 폴더가 돼요.
:attr:Blueprint.root_path 속성으로 리소스 폴더가 뭔지 확인할 수 있어요.
>>> simple_page.root_path
'/Users/username/TestProject/yourapplication'
이 폴더에서 소스를 빠르게 열려면 :meth:~Blueprint.open_resource 함수를 써요.
with simple_page.open_resource('static/style.css') as f:
code = f.read()
정적 파일
블루프린트는 static_folder 인자로 파일시스템 폴더의 경로를 제공해 정적 파일을 담은 폴더를
노출할 수 있어요. 절대 경로이거나 블루프린트 위치에 상대적인 경로예요.
admin = Blueprint('admin', __name__, static_folder='static')
기본적으로 경로의 맨 오른쪽 부분이 웹에서 노출되는 위치예요. 이는 static_url_path 인자로
바꿀 수 있어요. 여기서 폴더 이름이 static이므로 블루프린트의 url_prefix + /static에서 쓸
수 있게 돼요. 블루프린트가 /admin 접두사를 가지면 정적 URL은 /admin/static이 돼요.
엔드포인트 이름은 blueprint_name.static이에요. 애플리케이션의 static 폴더를 쓸 때처럼
:func:url_for로 URL을 만들 수 있어요.
url_for('admin.static', filename='style.css')
하지만 블루프린트에 url_prefix가 없으면 블루프린트의 static 폴더에 접근할 수 없어요. 그 경우
URL이 /static이 되고 애플리케이션의 /static 라우트가 우선하기 때문이에요. 템플릿 폴더와
달리, 블루프린트 static 폴더는 파일이 애플리케이션 static 폴더에 없어도 검색되지 않아요.
템플릿
블루프린트가 템플릿을 노출하게 하려면 :class:Blueprint 생성자에 template_folder 파라미터를
제공하면 돼요.
admin = Blueprint('admin', __name__, template_folder='templates')
static 파일과 마찬가지로 경로는 블루프린트 리소스 폴더에 절대 또는 상대일 수 있어요.
템플릿 폴더는 템플릿 검색 경로에 추가되는데, 실제 애플리케이션의 템플릿 폴더보다 우선순위가 낮아요. 그렇게 해서 블루프린트가 제공하는 템플릿을 실제 애플리케이션에서 쉽게 덮어쓸 수 있어요. 이는 블루프린트 템플릿이 실수로 덮이는 걸 원하지 않는다면, 다른 블루프린트나 실제 애플리케이션 템플릿에 같은 상대 경로가 없도록 해야 한다는 뜻이기도 해요. 여러 블루프린트가 같은 상대 템플릿 경로를 제공하면 먼저 등록된 블루프린트가 우선해요.
그래서 yourapplication/admin 폴더에 블루프린트가 있고 'admin/index.html' 템플릿을
렌더링하고 싶은데 template_folder로 templates를 제공했다면,
:file:yourapplication/admin/templates/admin/index.html 같은 파일을 만들어야 해요. 추가로
admin 폴더를 두는 이유는 실제 애플리케이션 템플릿 폴더에 있는 index.html이라는 템플릿에
덮이지 않게 하기 위해서예요.
다시 말하면, admin이라는 블루프린트가 있고 이 블루프린트에 특화된 :file:index.html 템플릿을
렌더링하고 싶다면 템플릿을 이렇게 배치하는 게 가장 좋아요.
yourpackage/
blueprints/
admin/
templates/
admin/
index.html
__init__.py
그리고 템플릿을 렌더링할 때는 :file:admin/index.html을 조회 이름으로 써요. 올바른 템플릿을
로드하는 데 문제가 생기면 EXPLAIN_TEMPLATE_LOADING 설정 변수를 켜요. 그러면 Flask가 매
render_template 호출마다 템플릿을 찾는 과정을 출력해 줘요.
URL 만들기
한 페이지에서 다른 페이지로 연결하고 싶다면 평소처럼 :func:url_for 함수를 쓰되 URL
엔드포인트 앞에 블루프린트 이름과 점(.)을 붙이면 돼요.
url_for('admin.index')
추가로 블루프린트 뷰 함수나 렌더링된 템플릿 안에서 같은 블루프린트의 다른 엔드포인트로 연결하고 싶다면, 엔드포인트에 점만 접두사로 붙여 상대 리다이렉트를 쓸 수 있어요.
url_for('.index')
현재 요청이 다른 admin 블루프린트 엔드포인트로 디스패치됐다면 이것은 예를 들어 admin.index로
연결돼요.
블루프린트 오류 핸들러
블루프린트는 :class:Flask 애플리케이션 객체처럼 errorhandler 데코레이터를 지원하므로,
블루프린트 특화 커스텀 오류 페이지를 쉽게 만들 수 있어요.
"404 Page Not Found" 예외의 예시를 볼게요.
@simple_page.errorhandler(404)
def page_not_found(e):
return render_template('pages/404.html')
대부분의 오류 핸들러는 예상대로 동작해요. 다만 404와 405 예외 핸들러에는 주의할 점이 있어요.
이 오류 핸들러는 블루프린트의 다른 뷰 함수에서 적절한 raise 문이나 abort 호출에서만 호출
돼요. 예를 들어 잘못된 URL 접근으로는 호출되지 않아요. 블루프린트가 특정 URL 공간을 "소유"하지
않으므로, 잘못된 URL이 주어졌을 때 애플리케이션 인스턴스가 어떤 블루프린트 오류 핸들러를
실행해야 할지 알 수 없기 때문이에요. URL 접두사에 따라 이런 오류에 다른 처리 전략을 실행하고
싶다면, request 프록시 객체를 써서 애플리케이션 레벨에서 정의하면 돼요.
@app.errorhandler(404)
@app.errorhandler(405)
def _handle_api_error(ex):
if request.path.startswith('/api/'):
return jsonify(error=str(ex)), ex.code
else:
return ex
더 알아보기
- 앱·요청 컨텍스트 — 블루프린트와 컨텍스트의 관계
- 템플릿 (Jinja) — 블루프린트에서 템플릿 필터 등록
- Flask 퀵스타트 — 라우팅·엔드포인트의 기본