글쓰기 프리뷰
    React + Vite 프로젝트에 SSG(Static Site Generation) 적용하기

    React + Vite 프로젝트에 SSG(Static Site Generation) 적용하기

    (수정: 2025년 12월 23일 오후 04:03)

    왜 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 비교

    특성SPASSGSSR
    빌드 시 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 동작 원리

    1. 초기 로드: 서버에서 사전 렌더링된 HTML이 즉시 표시됨
    2. JS 로드: app.js 번들이 비동기로 로드됨
    3. Hydration: React가 기존 HTML에 이벤트 리스너를 연결
    4. SPA 전환: 이후 페이지 이동은 클라이언트 사이드 라우팅으로 처리

    data-server-rendered="true" 속성이 있으면 SSG가 정상 적용된 것입니다.

    Dunde's Portfolio

    © 2026 Dunde. All rights reserved.

    Built with React, TypeScript, and Vite. Deployed on GitHub Pages.