순수 SPA(React/Vite)의 한계와 초기 빈 HTML 문제 분석
클라이언트 사이드 렌더링(CSR)이 크롤링에 미치는 악영향과 자바스크립트 실행 지연(Render Queue)으로 인한 SEO 페널티 원리를 설명합니다.
SPA란 무엇이고, 왜 SEO 문제가 발생하는가
SPA(Single Page Application)란 전체 페이지를 서버에서 매번 새로 받는 전통적인 MPA(Multi Page Application)와 달리, 최초 1회만 HTML 셸을 다운로드하고 이후 모든 페이지 전환을 JavaScript로 처리하는 웹 애플리케이션입니다. React, Vue, Angular 같은 프레임워크와 Vite 같은 번들러로 만들어집니다.
SPA의 동작 원리
- 브라우저가 서버에
index.html을 요청 - 서버가 거의 빈 HTML(div#root + JS 번들 링크)을 응답
- 브라우저가 JS 번들(수백 KB~수 MB)을 다운로드
- JS가 실행되면서 클라이언트 측에서 DOM을 구성 (콘텐츠 렌더링)
- 이후 페이지 전환은 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 등 모든 크롤러 호환
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 Router | example.com/#/about | ❌ 심각 — # 이후는 서버에 전송되지 않음. 모든 URL이 같은 페이지로 인식 |
| Browser Router | example.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 + Vite | CSR (순수 클라이언트) | ❌ 없음 | vite-plugin-ssr / vite-ssg 플러그인 → 설정 복잡 | 🔴 나쁨 |
| Vue + Vite | CSR (순수 클라이언트) | ❌ 없음 | Nuxt.js (SSR/SSG 프레임워크)로 전환 권장 | 🔴 나쁨 |
| Angular | CSR (순수 클라이언트) | ❌ 없음 | Angular Universal (SSR) → 설정 복잡 | 🔴 나쁨 |
| Next.js | SSR/SSG/ISR 선택 가능 | ✅ 내장 (Metadata API) | 기본 제공 (App Router) | 🟢 우수 |
| Nuxt.js | SSR/SSG 선택 가능 | ✅ 내장 | 기본 제공 | 🟢 우수 |
| Astro | SSG (기본), SSR 가능 | ✅ 내장 | 기본 제공 | 🟢 매우 우수 |
| Remix | SSR (기본) | ✅ 내장 | 기본 제공 | 🟢 우수 |
| Gatsby | SSG (빌드 타임) | ✅ 내장 | 빌드 타임 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에 문제가 있는지 확인하기
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도 문제없다"는 말이 맞나요?
Q. react-helmet으로 메타 태그를 동적으로 설정하면 SEO가 해결되나요?
react-helmet은 클라이언트 측에서 JS가 실행된 후에 메타 태그를 삽입합니다. Google WRS가 렌더링한 후에는 인식할 수 있지만: (1) 네이버·소셜 미디어 크롤러는 JS를 실행하지 않으므로 메타 태그가 없는 것으로 인식합니다. (2) 카카오톡·LINE·Slack 등에서 링크 공유 시 빈 미리보기가 표시됩니다. (3) Google도 1단계 크롤에서는 초기 HTML의 메타 태그를 수집하므로 모든 페이지가 동일한 제목으로 인식됩니다. 서버 측에서 메타 태그를 설정해야만 모든 크롤러에서 정상 인식됩니다.