05: 서버-프론트엔드 조합 가이드 — 언제 뭘 쓰는가
예상 시간: 5분
05: 서버-프론트엔드 조합 가이드 — 언제 뭘 쓰는가
이 문서에서 배우는 것
- Flask + React/Svelte (정적 빌드): CSR 조합
- Express + React/Svelte (직접 SSR): 수동 배관 조합
- Next.js (React): 프로덕션 표준 SSR
- SvelteKit (Svelte): 프로덕션 표준 SSR
- 각 조합의 장단점 표
- 실무 선택 가이드: 상황별 최적 조합
PM 요청 (김도연)
김도연 PM: 준혁님, 01~04편을 통해 정적 파일 서빙, CSR/SSR 차이, Next.js가 왜 다른지, 언어 런타임이 SSR을 결정한다는 걸 배웠는데요. 이제 종합적으로 정리해주세요. 실제로 프로젝트를 시작할 때 어떤 조합을 선택해야 하나요?
이준혁 시니어: 좋아! 지금까지 배운 걸 전부 종합해서 실무 가이드를 만들어보자. 각 조합의 장단점과 언제 뭘 써야 하는지 명확하게 정리할게.
시니어 멘토링 (이준혁)
4가지 주요 조합
이준혁: 서버-프론트엔드 조합은 크게 4가지로 나뉘어.
┌─────────────────────────────────────────────────────────────┐
│ 조합 1: Flask + React/Svelte (정적 빌드) │ CSR │
│ 조합 2: Express + React/Svelte (직접 SSR) │ 수동 SSR │
│ 조합 3: Next.js (React 내장) │ 자동 SSR │
│ 조합 4: SvelteKit (Svelte 내장) │ 자동 SSR │
└─────────────────────────────────────────────────────────────┘하나씩 자세히 보자.
조합 1: Flask + React/Svelte (정적 빌드)
구조
┌──────────┐ ┌──────────────────────────┐
│ 브라우저 │──────→│ Flask (Python) │
│ │ │ │
│ │←──────│ /api/* → JSON 응답 │
│ │ HTML │ /* → dist/index.html │
│ │←──────│ → 정적 파일 서빙 │
└──────────┘ └──────────────────────────┘코드
# app.py — Flask 서버
from flask import Flask, send_from_directory, jsonify
import os
app = Flask(__name__)
BUILD_DIR = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
# API
@app.route('/api/users')
def get_users():
users = db.query('SELECT * FROM users')
return jsonify(users)
# 정적 파일 서빙 (React/Svelte 빌드 결과물)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
if path and os.path.exists(os.path.join(BUILD_DIR, path)):
return send_from_directory(BUILD_DIR, path)
return send_from_directory(BUILD_DIR, 'index.html')// frontend/src/App.jsx — React (CSR)
import { useState, useEffect } from 'react';
export default function App() {
const [users, setUsers] = useState([]);
useEffect(() => {
// 브라우저에서 API 호출
fetch('/api/users')
.then(res => res.json())
.then(setUsers);
}, []);
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}특징
렌더링: CSR (브라우저가 화면 그림)
서버 언어: Python
프론트 빌드: npm run build → 정적 파일
서버 역할: 파일 전달 + API
SSR: 불가 (Python에서 JS 실행 불가)
SEO: 불리 (빈 HTML)장단점
| 장점 | 단점 |
|---|---|
| Python 백엔드 활용 가능 | SSR 불가 (SEO 불리) |
| 구조가 단순함 (서버 1개) | 초기 로딩 느림 (빈 HTML → JS 실행) |
| 배포 간단 | 프론트 빌드 필요 (npm run build) |
| 기존 Flask 프로젝트에 추가 쉬움 | HMR은 Vite 개발 서버 따로 필요 |
| 프론트 프레임워크 자유롭게 선택 | - |
조합 2: Express + React/Svelte (직접 SSR)
구조
┌──────────┐ ┌───────────────────────────────────┐
│ 브라우저 │──────→│ Express (Node.js) │
│ │ │ │
│ │←──────│ 1. renderToString(React컴포넌트) │
│ │ HTML │ 2. 완성된 HTML 응답 │
│ │←──────│ 3. JS 번들 서빙 (Hydration용) │
│ │ JS │ 4. /api/* → JSON 응답 │
└──────────┘ └───────────────────────────────────┘코드
// server.js — Express SSR
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import HomePage from './components/HomePage.jsx';
const app = express();
app.use('/public', express.static('public'));
// SSR
app.get('/', (req, res) => {
const html = renderToString(<HomePage />);
res.send(`<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script src="/public/bundle.js"></script>
</body>
</html>`);
});
// API
app.get('/api/users', async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
});
app.listen(3000);// build.js — 빌드 설정 (수동)
import esbuild from 'esbuild';
// 서버 번들
await esbuild.build({
entryPoints: ['./server.js'],
outfile: './dist/server.js',
platform: 'node',
bundle: true,
jsx: 'automatic',
});
// 클라이언트 번들 (Hydration용)
await esbuild.build({
entryPoints: ['./client/entry.jsx'],
outfile: './public/bundle.js',
platform: 'browser',
bundle: true,
jsx: 'automatic',
minify: true,
});// client/entry.jsx — Hydration
import { hydrateRoot } from 'react-dom/client';
import HomePage from '../components/HomePage.jsx';
hydrateRoot(document.getElementById('root'), <HomePage />);특징
렌더링: SSR (서버가 HTML 생성) + Hydration
서버 언어: JavaScript (Node.js)
빌드: 서버 번들 + 클라이언트 번들 수동 설정
서버 역할: HTML 생성 + JS 서빙 + API
SSR: 가능 (Node.js가 JS 실행)
SEO: 유리 (완성된 HTML)장단점
| 장점 | 단점 |
|---|---|
| SSR 완전 제어 가능 | 배관 코드를 직접 작성해야 함 |
| SSR 원리를 깊이 이해 가능 | 새 페이지 추가 시 5단계 수동 작업 |
| 프레임워크 의존성 없음 | 빌드 설정 직접 관리 (esbuild/webpack) |
| 극한의 커스터마이징 가능 | HMR 직접 구현 필요 |
| 학습 목적으로 매우 좋음 | Code Splitting 직접 구현 |
조합 3: Next.js (React)
구조
┌──────────┐ ┌───────────────────────────────────┐
│ 브라우저 │──────→│ Next.js (Node.js) │
│ │ │ │
│ │←──────│ 자동 SSR / SSG / ISR │
│ │ HTML │ 자동 Code Splitting │
│ │←──────│ 자동 Hydration │
│ │ JS │ API Routes 내장 │
│ │←──────│ Middleware 내장 │
└──────────┘ └───────────────────────────────────┘코드
// app/page.tsx — 서버 컴포넌트 (기본값)
// SSR 자동! 배관 코드 0줄!
export default async function HomePage() {
// 서버에서 직접 DB 조회 — API 라우트 불필요
const users = await db.query('SELECT * FROM users');
return (
<div>
<h1>사용자 목록</h1>
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
</div>
);
}// app/api/users/route.ts — API Route
export async function GET() {
const users = await db.query('SELECT * FROM users');
return Response.json(users);
}// app/dashboard/page.tsx — 클라이언트 컴포넌트
'use client';
import { useState } from 'react';
export default function Dashboard() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
</div>
);
}특징
렌더링: SSR / SSG / ISR / CSR (페이지별 선택)
서버 언어: JavaScript/TypeScript (Node.js)
빌드: next build (자동)
서버 역할: HTML 생성 + JS 서빙 + API + 미들웨어 + 모든 것
SSR: 자동 (기본값)
SEO: 최적 (자동 메타데이터, OG 태그, sitemap)장단점
| 장점 | 단점 |
|---|---|
| SSR/SSG/ISR 자동 지원 | Node.js 서버 필요 (정적 호스팅 불가) |
| 배관 코드 0줄 | 프레임워크에 종속 (Next.js 규칙 따라야) |
| API Routes 내장 | 내부 동작 블랙박스 |
| 자동 Code Splitting | 학습 곡선 (App Router, Server/Client Components) |
| Fast Refresh (HMR) | 빌드 시간 상대적으로 김 |
| Image/Font 최적화 자동 | Python/Ruby 백엔드와 혼용 시 복잡 |
| 대규모 생태계 + 커뮤니티 | - |
조합 4: SvelteKit (Svelte)
구조
┌──────────┐ ┌───────────────────────────────────┐
│ 브라우저 │──────→│ SvelteKit (Node.js) │
│ │ │ │
│ │←──────│ 자동 SSR / SSG │
│ │ HTML │ 자동 Hydration │
│ │←──────│ API Routes (Server Routes) 내장 │
│ │ JS │ 더 작은 JS 번들 (컴파일러) │
└──────────┘ └───────────────────────────────────┘코드
<!-- src/routes/+page.svelte — 페이지 -->
<script>
export let data;
</script>
<h1>사용자 목록</h1>
<ul>
{#each data.users as user}
<li>{user.name}</li>
{/each}
</ul>// src/routes/+page.server.ts — 서버 데이터 로딩
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
const users = await db.query('SELECT * FROM users');
return { users };
};// src/routes/api/users/+server.ts — API Route
import { json } from '@sveltejs/kit';
export async function GET() {
const users = await db.query('SELECT * FROM users');
return json(users);
}특징
렌더링: SSR / SSG / CSR (페이지별 선택)
서버 언어: JavaScript/TypeScript (Node.js)
빌드: vite build (자동)
서버 역할: HTML 생성 + JS 서빙 + API
SSR: 자동 (기본값)
SEO: 우수 (자동 메타데이터)
특이점: 컴파일러 기반 → 더 작은 JS 번들장단점
| 장점 | 단점 |
|---|---|
| SSR/SSG 자동 지원 | Node.js 서버 필요 |
| 더 작은 JS 번들 (컴파일러) | React보다 작은 생태계 |
| 직관적인 문법 | 채용 시장이 React보다 작음 |
| 빠른 빌드 속도 (Vite 기반) | 대규모 프로젝트 레퍼런스 적음 |
| Server Routes 내장 | 서드파티 라이브러리 선택지 적음 |
| 코드 양이 적음 (보일러플레이트 적음) | - |
종합 비교표
이준혁: 4가지 조합을 한눈에 비교해보자.
| 항목 | Flask + 정적 빌드 | Express + 직접 SSR | Next.js | SvelteKit |
|---|---|---|---|---|
| 렌더링 | CSR | SSR (수동) | SSR/SSG/ISR (자동) | SSR/SSG (자동) |
| 서버 언어 | Python | JavaScript | JavaScript | JavaScript |
| SSR | 불가 | 가능 (수동) | 자동 | 자동 |
| SEO | 불리 | 유리 | 최적 | 우수 |
| 초기 로딩 | 느림 | 빠름 | 빠름 | 빠름 |
| 서버 부하 | 낮음 | 높음 | 중~높음 | 중~높음 |
| 배포 복잡도 | 낮음 | 높음 | 중간 | 중간 |
| 배관 코드 | 최소 | 많음 | 없음 | 없음 |
| 빌드 설정 | 프론트만 | 서버+클라이언트 수동 | 자동 | 자동 |
| HMR | Vite 별도 | 직접 구현 | 자동 | 자동 |
| Code Splitting | Vite 자동 | 직접 구현 | 자동 | 자동 |
| API | Flask 직접 | Express 직접 | API Routes | Server Routes |
| 학습 목적 | 좋음 | 매우 좋음 | 중간 | 중간 |
| 프로덕션 적합 | 소규모 | 특수 케이스 | 표준 | 표준 |
| 기존 Python 코드 | 재사용 가능 | 재작성 필요 | 재작성 필요 | 재작성 필요 |
실무 선택 가이드
김도연: 그래서 결국 뭘 써야 하나요?
이준혁: 상황별로 명확한 가이드를 줄게.
의사결정 트리
프로젝트를 새로 시작하는가?
│
├── Yes: SSR이 필요한가?
│ │
│ ├── Yes: React 생태계를 원하는가?
│ │ ├── Yes → Next.js ⭐
│ │ └── No → SvelteKit ⭐
│ │
│ └── No: 어떤 프론트 프레임워크를 원하는가?
│ ├── React → Vite + React (정적 빌드)
│ ├── Svelte → Vite + Svelte (정적 빌드)
│ └── Vue → Vite + Vue (정적 빌드)
│ └── 서버? → 아무거나 (Flask, Express, Nginx, CDN)
│
└── No: 기존 백엔드가 있는가?
│
├── Flask/Django (Python):
│ ├── SSR 불필요 → Flask + 정적 빌드 (01편 방식)
│ ├── SSR 필요 + Flask 유지 → Flask + Next.js + Nginx
│ └── SSR 필요 + Flask 제거 가능 → Next.js로 전환
│
├── Express (Node.js):
│ ├── 학습 목적 → Express + 직접 SSR (조합 2)
│ └── 프로덕션 → Next.js 또는 SvelteKit으로 전환
│
└── 기타 (Rails, Spring, Go):
├── SSR 불필요 → 기존 서버 + 정적 빌드
└── SSR 필요 → Next.js/SvelteKit 별도 + 리버스 프록시시나리오별 추천
이준혁: 구체적인 시나리오로 보자.
시나리오 1: 개인 블로그
요구사항: SEO 중요, 콘텐츠 중심, 빠른 로딩
추천: Next.js (SSG/ISR)
이유:
- 블로그 글은 정적 생성(SSG)으로 빌드 시점에 HTML 생성
- ISR로 새 글 발행 시 자동 재생성
- SEO 자동 최적화 (메타태그, sitemap, OG 태그)시나리오 2: 사내 관리자 대시보드
요구사항: 로그인 필수, SEO 불필요, 차트/테이블 많음
추천: Flask + React (정적 빌드) 또는 Vite + React 단독
이유:
- SSR 불필요 (검색엔진 노출 안 해도 됨)
- 기존 Python 백엔드 재활용 가능
- 구조가 단순하고 배포 쉬움시나리오 3: E-commerce 사이트
요구사항: SEO 필수 (상품 검색), 빠른 로딩, 결제 시스템
추천: Next.js
이유:
- 상품 페이지: SSR/ISR (검색 노출 + 실시간 재고)
- 장바구니/결제: CSR (비공개, 인터랙티브)
- API Routes로 결제 로직 처리
- 페이지별로 렌더링 방식 선택 가능시나리오 4: 실시간 협업 도구 (Figma, Notion 같은)
요구사항: 실시간 인터랙션, 웹소켓, 로그인 필수
추천: Vite + React + 별도 백엔드 (Express/Flask)
이유:
- SSR 불필요 (로그인 후 사용)
- 실시간 인터랙션이 핵심 (CSR이 적합)
- 웹소켓 서버는 별도로 관리하는 게 좋음시나리오 5: 학습/교육 목적
요구사항: SSR 원리 이해, 직접 구현해보기
추천: Express + React (직접 SSR)
이유:
- renderToString, Hydration을 직접 구현
- 배관 코드를 직접 작성하면서 원리 이해
- Next.js의 "마법"이 실제로 무엇을 하는지 체득시나리오 6: 기존 Flask 프로젝트에 프론트엔드 추가
요구사항: Python 백엔드 유지, 프론트 현대화
추천: Flask + React/Svelte (정적 빌드)
이유:
- 기존 코드 수정 최소화
- 프론트엔드만 React/Svelte로 전환
- SSR 불필요하면 가장 간단한 방법2025년 기준 실무 현황
이준혁: 마지막으로 실무에서 실제로 뭘 많이 쓰는지 알려줄게.
┌─────────────────────────────────────────────────────────────┐
│ 2025년 기준 서버-프론트엔드 조합 현황 │
│ │
│ 🏆 가장 많이 사용: │
│ Next.js — React 프로젝트의 사실상 표준 │
│ 대기업, 스타트업 모두 채택 (Vercel, Netflix, TikTok 등) │
│ │
│ 📈 빠르게 성장: │
│ SvelteKit — Svelte 프로젝트의 표준 │
│ 작은 번들, 빠른 성능으로 인기 급상승 │
│ │
│ 💼 여전히 건재: │
│ Flask/Django + React (정적 빌드) │
│ Python 생태계가 강한 분야 (ML/AI, 데이터) │
│ │
│ 📚 교육/학습용: │
│ Express + React (직접 SSR) │
│ 원리를 이해하기 위한 용도로 활용 │
│ │
└─────────────────────────────────────────────────────────────┘이준혁: 핵심을 한 줄로:
SSR 필요 없으면: 아무 서버 + React/Svelte 정적 빌드
SSR 필요하면: Next.js (React) 또는 SvelteKit (Svelte)핵심 정리
4가지 조합 요약
1. Flask + 정적 빌드
→ CSR, 간단, SSR 불가, Python 활용
→ "SSR 필요 없고 Python 쓸 때"
2. Express + 직접 SSR
→ SSR 수동, 복잡, 원리 학습용
→ "SSR 원리를 배울 때"
3. Next.js
→ SSR 자동, 프로덕션 표준, React 생태계
→ "React로 프로덕션 서비스 만들 때"
4. SvelteKit
→ SSR 자동, 작은 번들, Svelte 생태계
→ "Svelte로 프로덕션 서비스 만들 때"최종 의사결정 요약표
| 상황 | 추천 조합 |
|---|---|
| Python 백엔드 + SEO 불필요 | Flask + React/Svelte 정적 빌드 |
| SSR 원리 학습 | Express + React 직접 SSR |
| React + SEO 필요 | Next.js |
| Svelte + SEO 필요 | SvelteKit |
| 새 프로젝트 + 빠른 개발 | Next.js 또는 SvelteKit |
| 사내 도구 / 대시보드 | Vite + React 또는 Svelte (CSR) |
핵심 한 줄
"SSR 필요 없으면 Flask + 정적 빌드로 충분하고, SSR 필요하면 Next.js 또는 SvelteKit을 쓴다."
T003 시리즈 전체 요약
이준혁: 5편 전체를 관통하는 하나의 이야기로 정리할게.
01편: React/Svelte를 빌드하면 "파일"이 된다. Flask도 서빙 가능.
↓
02편: 이 방식은 CSR. SSR은 서버가 HTML을 만드는 것.
↓
03편: Next.js 빌드 결과는 "파일"이 아니라 "애플리케이션". Node.js 필요.
↓
04편: SSR 가능 여부 = 언어 런타임. JS 컴포넌트 → JS 엔진(Node.js) 필요.
↓
05편: SSR 필요 없으면 아무 서버 + 정적 빌드. 필요하면 Next.js/SvelteKit.김도연: 결국 "빌드 결과물이 파일이냐 애플리케이션이냐"가 모든 것의 시작이었네요!
이준혁: 정확해! 그리고 그 차이를 만드는 건 SSR의 필요성이고, SSR이 가능한지를 결정하는 건 서버의 언어 런타임이야. 이 흐름을 이해하면 어떤 기술 조합이든 왜 그렇게 구성되는지 바로 보일 거야.
작성: 2026-02-09 버전: 1.0 예상 독서 시간: 15분