
React + Vite 프로젝트에 SSG(Static Site Generation) 적용하기
왜 SSG가 필요한가?
일반적인 React SPA(Single Page Application)는 빌드 시 빈 HTML 껍데기만 생성됩니다.
<div id="root"></div> <script src="/assets/app.js"></script>
JavaScript가 실행되어야 컨텐츠가 렌더링되기 때문에:
- 검색 엔진 크롤러가 컨텐츠를 제대로 인식하지 못할 수 있음
- 초기 로딩 시 빈 화면이 잠시 보임 (First Contentful Paint 지연)
SSG를 적용하면 빌드 시점에 각 페이지의 HTML을 미리 렌더링하여 이 문제를 해결할 수 있습니다.
vite-react-ssg 설치
yarn add vite-react-ssg # 또는 npm install vite-react-ssg
프로젝트 구조 변경
1. routes.tsx 생성
기존 App.tsx에 있던 라우트 정의를 별도 파일로 분리합니다.
// src/routes.tsx import type { RouteRecord } from "vite-react-ssg"; import Layout from "./Layout"; import Index from "./pages/Index"; import About from "./pages/About"; import Blog from "./pages/Blog"; import BlogPost from "./pages/BlogPost"; import NotFound from "./pages/NotFound"; import posts from "./data/posts.json"; // 블로그 포스트 데이터 export const routes: RouteRecord[] = [ { path: "/", element: <Layout />, children: [ { index: true, element: <Index />, }, { path: "about", element: <About />, }, { path: "blog", element: <Blog />, }, { path: "blog/:slug", element: <BlogPost />, // 동적 라우트의 경우 빌드할 경로 목록을 반환 getStaticPaths: () => posts.map((post) => `/blog/${post.slug}`), }, { path: "*", element: <NotFound />, }, ], }, ];
핵심 포인트:
getStaticPaths는 동적 라우트(:slug같은 파라미터가 있는 경로)에서 어떤 페이지들을 미리 생성할지 알려줍니다.
2. Layout.tsx 생성
공통 프로바이더와 레이아웃을 담당하는 컴포넌트를 생성합니다.
// src/Layout.tsx import { Outlet } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient(); const Layout = () => { return ( <QueryClientProvider client={queryClient}> {/* 기타 프로바이더들 */} <Outlet /> </QueryClientProvider> ); }; export default Layout;
3. main.tsx 수정
기존의 createRoot 방식에서 ViteReactSSG로 변경합니다.
// src/main.tsx (변경 전) import { createRoot } from "react-dom/client"; import App from "./App"; import "./index.css"; createRoot(document.getElementById("root")!).render(<App />);
// src/main.tsx (변경 후) import { ViteReactSSG } from "vite-react-ssg"; import { routes } from "./routes"; import "./index.css"; export const createRoot = ViteReactSSG( { routes }, ({ isClient }) => { // 클라이언트 사이드 초기화 로직 (필요시) } );
4. vite.config.ts 수정
SSG 옵션을 추가합니다.
// vite.config.ts import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; export default defineConfig({ plugins: [react()], ssgOptions: { script: "async", formatting: "minify", }, });
5. package.json 빌드 스크립트 수정
{ "scripts": { "build": "vite-react-ssg build" } }
주의사항: 데이터 로딩 방식
SSG에서 데이터를 사전 렌더링하려면 빌드 시점에 데이터가 동기적으로 사용 가능해야 합니다.
❌ 잘못된 방식 (useQuery 사용)
const Blog = () => { const { data: posts, isLoading } = useQuery({ queryKey: ["posts"], queryFn: fetchPosts, }); if (isLoading) return <div>로딩 중...</div>; // ... };
이 방식은 빌드 시 "로딩 중..."이 HTML에 렌더링됩니다.
✅ 올바른 방식 (정적 import)
import posts from "@/data/posts.json"; const Blog = () => { return ( <div> {posts.map((post) => ( <PostCard key={post.id} post={post} /> ))} </div> ); };
JSON 파일을 직접 import하면 빌드 시점에 데이터가 포함됩니다.
빌드 결과 확인
yarn build
빌드가 완료되면 다음과 같은 출력을 확인할 수 있습니다:
[vite-react-ssg] Rendering Pages... (4) dist/index.html 5.84 KiB dist/about.html 4.72 KiB dist/blog.html 4.91 KiB dist/blog/my-first-post.html 28.80 KiB
각 페이지가 개별 HTML 파일로 생성되며, 실제 컨텐츠가 포함되어 있습니다.
생성된 HTML 구조
<!DOCTYPE html> <html lang="ko"> <head> <script type="module" async src="/assets/app.js"></script> <link rel="stylesheet" href="/assets/app.css"> </head> <body> <!-- 사전 렌더링된 컨텐츠 --> <div id="root" data-server-rendered="true"> <nav>...</nav> <main> <h1>Blog</h1> <article>실제 포스트 내용...</article> </main> </div> <!-- Hydration을 위한 데이터 --> <script>window.__staticRouterHydrationData = ...</script> </body> </html>
SSG vs SSR vs SPA 비교
| 특성 | SPA | SSG | SSR |
|---|---|---|---|
| 빌드 시 HTML 생성 | ❌ | ✅ | ❌ |
| 서버 필요 | ❌ | ❌ | ✅ |
| SEO | 제한적 | 우수 | 우수 |
| 정적 호스팅 가능 | ✅ | ✅ | ❌ |
| 실시간 데이터 | ✅ | ❌ (빌드 시점 데이터) | ✅ |
GitHub Pages 같은 정적 호스팅 환경에서는 SSG가 SEO와 성능을 모두 잡을 수 있는 최적의 선택입니다.
트러블슈팅
react-syntax-highlighter ESM 오류
SSR/SSG 환경에서 react-syntax-highlighter를 사용할 때 모듈 해석 오류가 발생할 수 있습니다.
// ❌ ESM 경로 (SSG에서 오류 발생 가능) import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; // ✅ CJS 경로 (SSG 호환) import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism";
마무리
vite-react-ssg를 사용하면 기존 React + Vite 프로젝트에 최소한의 변경으로 SSG를 적용할 수 있습니다. 검색 엔진 최적화가 필요하면서도 서버 없이 정적 호스팅을 사용해야 하는 경우에 좋은 선택입니다.
동작 확인
Preview 서버로 확인
yarn preview # http://localhost:4173 에서 확인
curl로 HTML 응답을 확인하면 JavaScript 실행 없이도 컨텐츠가 포함되어 있습니다:
curl -s http://localhost:4173/blog | grep "data-server-rendered" # <div id="root" data-server-rendered="true">...
Hydration 동작 원리
- 초기 로드: 서버에서 사전 렌더링된 HTML이 즉시 표시됨
- JS 로드:
app.js번들이 비동기로 로드됨 - Hydration: React가 기존 HTML에 이벤트 리스너를 연결
- SPA 전환: 이후 페이지 이동은 클라이언트 사이드 라우팅으로 처리
data-server-rendered="true" 속성이 있으면 SSG가 정상 적용된 것입니다.