SSR 없이 Vite 멀티 페이지 앱에 프리렌더링을 추가한 방법

발행: (2026년 5월 11일 AM 09:20 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

Cover image for How I Added Pre-Rendering to a Vite Multi-Page App Without SSR

저는 25개 언어에 걸쳐 50개 이상의 도구를 제공하는 온라인 파일 변환 사이트인 RelahConvert를 운영하고 있습니다. 지난 주 Ahrefs 감사에서 문제를 발견했는데, 1,449개 페이지 모두가 “H1 missing”“Low word count.” 로 표시되었습니다.

페이지가 비어 있지는 않았습니다. H1, 설명, FAQ가 있었고—방문하면 모두 정상적으로 렌더링되었습니다. 하지만 크롤러는 빈 본문을 보았습니다.

설정

사이트는 SPA가 아니라 Vite MPA(다중 페이지 앱)입니다. 각 도구는 자체 .html 파일을 가지고 있습니다. 도구별 JavaScript가 런타임에 다음과 같이 페이지 콘텐츠를 삽입합니다:

document.querySelector('#app').innerHTML = template;

이 방식은 사용자에게는 잘 작동합니다: 페이지가 로드되고, JS가 실행되어 콘텐츠가 표시됩니다. 하지만 JavaScript를 실행하지 않는 크롤러(Ahrefs, 기본 봇, Facebook 및 X와 같은 소셜 미디어 스크래퍼)는 빈 본문을 보게 됩니다. JS를 실행하는 Google조차도 두 번째 렌더링이 지연되어 크롤링 예산을 소모하고 새로운 페이지의 인덱싱을 늦춥니다.

일반적인 해결책은 맞지 않았다

SolutionWhy it didn’t work
Server‑Side Rendering (SSR)런타임 백엔드가 필요함; 나는 Cloudflare Pages(정적 호스팅)를 사용 중.
Static Site Generation (SSG)Astro나 Vike 같은 프레임워크를 중심으로 전체 구조를 재구성해야 함—이미 배포 중인 사이트에 큰 리팩터가 필요.
Pre‑rendering with Puppeteer모든 라우트마다 헤드리스 브라우저를 띄우면 빌드 시간이 ~3‑5분 늘어나고 무거운 의존성이 추가됨.

나는 더 가벼운 방법을 원했다: UI를 Node로 전체 재구현하지 않고, 빌드 시 정적 HTML에 SEO 관련 콘텐츠(H1, description, FAQs)만 주입하고 싶었다.

접근 방식: Vite 플러그인을 활용한 하이브리드 프리렌더링

기존 빌드 플러그인을 확장하여 i18n 사전을 읽고 SEO 콘텐츠를 각 HTML 파일에 직접 기록했습니다. 인터랙티브 툴 UI(드래그‑드롭, 캔버스 작업, 변환 로직)는 JS로 렌더링된 상태를 유지하고, 크롤러에게 중요한 콘텐츠만 미리 삽입됩니다.

플러그인의 대략적인 구조

function preRenderPlugin() {
  return {
    name: 'pre-render-seo',
    apply: 'build',
    closeBundle() {
      const tools = ['compress', 'resize', 'merge-pdf', /* … */];
      const langs = ['en', 'fr', 'es', 'de', 'ar', /* … */];

      for (const tool of tools) {
        for (const lang of langs) {
          const i18n = loadI18n(lang);
          const seo = i18n.seo[tool];

          const html = readHTML(`dist/${lang}/${slugFor(tool, lang)}/index.html`);
          const injected = injectSEOContent(html, {
            h1: i18n.nav_short[tool],
            description: i18n.heroDesc,
            body: seo.body,
            faqs: seo.faqs,
          });

          // The content gets injected into the placeholder.
          // When JS runs at runtime, it replaces the contents with the interactive UI —
          // but the pre‑rendered version is what crawlers see.
          writeHTML(path, injected);
        }
      }
    },
  };
}

세 가지 함정

  1. 언어별 메타데이터 – 내 도구 페이지는 프랑스어 URL에서도 하드코딩된 영어 <title><meta> 태그를 사용하고 있었다. 사전 렌더링을 통해 불일치가 드러났고, 빌드 시점에 i18n에서 언어별 제목과 메타 설명을 가져오도록 플러그인을 확장했다.

  2. 언어별 URL에서의 FOUC – 처음에는 홈페이지 템플릿에서 언어별 URL을 파생시켰다. 런타임에 JS가 라우트를 감지하고 본문을 비운 뒤 도구를 삽입했는데, 이 과정에서 프랑스어 콘텐츠가 잠깐 깜빡였다가 사라졌다. 해결책은 각 도구의 자체 템플릿을 해당 언어 URL의 기본으로 사용하는 것이었다.

  3. Cloudflare 봇 보호가 소셜 스크래퍼 차단 – OG 태그를 수정한 뒤 Facebook Sharing Debugger가 403을 반환했다. Cloudflare의 Browser Integrity Check가 Facebook 스크래퍼를 차단하고 있었다. 해결 방법은 코드 변경이 아니라 Cloudflare 대시보드에서 간단히 설정을 조정하는 것이었다.

Result

  • 빌드 시간은 22.7 s에서 ~23 s로 증가했으며, 무시할 수 있는 오버헤드입니다.
  • 이제 모든 도구 페이지는 25개 언어에 걸쳐 정적 HTML에 적절한 H1, 설명, FAQ 및 내부 링크를 포함합니다.
  • 소셜 미리보기가 작동합니다.
  • Ahrefs와 Google이 첫 번째 크롤링 시 콘텐츠를 읽을 수 있습니다.

실제 결과는 image compressor에서 확인할 수 있습니다 – 페이지의 view‑source를 보면 사전 렌더링된 콘텐츠가 표시됩니다.

요약

프레임워크 없이 Vite를 사용하고 전체 정적 사이트 생성(SSG) 없이 크롤러 친화적인 HTML이 필요하다면, 빌드 시에 기존 구조에 SEO 콘텐츠를 삽입하는 플러그인은 의외로 깔끔한 중간 해결책입니다.

0 조회
Back to Blog

관련 글

더 보기 »