01: Flask에서 React/Svelte 정적 파일 서빙
01: Flask에서 React/Svelte 정적 파일 서빙
이 문서에서 배우는 것
- React/Svelte를 빌드하면 결국 무엇이 나오는지
- Flask의
send_from_directory로 빌드된 파일을 서빙하는 방법 - 경로별로 다른 프레임워크(React, Svelte, Vue)를 동시에 서빙하는 방법
- API 백엔드로만 사용하는 SPA 방식과의 비교
PM 요청 (김도연)
김도연 PM: 준혁님, 저희 프로젝트에서 Flask 백엔드를 쓰고 있는데요, 프론트엔드를 React로 만들고 싶어요. Flask에서 React를 쓸 수 있나요? 아니면 서버를 Node.js로 바꿔야 하나요?
이준혁 시니어: 아, 그 걱정 안 해도 돼! Flask에서 React를 완전히 서빙할 수 있어. 사실 핵심은 간단해 — React를 빌드하면 결국 그냥 파일이 나온다는 거거든.
시니어 멘토링 (이준혁)
질문 1: React/Svelte를 빌드하면 뭐가 나오지?
이준혁: 먼저 물어볼게. React 프로젝트에서 npm run build를 실행하면 뭐가 나올 것 같아?
김도연: 음... 뭔가 복잡한 게 나오지 않나요? 서버가 필요한 무언가?
이준혁: 아니야! 생각보다 단순해. 확인해보자.
# React 프로젝트 빌드 (Vite 기준)
$ cd my-react-app
$ npm run build
# 빌드 결과물 확인
$ ls dist/
index.html
assets/
index-DiwrgTda.css # CSS 파일
index-CdTgQbWo.js # JavaScript 번들
logo-BpkVMmHf.svg # 이미지 등 에셋이준혁: 보이지? 결과물은 딱 3가지야:
┌─────────────────────────────────────────────────┐
│ React/Svelte 빌드 결과물 │
│ │
│ 📄 index.html — 진입점 HTML 파일 │
│ 📦 *.js — JavaScript 번들 │
│ 🎨 *.css — 스타일시트 │
│ 🖼️ *.svg/png — 이미지 등 에셋 │
│ │
│ → 전부 "정적 파일(Static Files)" │
│ → 어떤 웹 서버든 서빙 가능! │
└─────────────────────────────────────────────────┘김도연: 그냥 HTML, JS, CSS 파일이요? 서버가 필요 없는 건가요?
이준혁: 정확해! 빌드된 React/Svelte는 그냥 파일이야. Nginx, Apache, Flask, Express, 심지어 GitHub Pages에서도 서빙할 수 있어. 서버는 이 파일들을 "전달"해주기만 하면 돼.
질문 2: Svelte도 마찬가지야?
김도연: React는 알겠는데, Svelte도 같나요?
이준혁: 완전 같아! 비교해보자.
# Svelte 프로젝트 빌드 (Vite 기준)
$ cd my-svelte-app
$ npm run build
$ ls dist/
index.html
assets/
index-BkH2wQ3r.css
index-Dpl1XfMb.js# Vue 프로젝트 빌드 (Vite 기준)
$ cd my-vue-app
$ npm run build
$ ls dist/
index.html
assets/
index-D4xHv8pU.css
index-CzO2olXS.js이준혁: React, Svelte, Vue — 전부 빌드하면 HTML + JS + CSS 파일이 나와. 프레임워크가 다르지만 결과물의 형태는 동일해.
React ─── npm run build ──→ HTML + JS + CSS
Svelte ─── npm run build ──→ HTML + JS + CSS
Vue ─── npm run build ──→ HTML + JS + CSS
→ 결국 다 똑같은 "정적 파일"질문 3: Flask에서 어떻게 서빙해?
김도연: 그럼 이 파일들을 Flask에서 어떻게 보여주나요?
이준혁: Flask에는 send_from_directory라는 함수가 있어. 이걸로 정적 파일을 서빙할 수 있지.
가장 단순한 Flask + React 구조
my-project/
├── app.py # Flask 서버
├── api/ # API 라우트들
│ └── users.py
└── frontend/
└── dist/ # React 빌드 결과물
├── index.html
└── assets/
├── index-CdTgQbWo.js
└── index-DiwrgTda.css# app.py
from flask import Flask, send_from_directory
import os
app = Flask(__name__)
# React 빌드 폴더 경로
REACT_BUILD = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
# ── API 라우트 ──────────────────────────
@app.route('/api/users')
def get_users():
return {'users': ['Alice', 'Bob', 'Charlie']}
# ── React 정적 파일 서빙 ────────────────
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve_react(path):
# 요청된 파일이 존재하면 그 파일을 보내고
if path and os.path.exists(os.path.join(REACT_BUILD, path)):
return send_from_directory(REACT_BUILD, path)
# 없으면 index.html을 보낸다 (SPA 라우팅을 위해)
return send_from_directory(REACT_BUILD, 'index.html')
if __name__ == '__main__':
app.run(port=5000)이준혁: 이게 전부야! 핵심은 두 줄이야:
# 파일이 있으면 → 그 파일 전송 (JS, CSS, 이미지 등)
return send_from_directory(REACT_BUILD, path)
# 파일이 없으면 → index.html 전송 (SPA 라우팅 처리)
return send_from_directory(REACT_BUILD, 'index.html')김도연: 왜 파일이 없을 때 index.html을 보내나요?
이준혁: 좋은 질문이야! SPA(Single Page Application)는 클라이언트 사이드 라우팅을 쓰거든. 예를 들어 사용자가 /dashboard에 접속하면:
브라우저 → /dashboard 요청
Flask → dashboard 파일 없음 → index.html 전송
브라우저 → index.html 로드 → JS 실행 → React Router가 /dashboard 렌더링모든 라우팅은 React(클라이언트)가 처리하니까, Flask는 항상 index.html만 보내주면 돼.
질문 4: 경로별로 다른 프레임워크를 동시에 서빙할 수 있어?
김도연: 저희 프로젝트에서 관리자 페이지는 React로, 사용자 대시보드는 Svelte로 만들고 싶은데... 가능한가요?
이준혁: 물론이지! 각각 빌드해서 다른 폴더에 넣고, Flask에서 경로별로 다르게 서빙하면 돼.
멀티 프레임워크 프로젝트 구조
my-project/
├── app.py
├── api/
│ └── users.py
├── frontend-react/ # 관리자 페이지 (React)
│ └── dist/
│ ├── index.html
│ └── assets/
├── frontend-svelte/ # 사용자 대시보드 (Svelte)
│ └── dist/
│ ├── index.html
│ └── assets/
└── frontend-vue/ # 랜딩 페이지 (Vue)
└── dist/
├── index.html
└── assets/# app.py — 경로별 다른 프레임워크 서빙
from flask import Flask, send_from_directory
import os
app = Flask(__name__)
BASE_DIR = os.path.dirname(__file__)
REACT_BUILD = os.path.join(BASE_DIR, 'frontend-react', 'dist')
SVELTE_BUILD = os.path.join(BASE_DIR, 'frontend-svelte', 'dist')
VUE_BUILD = os.path.join(BASE_DIR, 'frontend-vue', 'dist')
# ── API 라우트 ──────────────────────────────────────
@app.route('/api/users')
def get_users():
return {'users': ['Alice', 'Bob', 'Charlie']}
@app.route('/api/stats')
def get_stats():
return {'visits': 1234, 'signups': 56}
# ── /admin/* → React 서빙 ──────────────────────────
@app.route('/admin/', defaults={'path': ''})
@app.route('/admin/<path:path>')
def serve_admin(path):
if path and os.path.exists(os.path.join(REACT_BUILD, path)):
return send_from_directory(REACT_BUILD, path)
return send_from_directory(REACT_BUILD, 'index.html')
# ── /dashboard/* → Svelte 서빙 ─────────────────────
@app.route('/dashboard/', defaults={'path': ''})
@app.route('/dashboard/<path:path>')
def serve_dashboard(path):
if path and os.path.exists(os.path.join(SVELTE_BUILD, path)):
return send_from_directory(SVELTE_BUILD, path)
return send_from_directory(SVELTE_BUILD, 'index.html')
# ── / (루트) → Vue 랜딩 페이지 서빙 ────────────────
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve_landing(path):
if path and os.path.exists(os.path.join(VUE_BUILD, path)):
return send_from_directory(VUE_BUILD, path)
return send_from_directory(VUE_BUILD, 'index.html')
if __name__ == '__main__':
app.run(port=5000)이준혁: 이렇게 하면:
/admin/* → React 관리자 페이지
/dashboard/* → Svelte 사용자 대시보드
/* → Vue 랜딩 페이지
/api/* → Flask API각 프레임워크는 독립적으로 빌드되고, Flask는 경로만 보고 다른 폴더에서 파일을 꺼내주는 것뿐이야.
┌─────────────────────────────────────────────────────────┐
│ Flask 서버 │
│ │
│ /admin/* ──→ frontend-react/dist/ │
│ /dashboard/* ──→ frontend-svelte/dist/ │
│ /* ──→ frontend-vue/dist/ │
│ /api/* ──→ Python 비즈니스 로직 │
│ │
│ Flask는 "파일 배달부"일 뿐! │
└─────────────────────────────────────────────────────────┘김도연: 와, Flask는 정말 파일만 전달해주는 거네요. React인지 Svelte인지 모르는 거죠?
이준혁: 정확해! Flask 입장에서는 전부 그냥 HTML, JS, CSS 파일이야. React든 Svelte든 Vue든 구분할 필요 없어. 빌드하면 다 똑같은 정적 파일이니까.
질문 5: SPA 방식 vs 정적 파일 서빙 방식, 뭐가 다른 거야?
김도연: 그런데 "SPA 방식으로 API 백엔드로만 사용한다"는 말도 들어봤는데, 지금 설명해주신 것과 뭐가 다른 건가요?
이준혁: 좋은 질문이야! 사실 두 가지 방식이 있어.
방식 1: Flask가 정적 파일도 서빙 (지금 설명한 방식)
┌──────────┐ ┌──────────────────────────┐
│ 브라우저 │──────→│ Flask (포트 5000) │
│ │ HTML │ │
│ │←──────│ / → index.html │
│ │ JS │ /assets → JS/CSS 파일 │
│ │←──────│ /api → JSON 데이터 │
└──────────┘ └──────────────────────────┘
→ 서버 1개로 모든 걸 처리
→ 배포가 간단
→ CORS 문제 없음방식 2: Flask는 API만, 프론트는 별도 서버 (분리 배포)
┌──────────┐ ┌──────────────────────────┐
│ 브라우저 │──────→│ Nginx (포트 80) │
│ │ HTML │ → 정적 파일 서빙 │
│ │←──────│ index.html, JS, CSS │
│ │ └──────────────────────────┘
│ │
│ │ ┌──────────────────────────┐
│ │──────→│ Flask (포트 5000) │
│ │ JSON │ → API만 처리 │
│ │←──────│ /api/users → JSON │
└──────────┘ └──────────────────────────┘
→ 서버 2개 (혹은 CDN + 서버)
→ 각각 독립적으로 스케일링 가능
→ CORS 설정 필요이준혁: 비교해보면:
| 항목 | 방식 1 (Flask 통합) | 방식 2 (분리 배포) |
|---|---|---|
| 서버 수 | 1개 | 2개 이상 |
| 배포 | 간단 (한 곳에 배포) | 복잡 (프론트/백엔드 따로) |
| CORS | 필요 없음 (같은 origin) | 설정 필요 |
| 스케일링 | 함께 스케일 | 독립적 스케일 |
| CDN 활용 | 어려움 | 정적 파일은 CDN 가능 |
| 개발 환경 | 한 프로세스 | 두 프로세스 (프론트 dev 서버 + Flask) |
| 적합한 규모 | 소~중규모 | 중~대규모 |
김도연: 소규모 프로젝트면 방식 1이 더 편하겠네요?
이준혁: 맞아! 처음 시작할 때는 방식 1이 훨씬 간단해. 나중에 트래픽이 늘어나면 방식 2로 전환하면 되고. 실제로 많은 스타트업이 처음에 방식 1로 시작해서 나중에 방식 2로 넘어가.
개발 환경 vs 프로덕션 환경
김도연: 한 가지 더 궁금한 게 있어요. 개발할 때도 이렇게 하나요?
이준혁: 아니! 개발할 때는 보통 다르게 해.
개발 환경 (Development)
┌──────────┐ ┌───────────────────────────┐
│ 브라우저 │──────→│ Vite Dev Server (포트 5173)│
│ │ HTML │ → React/Svelte 개발 서버 │
│ │←──────│ → HMR (코드 수정 시 즉시 반영)│
│ │ └───────────────────────────┘
│ │ │ (프록시)
│ │ ┌───────────────────────────┐
│ │ JSON │ Flask (포트 5000) │
│ │←──────│ → API만 처리 │
└──────────┘ └───────────────────────────┘// vite.config.js — 개발 시 API 프록시 설정
export default {
server: {
proxy: {
'/api': 'http://localhost:5000' // API 요청은 Flask로 프록시
}
}
}프로덕션 환경 (Production)
# 1. 프론트엔드 빌드
$ cd frontend
$ npm run build # → dist/ 폴더에 정적 파일 생성
# 2. 빌드 결과물을 Flask 프로젝트로 복사
$ cp -r dist/ ../flask-app/frontend/dist/
# 3. Flask 서버 실행 (정적 파일 + API 모두 처리)
$ cd ../flask-app
$ python app.py이준혁: 정리하면:
- 개발: Vite Dev Server (HMR) + Flask (API) → 서버 2개
- 프로덕션: Flask (정적 파일 + API) → 서버 1개
개발할 때는 Vite의 HMR(Hot Module Replacement)이 너무 편하니까 프론트 개발 서버를 따로 띄우고, 배포할 때는 빌드해서 Flask에 합치는 거야.
실습: 최소 Flask + React 프로젝트
이준혁: 직접 해보자. 5분이면 돼.
Step 1: React 프로젝트 생성 및 빌드
# React 프로젝트 생성
$ npm create vite@latest frontend -- --template react
$ cd frontend
$ npm install
$ npm run build
# 빌드 결과 확인
$ ls dist/
index.html assets/Step 2: Flask 서버 작성
# app.py
from flask import Flask, send_from_directory, jsonify
import os
app = Flask(__name__)
REACT_BUILD = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
@app.route('/api/hello')
def hello():
return jsonify({'message': 'Hello from Flask!'})
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
if path and os.path.exists(os.path.join(REACT_BUILD, path)):
return send_from_directory(REACT_BUILD, path)
return send_from_directory(REACT_BUILD, 'index.html')
if __name__ == '__main__':
app.run(port=5000, debug=True)Step 3: 실행
$ pip install flask
$ python app.py
# 브라우저에서 http://localhost:5000 접속
# → React 앱이 Flask에서 서빙됨!김도연: 이게 진짜 이게 전부인 거예요?
이준혁: 응! Flask에서 React를 서빙하는 건 이게 전부야. Flask는 파일을 전달하는 역할만 하니까.
핵심 정리
빌드 결과물의 본질
React/Svelte/Vue ──→ npm run build ──→ HTML + JS + CSS
(정적 파일)빌드된 프론트엔드는 그냥 파일이다. 어떤 웹 서버에서든 서빙할 수 있다.
Flask에서 서빙하는 핵심 코드
# 이 두 줄이 전부
send_from_directory(BUILD_DIR, path) # 파일이 있으면 전송
send_from_directory(BUILD_DIR, 'index.html') # 없으면 index.html경로별 다른 프레임워크
/admin/* → React (관리자)
/dashboard/* → Svelte (대시보드)
/* → Vue (랜딩)
/api/* → Flask (API)Flask 입장에서는 전부 같은 정적 파일 — 프레임워크를 구분하지 않는다.
핵심 한 줄
"React/Svelte/Vue를 빌드하면 그냥 파일이 된다. Flask는 그 파일을 전달해줄 뿐이다."
다음 단계
02-csr-vs-ssr.md에서 Flask가 서빙하는 이 **정적 파일 방식(CSR)**이 뭔지, 그리고 **서버에서 HTML을 직접 만드는 방식(SSR)**과 어떻게 다른지 알아봅니다.
작성: 2026-02-09 버전: 1.0 예상 독서 시간: 10분