중급GoogleNaver

순수 SPA(React/Vite)의 한계와 초기 빈 HTML 문제 분석

핵심 요약 (TL;DR)

클라이언트 사이드 렌더링(CSR)이 크롤링에 미치는 악영향과 자바스크립트 실행 지연(Render Queue)으로 인한 SEO 페널티 원리를 설명합니다.

읽기 14 2025-03-23

SPA란 무엇이고, 왜 SEO 문제가 발생하는가

SPA(Single Page Application)란 전체 페이지를 서버에서 매번 새로 받는 전통적인 MPA(Multi Page Application)와 달리, 최초 1회만 HTML 셸을 다운로드하고 이후 모든 페이지 전환을 JavaScript로 처리하는 웹 애플리케이션입니다. React, Vue, Angular 같은 프레임워크와 Vite 같은 번들러로 만들어집니다.

SPA의 동작 원리

  1. 브라우저가 서버에 index.html을 요청
  2. 서버가 거의 빈 HTML(div#root + JS 번들 링크)을 응답
  3. 브라우저가 JS 번들(수백 KB~수 MB)을 다운로드
  4. JS가 실행되면서 클라이언트 측에서 DOM을 구성 (콘텐츠 렌더링)
  5. 이후 페이지 전환은 History API로 URL만 변경하고 JS가 새 콘텐츠 렌더링

문제는 2번입니다. 검색엔진 크롤러가 받는 것도 이 "빈 HTML"입니다. JS를 실행하지 않으면 콘텐츠가 전혀 보이지 않습니다.

빈 HTML 셸의 실제 모습

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>My App</title>  <!-- 모든 페이지에서 동일한 제목 -->
  <!-- meta description 없음 -->
  <!-- OG 태그 없음 -->
</head>
<body>
  <div id="root"></div>  <!-- 콘텐츠 없음! -->
  <script type="module" src="/assets/index-3a8f2c.js"></script>
</body>
</html>

위 HTML이 크롤러에게 전달되면: 제목은 "My App"이 모든 페이지에서 동일하고, 설명(description)도 없고, 본문 콘텐츠도 전혀 없습니다. SEO 관점에서 이 페이지는 "빈 페이지"입니다.

MPA (전통 서버 렌더링)

  • 서버가 완성된 HTML 응답
  • 크롤러가 즉시 콘텐츠 확인
  • 페이지별 고유 Title/Meta
  • JS 실행 불필요
  • 네이버·Bing 등 모든 크롤러 호환
vs

SPA (클라이언트 렌더링)

  • 서버가 빈 HTML 셸 응답
  • 크롤러가 JS 실행해야 콘텐츠 확인
  • 기본 Title이 모든 경로에서 동일
  • JS 실행 필수
  • Google만 JS 렌더링 지원 (제한적)

Googlebot의 2단계 인덱싱: 렌더링 큐의 실체

Google은 세계에서 유일하게 JavaScript를 실행하여 SPA를 렌더링할 수 있는 검색엔진입니다. 그러나 이 과정은 즉각적이지 않습니다. Google의 인덱싱은 2단계(Two-Wave)로 진행됩니다.

2단계 인덱싱 프로세스

단계이름동작소요 시간
1단계HTML 크롤링Googlebot이 URL을 요청하고 서버가 보내는 원시 HTML을 파싱. 이 단계에서 발견한 콘텐츠·링크를 즉시 인덱싱수초~수분
2단계JS 렌더링 (WRS)Web Rendering Service(WRS)가 Headless Chromium으로 JS를 실행하고 최종 DOM을 추출. JS로만 생성되는 콘텐츠가 이때 인덱싱됨수초~수일 (최대 수주)

렌더링 큐(Render Queue)의 문제

WRS는 무한한 리소스를 가지고 있지 않습니다. 전 세계 수십억 개의 웹페이지를 렌더링해야 하므로 큐가 존재합니다.

  • 큐 대기 시간: 페이지의 중요도(인기도, 크롤 빈도)에 따라 렌더링 우선순위가 결정. 신규·비인기 페이지는 렌더링이 수일~수주 지연될 수 있음
  • 리소스 한계: JS를 실행하는 것은 HTML만 파싱하는 것보다 5~10배 더 많은 리소스를 소비. 대규모 사이트는 크롤 버짓 압박
  • 렌더링 실패: JS 에러, 타임아웃(Google WRS 타임아웃: 약 5초), 외부 API 의존 → 렌더링 실패 → 콘텐츠 누락

실제 영향: 인덱싱 지연 시나리오

사이트 유형MPA (서버 렌더링)SPA (CSR)
신규 블로그 글 인덱싱수분~수시간수시간~수일 (렌더링 큐 대기)
새 제품 페이지수시간 내 검색 가능수일 후 검색 가능 (프로모션 기간 놓칠 수 있음)
시사/뉴스 콘텐츠실시간 인덱싱 가능뉴스 가치가 사라진 후에야 인덱싱 (치명적)
대규모 이커머스 (10만+ 페이지)크롤 버짓 내에서 정상 처리크롤 버짓 고갈, 절반 이상 미인덱싱

SPA가 SEO에 미치는 7가지 구체적 문제

① 메타데이터 부재

SPA에서 react-helmet이나 Vue Meta로 동적으로 설정하는 <title>, <meta description>, OG 태그는 JS 실행 전에는 초기 HTML에 존재하지 않습니다.

  • 소셜 미디어 크롤러(Facebook, Twitter, LINE)는 JS를 실행하지 않음 → 링크 공유 시 제목·설명·이미지 없음
  • 네이버 크롤러도 JS 렌더링 미지원 → 네이버 검색에서 사실상 인식 불가
  • Google도 1단계에서는 빈 메타데이터를 수집하므로 초기 인덱싱이 부정확

② 내부 링크 발견 실패

SPA의 라우터(react-router, vue-router)는 URL을 변경하지만, 그 링크가 표준 <a href> 태그로 존재하지 않는 경우가 많습니다.

<!-- ❌ 크롤러가 발견하지 못하는 링크 -->
<div onClick={() => navigate('/about')}>About</div>

<!-- ✅ 크롤러가 발견하는 링크 -->
<a href="/about">About</a>
<Link to="/about">About</Link>  <!-- React Router의 Link 컴포넌트 -->

onClick 핸들러로만 네비게이션하면 Googlebot이 해당 페이지의 존재를 전혀 발견하지 못합니다.

③ URL 구조 문제: 해시 라우팅

라우팅 방식URL 예시SEO 영향
Hash Routerexample.com/#/about❌ 심각 — # 이후는 서버에 전송되지 않음. 모든 URL이 같은 페이지로 인식
Browser Routerexample.com/about⚠️ 부분적 — 깨끗한 URL이지만 서버 설정(Fallback) 필요. 없으면 404

규칙: SEO를 조금이라도 고려한다면 Hash Router는 절대 사용하면 안 됩니다. Browser Router + 서버 Fallback이 최소 요구사항입니다.

④ Core Web Vitals 악화

지표SPA 영향원인
LCP (Largest Contentful Paint)❌ 심각한 악화JS 번들 다운로드 + 파싱 + 실행 후에야 LCP 요소 렌더링. 서버 렌더링 대비 2~5초 지연
CLS (Cumulative Layout Shift)⚠️ 가능성 높음JS로 동적으로 주입되는 요소가 레이아웃 시프트 유발. 특히 이미지, 광고, 폰트
INP (Interaction to Next Paint)⚠️ 가능성 높음대규모 JS 번들이 메인 스레드를 차단하면 사용자 인터랙션 응답이 느려짐
FCP (First Contentful Paint)❌ 심각한 악화빈 HTML → JS 다운로드 → 실행 → 첫 콘텐츠 표시. 이 전체가 FCP까지의 시간

⑤ 구조화 데이터 누락

JSON-LD 구조화 데이터가 JS로 동적 삽입되면 1단계 크롤에서 나타나지 않습니다. Google은 렌더링 후 발견할 수 있지만, 즉시 Rich Result에 반영되지 않을 수 있습니다.

⑥ Google 이외 크롤러 무력화

크롤러/플랫폼JS 렌더링 지원SPA 대응
Googlebot (WRS)✅ 지원 (Headless Chromium)인덱싱 가능하지만 지연 있음
Bingbot⚠️ 제한적 지원일부 JS 렌더링 가능하나 신뢰도 낮음
네이버 Yeti❌ 미지원SPA 콘텐츠 사실상 인식 불가
Facebook/Twitter 크롤러❌ 미지원OG 태그 없이 공유 → 빈 미리보기
ChatGPT / Perplexity 크롤러❌ 미지원AI 검색에서 콘텐츠 인용 불가
Slack/Discord 프리뷰❌ 미지원링크 공유 시 제목·설명 없음

⑦ 사이트맵의 무력화

사이트맵에 URL을 등록해도, Googlebot이 해당 URL을 크롤했을 때 빈 HTML을 받으면 "콘텐츠 없음"으로 판단하여 인덱싱을 건너뛸 수 있습니다. GSC에서 "크롤됨 - 현재 인덱싱되지 않음(Crawled - Currently Not Indexed)" 상태가 대량으로 나타나는 것이 SPA 사이트의 전형적 증상입니다.

프레임워크·번들러별 SEO 한계 비교

기술기본 렌더링SEO 기본 지원서버 렌더링 옵션SEO 등급
React (create-react-app)CSR (순수 클라이언트)❌ 없음직접 구현 필요 (Express + ReactDOMServer) → 비현실적🔴 나쁨
React + ViteCSR (순수 클라이언트)❌ 없음vite-plugin-ssr / vite-ssg 플러그인 → 설정 복잡🔴 나쁨
Vue + ViteCSR (순수 클라이언트)❌ 없음Nuxt.js (SSR/SSG 프레임워크)로 전환 권장🔴 나쁨
AngularCSR (순수 클라이언트)❌ 없음Angular Universal (SSR) → 설정 복잡🔴 나쁨
Next.jsSSR/SSG/ISR 선택 가능✅ 내장 (Metadata API)기본 제공 (App Router)🟢 우수
Nuxt.jsSSR/SSG 선택 가능✅ 내장기본 제공🟢 우수
AstroSSG (기본), SSR 가능✅ 내장기본 제공🟢 매우 우수
RemixSSR (기본)✅ 내장기본 제공🟢 우수
GatsbySSG (빌드 타임)✅ 내장빌드 타임 SSG🟢 우수 (빌드 시간 주의)

핵심 결론

순수 SPA 프레임워크(React+Vite, Vue+Vite, Angular 등)는 SEO를 전혀 고려하지 않은 아키텍처입니다. SEO가 필요한 프로젝트라면:

  • 새 프로젝트: 처음부터 Next.js, Nuxt.js, Astro 같은 SSR/SSG 프레임워크를 선택하세요
  • 기존 SPA 프로젝트: (1) SSR 프레임워크로 마이그레이션, (2) 불가능하면 Dynamic Rendering(Pre-rendering) 적용, (3) 최소한 크리티컬 페이지만이라도 서버 렌더링
🔴 나쁨순수 SPA SEO 등급React+Vite, Vue+Vite, Angular 기본
🟢 우수SSR 프레임워크 SEO 등급Next.js, Nuxt.js, Astro, Remix
5~10배JS 렌더링 소요 리소스HTML 파싱 대비 크롤러 리소스 소비

진단 방법: 내 SPA가 SEO에 문제가 있는지 확인하기

5가지 진단 도구

도구확인 방법문제 징후
1. 브라우저 "소스 보기"Ctrl+U로 원시 HTML 소스 확인<div id="root"></div>만 보이고 콘텐츠가 없으면 ❌
2. GSC URL 검사 도구URL 입력 → "실시간 검사" → HTML 탭 확인렌더링된 HTML에만 콘텐츠가 있고 원시 HTML이 비어 있으면 ❌
3. site: 검색Google에 site:example.com 검색인덱싱된 페이지 수가 실제 페이지 수보다 현저히 적으면 ❌
4. JavaScript 비활성화Chrome DevTools → Settings → Disable JavaScript → 사이트 방문빈 화면 또는 "JS를 활성화하세요" 메시지만 보이면 ❌
5. Screaming Frog크롤 설정에서 "JavaScript 렌더링 OFF"로 크롤대부분의 페이지 Title이 동일하거나 비어 있으면 ❌

GSC에서 나타나는 SPA 문제 패턴

  • "크롤됨 - 현재 인덱싱되지 않음"이 대량으로 나타남 → 빈 HTML 때문에 Google이 인덱싱 가치가 없다고 판단
  • "발견됨 - 현재 인덱싱되지 않음"이 급증 → 사이트맵에는 있지만 아직 크롤도 안 된 URL (크롤 버짓 부족)
  • 모든 페이지의 제목이 동일하게 표시 → 서버가 동일한 <title>을 반환
  • 특정 페이지만 인덱싱 → 외부 백링크가 있는 페이지만 인덱싱되고 내부 링크만 있는 페이지는 미인덱싱

해결 방향: SPA에서 SEO 친화적 아키텍처로

4가지 해결 방향 비교

방향설명난이도SEO 효과적합한 경우
1. SSR 프레임워크로 마이그레이션Next.js, Nuxt.js 등으로 프로젝트 전환🔴 높음 (코드 전면 수정)★★★★★새 프로젝트 또는 대규모 리팩토링이 가능한 경우
2. Dynamic Rendering (Pre-rendering)Prerender.io 등으로 크롤러에게만 사전 렌더링된 HTML 제공🟡 중간 (서버 설정 변경)★★★★☆기존 SPA를 즉시 변경할 수 없는 경우
3. 하이브리드 (SEO 페이지만 SSR)검색 트래픽이 필요한 랜딩 페이지만 SSR, 나머지는 CSR 유지🟡 중간★★★★☆SEO가 필요한 페이지가 제한적인 경우 (SaaS 등)
4. PWA + App Shell 패턴앱 셸은 SSR로, 동적 콘텐츠는 CSR로 분리🟡 중간★★★☆☆PWA 앱이 주요 서비스이고 웹 검색 의존도가 낮은 경우

SaaS·대시보드 등 SEO가 불필요한 경우

모든 SPA가 SEO 문제인 것은 아닙니다. 로그인 후에만 접근하는 서비스(대시보드, 관리자 패널, 내부 도구)는 검색엔진에 인덱싱될 필요가 없으므로 순수 SPA가 적합합니다.

  • ✅ SPA 적합: 관리자 대시보드, 내부 CRM, SaaS 앱 내부 화면
  • ❌ SPA 부적합: 공개 블로그, 이커머스, 랜딩 페이지, 마케팅 사이트
  • 🟡 하이브리드: SaaS의 마케팅 페이지(SSR) + 앱 내부(CSR)

자주 묻는 질문 (FAQ)

Q. "Google이 JS를 실행할 수 있으니 SPA도 문제없다"는 말이 맞나요?
부분적으로만 맞습니다. Google WRS가 JS를 실행할 수 있는 것은 사실이지만, 3가지 한계가 있습니다: (1) 즉시 인덱싱이 아닙니다 — 렌더링 큐 대기 시간이 수초~수주까지 걸릴 수 있어 신규 콘텐츠의 적시 인덱싱이 보장되지 않습니다. (2) Google만 가능합니다 — 네이버, Bing, 소셜 미디어, AI 크롤러는 JS를 렌더링하지 않으므로 이들 플랫폼에서는 완전히 보이지 않습니다. (3) 모든 JS가 정상 실행된다는 보장이 없습니다 — WRS 타임아웃, 외부 API 의존, JS 에러 등으로 렌더링이 실패하면 빈 페이지로 인덱싱됩니다.
Q. react-helmet으로 메타 태그를 동적으로 설정하면 SEO가 해결되나요?
아닙니다. react-helmet클라이언트 측에서 JS가 실행된 후에 메타 태그를 삽입합니다. Google WRS가 렌더링한 후에는 인식할 수 있지만: (1) 네이버·소셜 미디어 크롤러는 JS를 실행하지 않으므로 메타 태그가 없는 것으로 인식합니다. (2) 카카오톡·LINE·Slack 등에서 링크 공유 시 빈 미리보기가 표시됩니다. (3) Google도 1단계 크롤에서는 초기 HTML의 메타 태그를 수집하므로 모든 페이지가 동일한 제목으로 인식됩니다. 서버 측에서 메타 태그를 설정해야만 모든 크롤러에서 정상 인식됩니다.
Q. 이미 SPA로 만들어진 사이트를 Next.js로 바꾸는 것이 현실적인가요?
규모에 따라 다릅니다. 소규모 사이트(10페이지 미만): 현실적입니다. React 컴포넌트를 거의 그대로 재사용할 수 있고, 라우팅과 데이터 페칭만 Next.js 패턴으로 변경하면 됩니다. 중규모 사이트(10~100페이지): 가능하지만 1~3개월의 작업 기간이 필요합니다. 대규모 사이트(100+ 페이지): 전면 마이그레이션보다 Dynamic Rendering(Prerender.io)을 먼저 적용하여 즉시 SEO 효과를 보고, 점진적으로 마이그레이션하는 것이 현실적입니다.
Q. Vite 공식 문서에 SSR 가이드가 있던데, Vite로 SSR을 직접 구현하면 안 되나요?
기술적으로는 가능하지만 비권장합니다. Vite SSR은 프레임워크가 아니라 빌드 도구의 SSR 지원입니다. 직접 구현해야 할 것이 방대합니다: 서버 엔트리 포인트, 스트리밍 SSR 설정, 라우팅 처리, 데이터 수화(Hydration), 에러 핸들링, 메타데이터 관리 등. 이 모든 것을 이미 해결한 Next.js나 Nuxt.js를 사용하는 것이 훨씬 효율적입니다. Vite SSR을 직접 구현하면 유지보수 부담이 극도로 높아지며, 커뮤니티·문서화도 프레임워크 대비 부족합니다.
Q. SPA에서 사이트맵을 제출하면 크롤링이 개선되나요?
사이트맵은 Google에게 "이 URL들이 존재한다"고 알려주는 역할을 합니다. SPA에서도 사이트맵을 제출하면 URL 발견(Discovery)에는 도움이 됩니다. 그러나 Googlebot이 해당 URL을 크롤했을 때 빈 HTML을 받으면, "크롤됨 - 현재 인덱싱되지 않음" 상태에 빠집니다. 사이트맵은 크롤링을 보장하지만 인덱싱을 보장하지 않습니다. 빈 HTML 문제가 해결되지 않으면 사이트맵만으로는 불충분합니다. 사이트맵 + SSR/프리렌더링을 함께 적용해야 효과가 있습니다.

지금 읽으신 SEO 지식, 바로 적용해보세요!

검색엔진 최적화는 실전입니다. SEO SOVISS의 무료 분석 도구로 내 웹사이트의 오디트 점수를 즉시 확인하고 기술적 문제점을 점검해보세요.

내 웹사이트 진단하기 →
정수아

데이터분석팀 선임

정수아

GA4, Search Console 및 서버 로그 데이터를 기반으로 사용자 행동을 분석하고 트래픽 갭(Traffic Gap)을 도출합니다.

SEO SOVISS 전체 집필진 보기 →
순수 SPA(React/Vite)의 한계와 초기 빈 HTML 문제 분석