1 / 2

01: Flask에서 React/Svelte 정적 파일 서빙

예상 시간: 5분

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분