04: SSR 가능 여부를 결정하는 것 = 언어 런타임
04: SSR 가능 여부를 결정하는 것 = 언어 런타임
이 문서에서 배우는 것
- SSR의 본질: "서버에서 컴포넌트 코드를 실행해서 HTML 문자열을 만드는 것"
- JavaScript 컴포넌트를 실행하려면 JavaScript 엔진이 필요한 이유
- Flask(Python)에서 React/Svelte SSR이 불가능한 근본 원인
- 같은 논리로 Django 템플릿이 Node.js에서 안 되는 이유
- 억지로 하는 방법(PyV8 등)과 그것이 비현실적인 이유
PM 요청 (김도연)
김도연 PM: 준혁님, 03편에서 Next.js가 SSR을 하려면 Node.js가 필요하다고 했잖아요. 그런데 왜 Flask에서는 SSR을 못 하는 거예요? Flask도 서버인데, 서버에서 HTML을 만드는 건 Flask도 할 수 있잖아요?
이준혁 시니어: 아, 정확하게 핵심을 짚었어! Flask도 HTML을 만들 수 있지. Jinja2 템플릿으로 HTML을 만들잖아. 하지만 React 컴포넌트로 HTML을 만드는 건 못 해. 왜 그런지 한 번 파보자.
시니어 멘토링 (이준혁)
질문 1: SSR이 정확히 "무엇을 하는" 거야?
이준혁: SSR의 정의를 다시 짚어보자. SSR은:
SSR = 서버에서 컴포넌트 코드를 "실행"해서 HTML "문자열"을 만드는 것구체적으로 보면:
// React 컴포넌트
function HomePage() {
return (
<div>
<h1>Hello World</h1>
<p>Welcome to my site</p>
</div>
);
}
// SSR = 이 컴포넌트를 "실행"해서 HTML "문자열"로 변환
import { renderToString } from 'react-dom/server';
const html = renderToString(<HomePage />);
// 결과: "<div><h1>Hello World</h1><p>Welcome to my site</p></div>"이준혁: 핵심은 **"실행"**이야. renderToString()이 HomePage 함수를 **호출(실행)**해서 JSX를 HTML 문자열로 변환하는 거야.
김도연: 그러면... 이 renderToString() 함수는 JavaScript 코드니까...
이준혁: 맞아! 바로 그거야!
질문 2: JavaScript를 실행하려면 뭐가 필요해?
이준혁: JavaScript 코드를 실행하려면 뭐가 필요할까?
김도연: JavaScript 엔진이요?
이준혁: 정확해! 비유로 설명할게.
┌─────────────────────────────────────────────────────────────┐
│ │
│ 비유: "한국어로 쓰인 레시피" │
│ │
│ 📖 레시피가 한국어로 쓰여있다면, │
│ 한국어를 읽을 수 있는 요리사만 이 레시피로 요리 가능 │
│ │
│ 📖 레시피가 일본어로 쓰여있다면, │
│ 일본어를 읽을 수 있는 요리사만 이 레시피로 요리 가능 │
│ │
│ 같은 논리: │
│ │
│ 📝 컴포넌트가 JavaScript로 쓰여있다면, │
│ JavaScript를 실행할 수 있는 서버만 SSR 가능 │
│ │
│ 📝 템플릿이 Python(Jinja2)으로 쓰여있다면, │
│ Python을 실행할 수 있는 서버만 렌더링 가능 │
│ │
└─────────────────────────────────────────────────────────────┘각 서버가 "읽을 수 있는 언어"
이준혁: 서버마다 실행할 수 있는 언어가 정해져 있어.
┌──────────────────────────────────────────────────────────┐
│ 서버 (언어 런타임) │ 실행 가능한 코드 │
│━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━│
│ Node.js (Express) │ JavaScript / TypeScript │
│ Python (Flask/Django) │ Python │
│ Ruby (Rails) │ Ruby │
│ Java (Spring) │ Java / Kotlin │
│ Go (Gin) │ Go │
│ PHP (Laravel) │ PHP │
│ Elixir (Phoenix) │ Elixir │
└──────────────────────────────────────────────────────────┘이준혁: 그러면 React/Svelte 컴포넌트는 무슨 언어로 쓰여있지?
김도연: JavaScript요!
이준혁: 맞아! 그러면 어떤 서버에서만 SSR이 가능하지?
김도연: JavaScript를 실행할 수 있는 서버... Node.js요!
이준혁: 정답!
React 컴포넌트 (JavaScript) + Node.js (JavaScript 엔진) = SSR 가능 ✅
React 컴포넌트 (JavaScript) + Flask (Python 엔진) = SSR 불가 ❌
React 컴포넌트 (JavaScript) + Rails (Ruby 엔진) = SSR 불가 ❌
React 컴포넌트 (JavaScript) + Spring (Java 엔진) = SSR 불가 ❌질문 3: Flask에서 정말 안 돼? 코드로 보여줘
김도연: 직관적으로는 이해가 되는데, 실제로 왜 안 되는지 코드로 보고 싶어요.
이준혁: 좋아! Flask에서 React SSR을 시도해보자.
Flask에서 React SSR을 시도하면?
# app.py — Flask에서 React SSR 시도
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
# React 컴포넌트 (JavaScript 코드)
component = """
function HomePage() {
return React.createElement('div', null,
React.createElement('h1', null, 'Hello World'),
React.createElement('p', null, 'Welcome to my site')
);
}
"""
# 이 JavaScript 코드를 Python에서 실행할 수 있을까?
# html = renderToString(HomePage) ← 이런 함수가 Python에 없다!
# Python은 JavaScript를 "이해"할 수 없다.
# eval(component) ← Python eval은 Python 코드만 실행 가능
# 결론: 불가능!
return "Error: Cannot execute JavaScript in Python"이준혁: 보이지? Python에는:
renderToString()같은 함수가 없음 — 이건 JavaScript 라이브러리(react-dom/server)- JavaScript 엔진이 없음 —
function이나React.createElement를 이해 못 함 - JSX를 파싱할 수 없음 — JSX는 JavaScript 확장 문법
Flask(Python)이 할 수 있는 것:
✅ Python 코드 실행
✅ Jinja2 템플릿 렌더링
✅ SQL 쿼리 실행
✅ JSON 응답 생성
Flask(Python)이 할 수 없는 것:
❌ JavaScript 코드 실행
❌ React 컴포넌트 렌더링
❌ Svelte 컴포넌트 렌더링
❌ Vue 컴포넌트 렌더링Node.js(Express)에서 React SSR을 하면?
// server.js — Express에서 React SSR
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
const app = express();
// React 컴포넌트 (JavaScript 코드)
function HomePage() {
return (
<div>
<h1>Hello World</h1>
<p>Welcome to my site</p>
</div>
);
}
app.get('/', (req, res) => {
// JavaScript 엔진(V8)이 있으니까 JavaScript 코드를 실행할 수 있다!
const html = renderToString(<HomePage />);
// 결과: "<div><h1>Hello World</h1><p>Welcome to my site</p></div>"
res.send(`<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>`);
});
app.listen(3000);이준혁: Express는 Node.js 위에서 실행되고, Node.js는 V8 JavaScript 엔진을 가지고 있어. 그래서 React 컴포넌트(JavaScript 코드)를 실행해서 HTML 문자열로 변환할 수 있는 거야.
질문 4: 그러면 반대도 마찬가지야?
김도연: 그러면 Flask의 Jinja2 템플릿을 Node.js에서 쓸 수 있나요?
이준혁: 물론 안 돼! 같은 논리가 반대 방향으로도 적용돼.
Flask의 Jinja2 템플릿
# Flask에서 Jinja2 렌더링 — 작동함 ✅
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
users = ['Alice', 'Bob', 'Charlie']
# Jinja2 템플릿을 Python으로 렌더링
return render_template('home.html', users=users)<!-- templates/home.html (Jinja2 문법) -->
<h1>사용자 목록</h1>
<ul>
{% for user in users %}
<li>{{ user }}</li>
{% endfor %}
</ul>Flask(Python) + Jinja2 템플릿 = 렌더링 가능 ✅
→ Jinja2는 Python 라이브러리이고, Flask는 Python을 실행할 수 있으니까Node.js에서 Jinja2를 시도하면?
// Express에서 Jinja2를 시도 — 안 됨 ❌
import express from 'express';
// import jinja2 from 'jinja2'; ← 이런 npm 패키지는 없다!
// Jinja2는 Python 패키지이고, Node.js에서는 실행할 수 없다.
const app = express();
app.get('/', (req, res) => {
const users = ['Alice', 'Bob', 'Charlie'];
// {% for user in users %} ← Node.js는 이 문법을 모른다!
// Jinja2 엔진은 Python으로 작성되어 있고,
// Node.js에는 Python 엔진이 없다.
// 결론: 불가능!
});Node.js(Express) + Jinja2 템플릿 = 렌더링 불가 ❌
→ Jinja2는 Python 라이브러리이고, Node.js에는 Python 엔진이 없으니까이준혁: 대신 Node.js에는 자기만의 템플릿 엔진이 있어:
// Express에서 EJS 사용 — 작동함 ✅
import express from 'express';
const app = express();
app.set('view engine', 'ejs');
app.get('/', (req, res) => {
const users = ['Alice', 'Bob', 'Charlie'];
res.render('home', { users }); // EJS 템플릿 렌더링
});<!-- views/home.ejs (EJS 문법 — JavaScript 기반) -->
<h1>사용자 목록</h1>
<ul>
<% users.forEach(user => { %>
<li><%= user %></li>
<% }); %>
</ul>정리: 언어 런타임 × 템플릿/컴포넌트 호환표
┌──────────────────┬───────────────┬──────────────┬─────────────┐
│ │ React/Svelte │ Jinja2 │ EJS │
│ │ (JavaScript) │ (Python) │ (JavaScript)│
├──────────────────┼───────────────┼──────────────┼─────────────┤
│ Node.js │ ✅ │ ❌ │ ✅ │
│ (JavaScript) │ SSR 가능 │ 실행 불가 │ 렌더링 가능 │
├──────────────────┼───────────────┼──────────────┼─────────────┤
│ Flask │ ❌ │ ✅ │ ❌ │
│ (Python) │ 실행 불가 │ 렌더링 가능 │ 실행 불가 │
├──────────────────┼───────────────┼──────────────┼─────────────┤
│ Rails │ ❌ │ ❌ │ ❌ │
│ (Ruby) │ 실행 불가 │ 실행 불가 │ 실행 불가 │
│ │ │ │ ERB는 가능 │
├──────────────────┼───────────────┼──────────────┼─────────────┤
│ Spring │ ❌ │ ❌ │ ❌ │
│ (Java) │ 실행 불가 │ 실행 불가 │ 실행 불가 │
│ │ │ │ Thymeleaf OK │
└──────────────────┴───────────────┴──────────────┴─────────────┘이준혁: 패턴이 보이지?
각 서버 프레임워크는 자기 언어로 된 것만 실행 가능:
• Node.js → JavaScript (React, Svelte, EJS)
• Flask → Python (Jinja2, Mako)
• Rails → Ruby (ERB, Haml)
• Spring → Java (Thymeleaf, JSP)질문 5: 억지로 하는 방법은 없어?
김도연: 정말 방법이 아예 없나요? Python에서 JavaScript를 실행하는 라이브러리 같은 건 없어요?
이준혁: 있긴 해! 몇 가지 시도가 있었어.
방법 1: PyV8 / PyMiniRacer (Python에 V8 엔진 내장)
# PyMiniRacer — Python에서 V8 엔진 실행
from py_mini_racer import MiniRacer
ctx = MiniRacer()
# 간단한 JavaScript는 실행 가능
result = ctx.eval("2 + 3") # → 5
# 하지만 React 컴포넌트를 실행하려면?
# React, ReactDOMServer 라이브러리 전체를 V8에 로드해야 함
# + Babel로 JSX를 트랜스파일해야 함
# + 수백 개의 의존성을 관리해야 함
# + 메모리 관리, 에러 처리...방법 2: 서브프로세스로 Node.js 호출
# Flask에서 Node.js를 서브프로세스로 호출
import subprocess
import json
def render_react_component(component_name, props):
# Node.js 프로세스를 매 요청마다 실행
result = subprocess.run(
['node', 'render.js', component_name, json.dumps(props)],
capture_output=True,
text=True
)
return result.stdout
@app.route('/')
def home():
html = render_react_component('HomePage', {'title': 'Hello'})
return f"""
<html>
<body>
<div id="root">{html}</div>
<script src="/bundle.js"></script>
</body>
</html>
"""이준혁: 이 방법들의 문제:
┌─────────────────────────────────────────────────────────────┐
│ Python에서 JS 실행이 비현실적인 이유 │
│ │
│ 1. 성능 문제 │
│ • 서브프로세스: 매 요청마다 Node.js 프로세스 생성 │
│ → 100ms~500ms 오버헤드 │
│ • 내장 V8: 메모리 사용량 급증 (React 번들 로딩) │
│ │
│ 2. 복잡성 폭발 │
│ • React + 의존성 수백 개를 V8 컨텍스트에 로드 │
│ • JSX 트랜스파일 파이프라인 구축 │
│ • 두 언어 런타임 동시 관리 (Python + V8) │
│ • 버전 호환성 지옥 │
│ │
│ 3. 유지보수 불가 │
│ • React 업데이트 시 V8 바인딩도 업데이트 필요 │
│ • 디버깅이 극도로 어려움 (Python ↔ JS 경계) │
│ • 에러 추적, 로깅, 모니터링 모두 복잡 │
│ │
│ 4. 결론 │
│ • "가능은 하지만, 할 이유가 전혀 없다" │
│ • SSR이 필요하면 처음부터 Node.js를 쓰는 게 맞다 │
│ │
└─────────────────────────────────────────────────────────────┘김도연: 그러니까... 이론적으로 가능하지만 실무에서는 말이 안 되는 거군요.
이준혁: 정확해! 비유하면:
"한국어 레시피를 일본어 사전을 뒤져가며 번역해서 요리할 수 있을까?"
→ 가능은 하지만, 한국어를 아는 요리사를 고용하는 게 100배 낫다.
마찬가지로:
"Python에서 JavaScript를 실행해서 React SSR을 할 수 있을까?"
→ 가능은 하지만, Node.js 서버를 쓰는 게 100배 낫다.실무에서의 해법
김도연: 그러면 Flask를 쓰면서 SSR을 하고 싶으면 어떻게 해야 하나요?
이준혁: 현실적인 선택지는 3가지야.
선택지 1: SSR 포기하고 CSR 사용 (가장 간단)
# Flask — 정적 파일 서빙 (CSR)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
return send_from_directory('frontend/dist', path or 'index.html')장점: 간단, Flask만으로 충분
단점: SSR 없음 (SEO 불리, 초기 로딩 느림)
적합: 관리자 대시보드, 내부 툴, SEO 불필요한 서비스선택지 2: Next.js를 별도로 추가 (SSR 필요 시)
Nginx → /api/* → Flask (Python)
→ /* → Next.js (Node.js)장점: 완전한 SSR 지원
단점: 서버 2개 + 리버스 프록시, 복잡
적합: 기존 Flask API를 유지해야 하면서 SSR이 필요한 경우선택지 3: Flask 대신 Next.js로 전환 (새 프로젝트)
// Next.js API Route — Flask의 API를 대체
// app/api/users/route.ts
export async function GET() {
const users = await db.query('SELECT * FROM users');
return Response.json(users);
}
// Next.js 페이지 — SSR 자동
// app/page.tsx
export default async function Home() {
const users = await db.query('SELECT * FROM users');
return <UserList users={users} />;
}장점: 하나의 프레임워크로 API + SSR + 프론트엔드
단점: Python 백엔드 로직을 TypeScript로 재작성 필요
적합: 새 프로젝트, 또는 Python 의존성이 적은 경우Flask의 "SSR": Jinja2 서버 사이드 렌더링
김도연: 잠깐, Flask도 서버에서 HTML을 만들잖아요? Jinja2 템플릿으로요. 그것도 SSR 아닌가요?
이준혁: 아주 좋은 포인트야! 맞아, Flask의 Jinja2도 넓은 의미에서는 SSR이야!
# Flask + Jinja2 — 이것도 "서버 사이드 렌더링"이다!
@app.route('/users')
def users():
user_list = db.query('SELECT * FROM users')
return render_template('users.html', users=user_list)
# → 서버에서 HTML을 만들어서 보냄 = SSR!┌─────────────────────────────────────────────────────────────┐
│ "SSR"의 두 가지 의미 │
│ │
│ 넓은 의미의 SSR: │
│ • 서버에서 HTML을 만들어서 보내는 모든 방식 │
│ • Flask + Jinja2, Rails + ERB, PHP 등 │
│ • 웹의 원래 방식 (2010년 이전 대부분) │
│ │
│ 좁은 의미의 SSR (현대적 의미): │
│ • 서버에서 "JavaScript 컴포넌트"를 실행해서 HTML을 만드는 것│
│ • React SSR, Svelte SSR, Vue SSR │
│ • + Hydration으로 클라이언트에서 인터랙티브하게 만드는 것 │
│ │
│ 차이점: │
│ • Jinja2 SSR → 서버에서 HTML 생성 → 끝 (정적 페이지) │
│ • React SSR → 서버에서 HTML 생성 → Hydration (인터랙티브) │
│ │
└─────────────────────────────────────────────────────────────┘김도연: 아하! Flask도 SSR을 하긴 하지만, "React 컴포넌트"를 실행하는 SSR은 아닌 거네요.
이준혁: 정확해! Flask는 **Python 템플릿(Jinja2)**으로 SSR을 하고, Express/Next.js는 **JavaScript 컴포넌트(React/Svelte)**로 SSR을 해. 각자 자기 언어로 된 것만 실행할 수 있으니까.
핵심 정리
SSR = 서버에서 컴포넌트를 "실행"하는 것
SSR의 전제: 컴포넌트 코드를 "실행"할 수 있어야 한다
→ 실행하려면 해당 언어의 런타임이 필요
→ JavaScript 컴포넌트 → JavaScript 런타임(Node.js)
→ Python 템플릿 → Python 런타임(Flask)언어 런타임 호환표
React/Svelte Jinja2 EJS
(JavaScript) (Python) (JavaScript)
Node.js (JS) ✅ ❌ ✅
Flask (Python) ❌ ✅ ❌
Rails (Ruby) ❌ ❌ ❌ (ERB는 ✅)레시피 비유
한국어 레시피 → 한국어 아는 요리사만 가능
JavaScript 컴포넌트 → JavaScript 엔진 있는 서버만 SSR 가능
일본어 레시피 → 일본어 아는 요리사만 가능
Python 템플릿 → Python 엔진 있는 서버만 렌더링 가능핵심 한 줄
SSR 가능 여부를 결정하는 것은 프레임워크가 아니라 "언어 런타임"이다. React(JS)를 실행하려면 JS 엔진(Node.js)이 필요하고, Jinja2(Python)를 실행하려면 Python 엔진(Flask)이 필요하다.
다음 단계
05-server-frontend-combinations.md에서 이 모든 지식을 종합하여 "어떤 서버 + 어떤 프론트엔드 조합을 쓸 것인가"에 대한 실무 가이드를 다룹니다.
작성: 2026-02-09 버전: 1.0 예상 독서 시간: 12분