Step 07: 종합 비교 — Express SSR vs Next.js 의사결정 프레임워크
Step 07: 종합 비교 — Express SSR vs Next.js 의사결정 프레임워크
PM의 질문 (김도연)
김도연: "준혁님, 이제 Step 1부터 Step 6까지 모두 완료했는데요. 솔직히 머리가 복잡해요. 각 단계에서는 '아 이렇게 하는구나' 싶었는데, 지금까지 배운 걸 정리하면서 실제 프로젝트에서 어떤 걸 선택해야 할지 기준을 명확히 알고 싶어요."
이준혁: "좋은 질문이야. 지금까지 6단계의 배관 작업을 직접 해봤으니까, 이제 왜 Next.js가 존재하는지 완전히 이해했을 거야. 오늘은 전체를 조감하면서 의사결정 기준을 정리해보자."
1. 6단계 배관 작업 총정리
1.1 단계별 배관 코드량 비교
이준혁: "먼저 지금까지 우리가 한 작업을 숫자로 정리해보자."
| 영역 | Express 수동 배관 | Next.js 자동 배관 | 수동 코드량 |
|---|---|---|---|
| Step 1: SSR 구조 | renderToString + HTML 템플릿 수동 작성 + hydration 직접 연결 | page.tsx 생성하면 자동 실행 | ~60줄 → 0줄 |
| Step 2: Zustand | Factory 패턴 + JSON.stringify + window.__STATE__ 수동 주입 + 격리 처리 | Props 전달 + useEffect 초기화 | ~50줄 → 20줄 |
| Step 3: TanStack Query | prefetch + dehydrate + serialize + hydrate + clear 모두 수동 | <HydrationBoundary> 자동 처리 | ~80줄 → 30줄 |
| Step 4: 인증 | passport + session + requireAuth 미들웨어 + state 직렬화 수동 | NextAuth + middleware.ts 자동 | ~100줄 → 40줄 |
| Step 5: 라우팅 | 라우트 수동 등록 + SSR 보일러플레이트 반복 | 파일시스템 자동 + layout.tsx | 라우트당 30줄 → 0줄 |
| Step 6: 배포 | esbuild 4단계 + Docker + PM2 + 캐싱 설정 | next build + Vercel | ~100줄 → 5줄 |
| 합계 | ~440줄+ 배관 코드 | ~95줄 | 78% 감소 |
김도연: "와... 숫자로 보니까 확실히 차이가 크네요. 특히 Step 5의 '라우트당 30줄'이 무섭게 느껴져요."
이준혁: "맞아. 라우트가 10개면 300줄, 20개면 600줄이 보일러플레이트로 쌓여. 이게 Express SSR의 가장 큰 부담이지."
1.2 배관 누적 시각화
이준혁: "시각적으로 보면 더 명확해."
Express SSR — 기능 추가할 때마다 배관이 누적됨
Step 1: ████████ (기본 SSR, ~60줄)
Step 2: ████████████████ (+Zustand 직렬화, ~50줄)
Step 3: ████████████████████████████ (+TanStack Query dehydrate, ~80줄)
Step 4: ████████████████████████████████████████ (+인증 session+passport, ~100줄)
Step 5: ████████████████████████████████████████████████████ (+라우트마다 반복, 라우트당 ~30줄)
Step 6: ████████████████████████████████████████████████████████████████ (+빌드/배포, ~100줄)
──────────────────────────────────────────────────────────────────────────
총 ~440줄+ (라우트 수에 비례해 증가)
Next.js — 기능 추가해도 배관이 거의 늘지 않음
Step 1: ██ (page.tsx 생성)
Step 2: ███ (+Props 전달)
Step 3: █████ (+HydrationBoundary)
Step 4: ███████ (+middleware.ts)
Step 5: ███████ (+파일 추가만, 코드 증가 없음)
Step 6: █████████ (+next.config)
──────────────────────────────────────────────────────────────────────────
총 ~95줄 (라우트가 늘어도 증가량 미미)김도연: "Express는 계단식으로 쌓이는데, Next.js는 거의 수평이네요."
이준혁: "정확해. Express는 선형 증가(O(n)), Next.js는 **상수 시간(O(1))**에 가까워. 이게 프레임워크의 힘이지."
2. 철학적 차이
2.1 핵심 비교 테이블
이준혁: "기술적 차이 말고, 철학적 차이도 이해해야 해."
| 측면 | Express SSR | Next.js |
|---|---|---|
| 철학 | "모든 걸 직접 제어" | "최선의 방법 제공" |
| 비유 | 수동 변속기 자동차 | 자동 변속기 자동차 |
| 학습 가치 | 매우 높음 (SSR 본질 이해) | 중간 (프레임워크 사용법) |
| 투명성 | 높음 (모든 배관이 보임) | 중간 (일부 추상화) |
| 유연성 | 매우 높음 (뭐든 커스텀 가능) | 규칙 내 자유 |
| 생산성 | 낮음 (배관 작업 시간) | 높음 (자동화) |
| 유지보수 | 어려움 (커스텀 코드) | 쉬움 (표준 패턴) |
| 에코시스템 | 제한적 | 풍부 (Vercel, plugins 등) |
| 디버깅 | 쉬움 (내 코드) | 어려움 (프레임워크 내부) |
| 러닝커브 | 가파름 (모든 걸 알아야 함) | 완만함 (가이드 따라가기) |
김도연: "수동 변속기 vs 자동 변속기 비유가 딱이네요. 수동 변속기는 제어감은 있지만 운전이 피곤하고, 자동 변속기는 편한데 내부 동작은 모르겠고."
이준혁: "맞아. 그리고 중요한 건, 수동 변속기를 써본 사람은 자동 변속기의 내부 동작을 이해할 수 있다는 거야. 너는 지금 그 경험을 한 거고."
2.2 실제 사례로 보는 철학 차이
이준혁: "구체적인 사례로 보자. TanStack Query의 dehydrate를 기억해?"
Express SSR:
// server.js - 모든 게 명시적
const queryClient = new QueryClient();
await queryClient.prefetchQuery({ queryKey: ['user'], queryFn: fetchUser });
const dehydratedState = dehydrate(queryClient);
const serialized = JSON.stringify(dehydratedState);
const html = `<script>window.__REACT_QUERY_STATE__=${serialized}</script>`;
queryClient.clear(); // 요청별 격리를 위해 수동 정리Next.js:
// app/page.tsx - 추상화됨
const queryClient = new QueryClient();
await queryClient.prefetchQuery({ queryKey: ['user'], queryFn: fetchUser });
return <HydrationBoundary state={dehydrate(queryClient)}>...</HydrationBoundary>
// 나머지는 프레임워크가 알아서 함김도연: "Express는 JSON.stringify, <script> 태그, clear()까지 모두 보이는데, Next.js는 <HydrationBoundary>만 쓰면 끝이네요."
이준혁: "그래. Express는 투명하지만 번거롭고, Next.js는 편하지만 불투명해. 이게 트레이드오프야."
3. 의사결정 프레임워크
3.1 Express SSR을 선택해야 할 때
이준혁: "이제 실전 의사결정 기준을 알려줄게. 먼저 Express SSR을 선택해야 하는 경우."
✅ 1. 학습 목적
언제:
- SSR의 본질과 내부 동작을 깊이 이해하고 싶을 때
renderToString,dehydrate/hydrate가 어떻게 작동하는지 배우고 싶을 때- "왜 Next.js가 필요한가"를 체감하고 싶을 때
- 주니어 개발자 교육 목적
예시:
// 이 코드를 작성하면서 SSR의 핵심을 배움
const html = renderToString(<App />);
const state = JSON.stringify(store.getState());
const template = `
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script>window.__STATE__=${state}</script>
<script src="/client.js"></script>
</body>
</html>
`;김도연: "맞아요. 제가 Step 1에서 이 코드를 직접 작성하면서 'SSR은 서버에서 HTML을 만드는 것'을 완전히 이해했어요."
✅ 2. 특수한 요구사항
a) 기존 Express 서버에 SSR 부분 추가 (점진적 마이그레이션)
// 기존 Express API 서버에 SSR 라우트 추가
app.get('/api/*', apiRoutes); // 기존 API 유지
app.get('/admin/*', adminRoutes); // 기존 관리자 페이지 유지
app.get('/app/*', ssrReactRoutes); // React SSR 점진적 추가이준혁: "기존 서버를 Next.js로 완전히 갈아엎을 수 없을 때 유용해."
b) Multi-framework (React + Svelte + Vue 동시 사용)
// A003 프로젝트처럼 여러 프레임워크를 동시에 사용
app.get('/react/*', renderReactSSR);
app.get('/svelte/*', renderSvelteSSR);
app.get('/vue/*', renderVueSSR);김도연: "아! 그래서 A003 프로젝트가 Express를 쓴 거군요. Next.js는 React 전용이니까."
이준혁: "정확해. Multi-framework는 Express SSR의 독보적인 강점이지."
c) Next.js의 파일시스템 라우팅 규칙이 맞지 않을 때
// 복잡한 동적 라우팅이 필요한 경우
app.get('/products/:category/:subcategory?/:id?', (req, res) => {
// 유연한 매칭 로직
if (req.params.id) return renderProductDetail(req, res);
if (req.params.subcategory) return renderSubcategory(req, res);
return renderCategory(req, res);
});이준혁: "Next.js는 [...slug] 같은 패턴은 되지만, 완전히 자유로운 라우팅은 불가능해."
d) 빌드 파이프라인을 완전히 커스텀해야 할 때
// esbuild를 직접 제어해서 특수한 플러그인 사용
await esbuild.build({
plugins: [
myCustomPlugin(), // 커스텀 플러그인
specialTransformPlugin(), // 특수한 변환 로직
],
// 완전한 제어
});✅ 3. 팀/프로젝트 특성
a) 소규모 프로젝트 (라우트 5개 이하)
이준혁: "라우트가 5개 정도면 Express SSR의 보일러플레이트 부담이 적어. 오히려 Next.js의 설정이 오버엔지니어링일 수 있지."
b) 팀이 SSR 내부 동작을 잘 이해
이준혁: "팀 전체가 renderToString, dehydrate 같은 개념을 이해하고 있다면, Express SSR의 투명성이 오히려 장점이야."
c) 이미 Express 인프라가 구축되어 있음
이준혁: "PM2, 로드밸런서, 모니터링 등이 Express 기반으로 이미 있다면, Next.js로 갈아타는 비용이 커."
3.2 Next.js를 선택해야 할 때
이준혁: "이제 Next.js를 선택해야 하는 경우. 사실 대부분의 프로젝트는 여기 해당해."
✅ 1. 생산성 우선
a) 빠른 개발/배포 필요
김도연: "MVP를 2주 안에 만들어야 한다면?"
이준혁: "무조건 Next.js야. Express SSR은 배관만 1주 걸려."
b) 배관 작업에 시간 쓰고 싶지 않음
// Express: 라우트마다 이 코드를 반복
app.get('/dashboard', async (req, res) => {
const queryClient = new QueryClient();
await queryClient.prefetchQuery(...);
const dehydrated = dehydrate(queryClient);
const store = createStore();
const html = renderToString(...);
// ... 30줄 이상
});
// Next.js: 파일만 만들면 끝
// app/dashboard/page.tsx
export default async function Dashboard() {
const data = await fetch(...);
return <div>{data}</div>;
}c) MVP/프로토타입 빠르게 만들어야 함
이준혁: "아이디어 검증이 목적이면, 배관보다 기능 개발에 집중해야지."
✅ 2. 팀/프로젝트 특성
a) 중대형 프로젝트 (라우트 10개+)
이준혁: "라우트가 10개 넘어가면 Express SSR의 보일러플레이트가 감당 안 돼."
Express SSR: 10개 라우트 × 30줄 = 300줄 보일러플레이트
Next.js: 10개 파일 생성 = 추가 코드 0줄b) 팀원 간 일관된 패턴 필요
이준혁: "Express SSR은 각자 다르게 구현할 여지가 많아. Next.js는 하나의 방법만 제공해서 일관성이 보장돼."
c) 신규 팀원 온보딩 중요
이준혁: "신입이 들어왔을 때, Express SSR은 배관 코드 설명하는 데 3일 걸려. Next.js는 공식 문서 읽으면 바로 시작 가능해."
✅ 3. 기능 요구사항
a) ISR (Incremental Static Regeneration) 필요
// Next.js만 지원
export const revalidate = 60; // 60초마다 재생성
export default async function Page() {
const data = await fetch(...);
return <div>{data}</div>;
}이준혁: "Express SSR로 ISR 구현하려면 Redis + 캐싱 로직을 직접 짜야 해. Next.js는 한 줄이고."
b) Image 최적화 중요
// Next.js: 자동 최적화
import Image from 'next/image';
<Image src="/hero.jpg" width={800} height={600} />
// Express: 직접 구현 또는 별도 서비스 사용 (Cloudinary 등)c) SEO가 중요한 마케팅 페이지
이준혁: "Next.js는 metadata API, sitemap 생성, robots.txt 등이 다 자동화돼 있어."
// app/page.tsx
export const metadata = {
title: '우리 서비스',
description: 'SEO 최적화된 설명',
openGraph: { ... },
};d) Vercel 배포 활용
이준혁: "Vercel에 배포하면 자동 HTTPS, CDN, 프리뷰 배포가 다 제공돼. Express SSR은 인프라를 직접 구축해야 해."
4. 실제 비교: 동일 앱 구현
4.1 요구사항
김도연: "구체적인 예시를 보고 싶어요."
이준혁: "그럼 동일한 앱을 두 방식으로 구현해보자."
요구사항:
- 인증된 사용자만 접근 가능한 대시보드
- 서버에서 데이터 prefetch (TanStack Query)
- Zustand로 클라이언트 상태 관리
- 프로덕션 배포 가능한 빌드
4.2 Express SSR 구현 (~150줄)
// ============================================
// server.js (~60줄)
// ============================================
import express from 'express';
import session from 'express-session';
import passport from 'passport';
import { renderToString } from 'react-dom/server';
import { QueryClient, dehydrate } from '@tanstack/react-query';
import { createStore } from './store';
const app = express();
// 1. 인증 설정 (~15줄)
app.use(session({ secret: 'secret', resave: false, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());
passport.use(new LocalStrategy(...));
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser((id, done) => User.findById(id, done));
// 2. requireAuth 미들웨어 (~5줄)
function requireAuth(req, res, next) {
if (!req.isAuthenticated()) return res.redirect('/login');
next();
}
// 3. SSR 라우트 (~40줄)
app.get('/dashboard', requireAuth, async (req, res) => {
// TanStack Query prefetch
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 60000 } },
});
await queryClient.prefetchQuery({
queryKey: ['dashboard'],
queryFn: () => fetch(`/api/dashboard?userId=${req.user.id}`).then(r => r.json()),
});
const dehydratedState = dehydrate(queryClient);
// Zustand store 생성 및 초기화
const store = createStore();
store.getState().setUser(req.user);
// React 렌더링
const html = renderToString(
<QueryClientProvider client={queryClient}>
<StoreProvider store={store}>
<Dashboard />
</StoreProvider>
</QueryClientProvider>
);
// HTML 생성
const page = `
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script>
window.__REACT_QUERY_STATE__ = ${JSON.stringify(dehydratedState)};
window.__ZUSTAND_STATE__ = ${JSON.stringify(store.getState())};
</script>
<script src="/client.js"></script>
</body>
</html>
`;
// 정리
queryClient.clear();
res.send(page);
});
app.listen(3000);
// ============================================
// client/entry.jsx (~20줄)
// ============================================
import { hydrateRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider, Hydrate } from '@tanstack/react-query';
import { StoreProvider, createStore } from './store';
const queryClient = new QueryClient();
const store = createStore();
// Zustand 초기화
if (window.__ZUSTAND_STATE__) {
store.setState(window.__ZUSTAND_STATE__);
}
hydrateRoot(
document.getElementById('root'),
<QueryClientProvider client={queryClient}>
<Hydrate state={window.__REACT_QUERY_STATE__}>
<StoreProvider store={store}>
<Dashboard />
</StoreProvider>
</Hydrate>
</QueryClientProvider>
);
// ============================================
// build.js (~60줄)
// ============================================
import esbuild from 'esbuild';
// 1. 클라이언트 빌드
await esbuild.build({
entryPoints: ['client/entry.jsx'],
bundle: true,
outfile: 'dist/client.js',
platform: 'browser',
minify: true,
sourcemap: true,
});
// 2. 서버 빌드
await esbuild.build({
entryPoints: ['server.js'],
bundle: true,
outfile: 'dist/server.js',
platform: 'node',
packages: 'external',
});
// 3. React 빌드 (별도)
await esbuild.build({
entryPoints: ['components/Dashboard.jsx'],
bundle: true,
outfile: 'dist/Dashboard.js',
platform: 'neutral',
format: 'esm',
});
// 4. 서버 전용 빌드 (SSR용)
await esbuild.build({
entryPoints: ['server.js'],
bundle: true,
outfile: 'dist/server-ssr.js',
platform: 'node',
define: { 'process.env.SSR': 'true' },
});
// ============================================
// 총합: ~150줄 (라우트가 늘면 비례 증가)
// ============================================4.3 Next.js 구현 (~40줄)
// ============================================
// middleware.ts (~5줄)
// ============================================
export { default } from 'next-auth/middleware';
export const config = { matcher: ['/dashboard'] };
// ============================================
// app/api/auth/[...nextauth]/route.ts (~10줄)
// ============================================
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export const authOptions = {
providers: [
CredentialsProvider({
credentials: { email: {}, password: {} },
authorize: async (credentials) => {
const user = await User.findOne({ email: credentials.email });
if (user && await bcrypt.compare(credentials.password, user.password)) {
return { id: user.id, email: user.email };
}
return null;
},
}),
],
};
export const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
// ============================================
// app/dashboard/page.tsx (~15줄)
// ============================================
import { getServerSession } from 'next-auth';
import { QueryClient, dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { DashboardClient } from './DashboardClient';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
export default async function DashboardPage() {
const session = await getServerSession(authOptions);
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['dashboard'],
queryFn: () => fetch(`/api/dashboard?userId=${session.user.id}`).then(r => r.json()),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<DashboardClient user={session.user} />
</HydrationBoundary>
);
}
// ============================================
// app/dashboard/DashboardClient.tsx (~15줄)
// ============================================
'use client';
import { useSession } from 'next-auth/react';
import { useQuery } from '@tanstack/react-query';
import { useStore } from '@/store';
export function DashboardClient({ user }) {
const { data: session } = useSession();
const { data } = useQuery({ queryKey: ['dashboard'] });
const store = useStore();
// Zustand 초기화
useEffect(() => {
store.setUser(user);
}, [user]);
return <div>Dashboard: {data.name}</div>;
}
// ============================================
// app/layout.tsx (~5줄)
// ============================================
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
// ============================================
// 빌드/배포: 단일 명령어
// ============================================
// package.json
{
"scripts": {
"build": "next build",
"start": "next start"
}
}
// ============================================
// 총합: ~40줄 (라우트가 늘어도 증가량 미미)
// ============================================4.4 코드량 비교
이준혁: "같은 기능인데 코드량이 거의 4배 차이나."
| 항목 | Express SSR | Next.js |
|---|---|---|
| 인증 설정 | ~20줄 (passport + session) | ~15줄 (NextAuth) |
| SSR 라우트 | ~40줄 (수동 배관) | ~15줄 (자동) |
| 클라이언트 hydration | ~20줄 (수동 주입) | ~15줄 (자동) |
| 빌드 설정 | ~60줄 (4단계 esbuild) | 0줄 (자동) |
| 미들웨어 | ~10줄 (requireAuth) | ~5줄 (middleware.ts) |
| 총합 | ~150줄 | ~40줄 |
김도연: "그리고 라우트가 늘어나면 Express는 40줄씩 증가하지만, Next.js는 파일만 추가하면 되니까 거의 증가하지 않는 거죠?"
이준혁: "정확해. 이게 스케일의 차이야."
5. 학습 여정 회고
5.1 단계별 깨달음
이준혁: "지금까지의 여정을 돌아보자."
Step 1: "아, SSR은 서버에서 HTML을 만드는 거구나"
→ renderToString()의 본질 이해
Step 2: "상태를 클라이언트로 보내려면 직렬화가 필요하구나"
→ window.__STATE__의 목적 이해
Step 3: "dehydrate/hydrate는 캐시 통조림이구나"
→ TanStack Query의 SSR 메커니즘 이해
Step 4: "인증 추가하면 배관이 급격히 복잡해지는구나"
→ session + passport의 복잡성 체감
Step 5: "라우트마다 보일러플레이트를 반복하는 게 진짜 문제구나"
→ 파일시스템 라우팅의 가치 이해
Step 6: "프로덕션에서는 빌드/배포까지 직접 해야 하는구나"
→ esbuild 4단계 파이프라인의 복잡성 체감
Step 7: "Express SSR은 학습용으로 최고, 프로덕션은 Next.js가 합리적"
→ 도구 선택의 기준 확립김도연: "Step 1에서는 '이게 왜 필요해?'라고 생각했는데, Step 6까지 오니까 '그래서 Next.js가 있구나'가 됐어요."
5.2 핵심 개념 완전 이해
이준혁: "너는 이제 이 개념들을 완전히 이해한 거야."
1. renderToString의 본질
// 이게 단순히 문자열 변환이 아니라
const html = renderToString(<App />);
// "리액트 컴포넌트 → HTML 문자열" 변환을 통해
// 1) 초기 렌더링 속도 개선 (브라우저가 HTML을 즉시 표시)
// 2) SEO 최적화 (검색엔진이 내용 읽을 수 있음)
// 3) 자바스크립트 실패 시에도 기본 콘텐츠 표시
// 이 모든 걸 이해하고 있음2. dehydrate/hydrate의 본질
// "통조림" 비유로 완벽히 이해
const dehydrated = dehydrate(queryClient); // 서버에서 통조림 만들기
const serialized = JSON.stringify(dehydrated); // 운송 준비
// window.__REACT_QUERY_STATE__ = ...; // 브라우저로 운송
hydrate(queryClient, window.__REACT_QUERY_STATE__); // 클라이언트에서 개봉3. 요청별 격리의 중요성
// 이 코드가 왜 필요한지 완전히 이해
app.get('/dashboard', async (req, res) => {
const queryClient = new QueryClient(); // 요청마다 새로 생성
const store = createStore(); // 요청마다 새로 생성
// ... SSR ...
queryClient.clear(); // 메모리 누수 방지
});이준혁: "이 개념들을 이해하고 있으면, Next.js를 쓸 때도 '내부에서 뭘 하고 있는지' 알 수 있어. 이게 진짜 실력이지."
6. PM의 최종 정리 (김도연)
김도연: "준혁님, 이제 확실히 이해했어요!
Express SSR로 배관 작업을 직접 해보니 SSR의 핵심 개념들을 완전히 이해했어요:
renderToString이 컴포넌트를 HTML 문자열로 바꾸는 것dehydrate/hydrate가 서버 캐시를 클라이언트로 옮기는 '통조림' 과정window.__STATE__를 통한 상태 직렬화의 필요성- 요청별 store 격리의 중요성 (메모리 누수 방지)
- 왜 라우트마다 보일러플레이트가 반복되는지
- 프로덕션 배포를 위한 빌드 파이프라인의 복잡성
하지만 실제 제품 개발에서는 이 배관 작업이 큰 부담이네요. 라우트 10개만 되도 300줄이 보일러플레이트로 쌓이고, 팀원들도 이 커스텀 배관 코드를 다 이해해야 하니까요.
앞으로의 전략:
- 학습/PoC: Express SSR로 개념 이해 ✅ (이미 완료!)
- 프로덕션 React 앱: Next.js로 빠른 개발
- Multi-framework/특수 요구: Express SSR 활용 (예: A003 프로젝트)
- 기존 Express 서버 확장: Custom Server 패턴 검토
그리고 Next.js를 쓰더라도, 내부에서 어떤 일이 일어나는지 알고 있으니까 디버깅할 때 자신감이 생길 것 같아요!"
7. 시니어의 마지막 조언 (이준혁)
이준혁: "마지막으로 하나만 기억해. 프레임워크는 도구일 뿐이야.
Express SSR을 해본 사람은 Next.js를 쓸 때도 '내부에서 뭘 하고 있는지' 알고 있어. 예를 들어:
// Next.js app/page.tsx
export default async function Page() {
const data = await fetch(...);
return <div>{data}</div>;
}이 코드를 보면, Next.js만 쓴 사람은 '그냥 되네'라고 생각하지만, 너는 이제 알아:
- 서버에서
renderToString(<Page />)이 실행되고 있다 - fetch 결과가
dehydrate되어 클라이언트로 전달된다 - 브라우저에서
hydrate로 이벤트 핸들러가 연결된다 - 이 모든 게 자동화된 거다
반대로 Next.js만 쓴 사람은 문제가 생겼을 때 디버깅하기 어려워. 'HydrationBoundary가 왜 안 돼?'라고 물어보면, 내부 동작을 모르니까 해결 못 하지.
너는 이 튜토리얼을 통해 배관의 실체를 봤으니까, 이제 어떤 도구를 쓰든 자신감을 가져도 돼. 이해 위에 세운 편의성은 단순한 편의성과 다르거든.
실전 조언 3가지
1. 새 프로젝트는 Next.js로 시작해
- 특별한 이유가 없으면 Next.js
- 생산성이 압도적으로 높아
- 팀 협업도 훨씬 쉬워
2. 하지만 내부 동작은 항상 기억해
- Next.js가
renderToString을 언제 호출하는지 dehydrate/hydrate가 어디서 일어나는지- 이걸 알면 디버깅이 10배 빨라져
3. Express SSR은 특수한 경우에만
- Multi-framework 필요할 때 (A003)
- 기존 Express 서버 확장할 때
- 학습 목적으로 SSR 본질 이해할 때
다음 학습 경로
이준혁: "이제 다음 단계로 넘어가자."
단기 (1-2주):
- Next.js App Router 공식 튜토리얼 완료
- Vercel에 실제 배포 경험
- ISR, Image 최적화 등 Next.js 고유 기능 학습
중기 (1-3개월):
- Next.js로 실제 프로젝트 하나 완성
- Server Actions 학습 (폼 처리)
- Parallel Routes, Intercepting Routes 같은 고급 패턴
장기 (3개월+):
- Next.js 내부 소스코드 읽어보기
- Custom Server 패턴 실험
- Remix, Astro 같은 다른 메타프레임워크 비교 학습"
8. 최종 비교 체크리스트
김도연: "마지막으로 의사결정할 때 쓸 체크리스트 만들어주세요!"
이준혁: "좋아, 실전용 체크리스트 줄게."
🎯 Express SSR vs Next.js 의사결정 체크리스트
다음 중 하나라도 해당하면 Express SSR:
- Multi-framework (React + Svelte + Vue 등) 동시 사용
- 기존 Express 서버에 SSR 부분만 추가
- SSR 학습이 주 목적 (교육/연구)
- 라우트가 5개 이하인 소규모 프로젝트
- 빌드 파이프라인을 완전히 커스텀해야 함
- Next.js의 파일시스템 라우팅이 요구사항과 맞지 않음
다음 중 하나라도 해당하면 Next.js:
- 프로덕션 앱 빠르게 개발해야 함
- 라우트가 10개 이상
- SEO 중요 (마케팅 페이지, 블로그 등)
- Image 최적화 필요
- ISR (Incremental Static Regeneration) 필요
- Vercel 배포 활용
- 팀 협업 (일관된 패턴 필요)
- 신규 팀원 온보딩 중요
여전히 고민된다면: → Next.js 선택 (의심스러우면 생산성 우선)
9. 진짜 마지막 정리
Express SSR의 가치
✅ 학습 가치: SSR 본질 완전 이해
✅ 투명성: 모든 배관이 명시적으로 보임
✅ 유연성: 뭐든 커스텀 가능
✅ Multi-framework: React + Svelte + Vue 동시 사용
❌ 생산성: 라우트마다 30줄 보일러플레이트
❌ 유지보수: 커스텀 배관 코드 관리 부담
❌ 협업: 팀원마다 다르게 구현할 여지
❌ 에코시스템: 제한적Next.js의 가치
✅ 생산성: 배관 자동화로 개발 속도 4배
✅ 일관성: 하나의 방법 (파일시스템 라우팅)
✅ 에코시스템: Vercel, plugins, 풍부한 문서
✅ 기능: ISR, Image 최적화, SEO 자동화
❌ 학습: SSR 내부 동작 이해 부족
❌ 불투명성: 일부 추상화로 디버깅 어려움
❌ 규칙: 파일시스템 라우팅 규칙 따라야 함최종 결론
이준혁: "결국 목적에 따라 도구를 선택하는 거야."
목적이 학습 → Express SSR
목적이 프로덕션 → Next.js
목적이 Multi-framework → Express SSR
목적이 MVP → Next.js김도연: "명확해요! Express SSR로 배관을 직접 해보니까 Next.js의 가치를 완전히 이해했고, 이제 프로젝트 특성에 맞게 선택할 수 있을 것 같아요."
이준혁: "그래. 그리고 가장 중요한 건, 너는 이제 선택할 수 있는 사람이 됐다는 거야. Express SSR을 이해하지 못하면 Next.js밖에 못 쓰지만, 둘 다 이해하면 상황에 맞게 선택할 수 있거든.
이게 진짜 시니어의 무기야. 도구에 종속되지 않고, 도구를 선택하고 다룰 수 있는 능력. 이 튜토리얼을 끝까지 완료한 너는 이제 그 능력을 가진 거야."
10. 다음 단계
추천 학습 순서
-
Next.js App Router 공식 튜토리얼 (1주)
- https://nextjs.org/learn
- 파일시스템 라우팅, Server Components 완전 이해
-
실전 프로젝트 구현 (2-4주)
- 블로그 또는 대시보드 앱
- TanStack Query + Zustand 통합
- Vercel 배포
-
고급 패턴 학습 (1-2주)
- Server Actions
- Parallel Routes, Intercepting Routes
- Middleware 고급 활용
-
비교 학습 (선택)
- Remix 학습 (다른 React 메타프레임워크)
- Astro 학습 (정적 사이트 우선)
- SvelteKit 학습 (Svelte 메타프레임워크)
참고 자료
Express SSR:
- React 공식 문서:
renderToStringAPI - TanStack Query 공식 문서: SSR 가이드
- 이 튜토리얼 Step 1-6 복습
Next.js:
- Next.js 공식 문서: https://nextjs.org/docs
- Vercel 블로그: 최신 패턴 및 베스트 프랙티스
- Lee Robinson YouTube: Next.js 실전 영상
비교/심화:
- "Remix vs Next.js" 비교 글
- "When to use Astro vs Next.js"
- "Understanding React Server Components"
마치며
김도연: "준혁님, 정말 감사합니다! Step 1에서 '이게 왜 필요해?'라고 생각했는데, Step 7까지 오니까 SSR의 본질부터 프레임워크 선택 기준까지 완전히 이해했어요.
특히 Express SSR로 배관을 직접 짜보면서 renderToString, dehydrate/hydrate, window.__STATE__ 같은 개념들을 손으로 익힌 게 정말 값진 경험이었어요. 이제 Next.js를 쓸 때도 '내부에서 뭘 하고 있는지' 알고 쓸 수 있을 것 같아요!"
이준혁: "그래, 잘했어. 기억해. 이해 위에 세운 편의성은 단순한 편의성과 다르다. 너는 이제 도구를 선택하고 다룰 수 있는 사람이 됐으니까, 자신감 가져도 돼.
앞으로 어떤 프로젝트를 하든, 이 튜토리얼에서 배운 원리들을 기억하면서 최선의 도구를 선택해. 그게 진짜 시니어의 길이야. 파이팅!"
🎓 Step 1-7 완료! Express SSR vs Next.js 전체 여정 마스터!
✅ Step 1: SSR 본질 이해 (renderToString, hydration)
✅ Step 2: Zustand 직렬화 (Factory 패턴, window.__STATE__)
✅ Step 3: TanStack Query dehydrate/hydrate (통조림 비유)
✅ Step 4: 인증 통합 (passport + session, 복잡성 급증)
✅ Step 5: 라우팅 스케일 (보일러플레이트 누적 문제)
✅ Step 6: 프로덕션 배포 (esbuild 4단계 vs next build)
✅ Step 7: 의사결정 프레임워크 (언제 뭘 선택할지)
→ 이제 어떤 프로젝트든 자신감 있게 도구를 선택할 수 있습니다!