일반적으로 Flask와 Vue를 함께 하나의 웹서비스로 개발하는 방법은 많은 문서에서 소개되어 있음
이 문서에서는 구체적인 애플리케이션 내용에 대해서는 자세히 다루지 않고, Flask와 Vue의 연결고리와 실제 서비스 환경에 배포하는 과정에서 겪을 수 있는 문제점에 대해 깊이 다루고자 함
이하의 책 서비스 예제는 testdriven.io에서 공유한 예제를 참고하여 현행 vue 프로젝트로 재구성한 것임
https://testdriven.io/blog/developing-a-single-page-app-with-flask-and-vuejs/
책 서비스에 대한 Vue 앱과 Flask 앱에 대한 모든 저작권은 testdriven.io에 있음
일반적으로 Flask를 backend라고 부르고 Vue를 frontend라고 부르기 때문에, 하나의 프로젝트 저장소나 디렉토리 하위에 frontend와 backend 디렉토리를 생성하여 별도의 웹서비스인 것처럼 개발을 시작함
index.py라는 entry point 파일이 존재한다고 가정함
mkdir backend
cd backend
pip install Flask
# 기타 다른 python 모듈 설치
# 또는 requirements.txt 파일이 정의되어 있다면 pip install -r requirements.txt 실행
./index.py
실행에 성공하면 기본적으로 5000번 포트를 점유하게 되므로 다음 명령으로 정상 동작 여부를 확인할 수 있음
curl localhost:5000
# 우선 OS에 vue cli와 를 전역적으로 설치함
sudo npm install -g @vue/cli
sudo npm install -g @vue/cli-init
# frontend 디렉토리 생성
vue create frontend
cd frontend
npm install
# Vue 앱에 따라 필요한 의존성 설치
npm install --save axios
npm install --save bootstrap
npm install --save bootstrap-vue
npm run dev
일반적으로는 eslint로 인해서 컴파일에 실패하는데, 무시해도 되는 경고인 경우에는 .eslintrc.js 파일에 설정을 추가하여 무시할 수 있음
아래 rules 설정에서 indent, semi, no-trailing-spaces, comma-dangle, space-before-function-paren 등은 주로 들여쓰기, 공백이나 군더더기 세미콜론/쉼표와 관련된 규칙이라서 off 처리하였음
rules: {
// allow async-await
'generator-star-spacing': 'off',
'indent': 'off',
'semi': 'off',
'no-trailing-spaces': 'off',
'comma-dangle': 'off',
'space-before-function-paren': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
npm run dev로 실행에 성공하면 기본적으로 8080 포트를 점유하게 되므로 다음 명령으로 정상 동작 여부를 확인할 수 있음
curl localhost:8080
그러나 Vue와 같은 frontend framework들은 Javascript가 활성화된 브라우저에서만 정상적인 결과를 보여주므로 curl 보다는 Chrome이나 Firefox 등의 웹브라우저를 이용하여 확인해보는 게 필요함
개발 단계에서 Vue 애플리케이션은 axios를 이용하여 Ajax 요청을 backend로 호출하게 되는데, 일반적으로 http://localhost:5000 으로 시작하는 주소를 사용하게 됨
backend는 오로지 API 서비스로서만 기능하게 하고 frontend가 개발용 웹서비스로서 동작하는 것임. 다음과 같은 구성을 가진다고 할 수 있음
그러나 실제 서비스(프러덕션) 단계에서는 다음과 같은 구성을 취할 수 있음
frontend는 정적 자산(static assets)으로 빌드/패키징되기 때문에 굳이 node.js를 서버로 사용할 필요가 없음
그러므로 Apache HTTPd나 Nginx를 이용하여 정적 자산을 서비스하는 방식을 채택함
하나의 웹서비스가 frontend의 정적 자산과 backend의 비즈니스 로직을 모두 담당함으로써 서버 자원을 최소화할 수 있는 장점을 가짐
그러나 기업 환경에서는 서비스의 워크로드에 따른 스케일 아웃이나 보안 정책 때문에 2안을 사용하지 않음
BASE_URL을 기본값으로 설정해두고 사용하는 전략이 더 적합함
(BASE_URL이란 https://book.example.com/mybookapp인 경우 /mybookapp이 그에 해당됨)
1안의 구성을 따르기로 한다면 https://book.example.com이 됨
BASE_URL과 assetsPublicPath 모두 별도 설정없이 기본값인 /로 설정할 수 있음
그러나 부득이하게 BASE_URL을 사용해야 하는 경우에는 다음 설정을 잊지 말 것
module.exports = {
NODE_ENV: '"production"',
}
backend의 URL prefix가 별도로 지정되는 경우에는 BASE_URL 항목을 반드시 설정해주어야 함
module.exports = {
NODE_ENV: '"production"',
BASE_URL: '"/book/"',
}
config 디렉토리의 파일에서 설정할 수 있는 항목으로 HOST와 PORT가 있지만, 이것은 frontend 서버를 별도로 띄울 경우에만 필요하고 backend에 정적 자산으로 서비스를 위임할 때는 사용되지 않음
build: {
assetsPublicPath: '/',
build: {
assetsPublicPath: '/book/',
config/index.js를 수정하지 않고 기본값으로 사용하게 되면, frontend/dist 디렉토리에 정적 자산이 생성됨
다음 설정을 이용하여 dist에 패키징된 결과가 저장되는 게 아니라 backend/dist에 바로 저장할 수 있음
build: {
// Template for index.html
index: path.resolve(__dirname, 'Apache_HTTPd가_담당하는_정적 자산_설치위치/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, 'Apache_HTTPd가_담당하는_정적 자산_설치위치'),
assetsSubDirectory: 'static',
build: {
// Template for index.html
index: path.resolve(__dirname, '/var/www/html/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '/var/www/html/'),
assetsSubDirectory: 'static',
Apache HTTPd가 담당하는 정적 자산 설치위치는 apache2.conf(또는 httpd.conf)에서 DocumentRoot를 참고할 것
별도 위치를 사용할 경우, Alias 지시어를 이용하여 서비스할 수 있음
참고: https://httpd.apache.org/docs/2.4/ko/urlmapping.html
Flask로 backend 서비스를 구성하는 것은 다음 문서를 참고할 것
중요 코드만 보면 다음과 같음
app = Flask(__name__)
app.config.from_object(__name__)
Flask 서비스 인스턴스를 생성할 때 정적 자산 디렉토리와 HTML 템플릿 디렉토리를 지정할 수 있음
./dist/static, ./dist는 frontend에서 패키징을 담당하는 webpack의 기본 디렉토리 구조에 맞춘 값임
app = Flask(__name__, static_folder = "./dist/static", template_folder = "./dist")
app.config.from_object(__name__)
최상위 URL과 여타 URL에 대한 요청은 index.html로 응답함
index.html로 응답하면 webpack으로 패키징된 index.html + js + css 등의 정적 자산이 클라이언트에게 서비스됨
@app.route('/', defaults={'path': ''})
def catch_all(path):
if app.debug:
return requests.get('http://localhost:8080/{}'.format(path)).text
return render_template("index.html")
API의 URL은 /mybookapp/books로 시작하도록 정의함
필요한 경우, 버전 정보를 포함하기도 함 (예. /mybookapp/1.2/books)
@app.route("/book/books", methods=["GET", "POST"])
def all_books():
pass
@app.route("/book/books/<book_id>", methods=["PUT", "DELETE"])
def single_book(book_id):
pass
Flask 앱을 실행함
if __name__ == "__main__":
app.run(host="127.0.0.1")
전체 코드는 다음과 같음
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import uuid
from flask import Flask, jsonify, request, render_template
from flask_cors import CORS
app = Flask(__name__)
app.config.from_object(__name__)
CORS(app, resources={r"/*": {"origins": "*"}})
#@app.route('/', defaults={'path': ''})
#@app.route('/<path:path>')
#def catch_all(path):
# if app.debug:
# return requests.get('http://localhost:8080/{}'.format(path)).text
# return render_template("index.html")
BOOKS = [
{
'id': uuid.uuid4().hex,
'title': 'On the Road',
'author': 'Jack Kerouac',
'read': True
},
{
'id': uuid.uuid4().hex,
'title': 'Harry Potter and the Philosopher\'s Stone',
'author': 'J. K. Rowling',
'read': False
},
{
'id': uuid.uuid4().hex,
'title': 'Green Eggs and Ham',
'author': 'Dr. Seuss',
'read': True
}
]
@app.route("/book/books", methods=["GET", "POST"])
def all_books():
response_object = {"status": "success"}
if request.method == "POST":
post_data = request.get_json()
BOOKS.append({
"id": uuid.uuid4().hex,
"title": post_data.get("title"),
"author": post_data.get("author"),
"read": post_data.get("read")
})
response_object["message"] = "BOOK added!"
else:
response_object["books"] = BOOKS
return jsonify(response_object)
@app.route("/book/books/<book_id>", methods=["PUT", "DELETE"])
def single_book(book_id):
response_object = {"status": "success"}
if request.method == "PUT":
post_data = request.get_json()
remove_book(book_id)
BOOKS.append({
"id": uuid.uuid4().hex,
"title": post_data.get("title"),
"author": post_data.get("author"),
"read": post_data.get("read")
})
response_object["message"] = "Book updated!"
if request.method == "DELETE":
remove_book(book_id)
response_object["message"] = "Book removed!"
return jsonify(response_object)
def remove_book(book_id):
for book in BOOKS:
if book["id"] == book_id:
BOOKS.remove(book)
return True
return False
if __name__ == "__main__":
app.run(host="127.0.0.1")
frontend 코드에 변경이 생기면 다음 명령을 실행하여 배포하면 됨
cd frontend
npm run build
backend 코드에 변경이 생기면 다음 명령을 실행하여 재시작하면 됨
cd backend
touch index.wsgi