2 / 2

04: SSR 가능 여부를 결정하는 것 = 언어 런타임

예상 시간: 5분

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에는:

  1. renderToString() 같은 함수가 없음 — 이건 JavaScript 라이브러리(react-dom/server)
  2. JavaScript 엔진이 없음 — function이나 React.createElement를 이해 못 함
  3. 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분