고급Google
레거시 SPA를 위한 Dynamic Rendering (Prerender.io) 우회 기법
핵심 요약 (TL;DR)
구조를 즉시 개편할 수 없는 레거시 React/Vite 환경에서, 구글 봇에게만 사전 렌더링된 HTML을 제공하는 동적 렌더링(서버 사이드 우회) 테크닉.
Dynamic Rendering이란: 검색 봇을 위한 우회 전략
Dynamic Rendering(동적 렌더링)이란 동일한 URL에 대해 사용자에게는 기존 CSR(SPA) 페이지를, 검색엔진 크롤러에게는 사전 렌더링된 정적 HTML을 제공하는 기법입니다. Google 공식 문서에서 "workaround(우회책)"으로 분류하며, SSR/SSG로의 전환이 어려운 레거시 SPA를 위한 임시 솔루션으로 권장합니다.
핵심 개념
- User-Agent 감지: 웹 서버(Nginx, Cloudflare Workers 등)에서 요청의 User-Agent를 검사하여 Googlebot, Bingbot 등 크롤러인지 판별
- 크롤러 → 사전 렌더링된 HTML: 크롤러 요청이면 Headless 브라우저(Puppeteer, Chromium)로 SPA를 미리 렌더링한 HTML 스냅샷을 반환
- 사용자 → 기존 SPA: 일반 사용자 요청이면 기존 CSR 그대로 응답
Dynamic Rendering ≠ 클로킹(Cloaking)
클로킹은 사용자와 크롤러에게 다른 콘텐츠를 보여주는 것으로 Google 가이드라인 위반입니다. Dynamic Rendering은 동일한 콘텐츠를 다른 형식(CSR vs 정적 HTML)으로 전달하는 것이므로 클로킹이 아닙니다.
| 구분 | 클로킹 (❌ 위반) | Dynamic Rendering (✅ 허용) |
|---|---|---|
| 사용자에게 보이는 콘텐츠 | A (예: 정상 페이지) | A (SPA 렌더링 결과) |
| 크롤러에게 보이는 콘텐츠 | B (예: 키워드로 도배된 페이지) | A (동일 콘텐츠의 정적 HTML 버전) |
| Google 판단 | 의도적 속임수 → 수동 조치 (패널티) | 콘텐츠 동일 → 허용되는 우회 기법 |
단, Google은 Dynamic Rendering을 "장기적 솔루션이 아닌 임시 대안"이라고 명시합니다. 가능하다면 SSR/SSG로 전환하는 것이 정석입니다.
구현 방법 1: Prerender.io (SaaS 솔루션)
Prerender.io는 가장 널리 사용되는 Dynamic Rendering SaaS 서비스입니다. 별도의 서버 구축 없이 미들웨어 하나로 설정할 수 있습니다.
동작 원리
- Prerender.io 미들웨어가 요청의 User-Agent를 검사
- 크롤러 요청이면 Prerender.io 클라우드 서버로 요청 프록시
- Prerender.io가 Headless Chrome으로 SPA를 렌더링
- 렌더링된 HTML을 캐시하고 크롤러에게 응답
- 다음번 같은 URL 요청 시 캐시에서 즉시 응답
Express.js에서 Prerender.io 적용
// server.js
const express = require('express');
const prerender = require('prerender-node');
const path = require('path');
const app = express();
// Prerender.io 미들웨어 설정
app.use(prerender.set('prerenderToken', process.env.PRERENDER_TOKEN));
// 정적 파일 서빙 (React 빌드 결과물)
app.use(express.static(path.join(__dirname, 'build')));
// SPA Fallback: 모든 경로에서 index.html 반환
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
app.listen(3000);
Nginx에서 Prerender.io 적용
server {
listen 80;
server_name example.com;
root /var/www/app/build;
location / {
# 크롤러 User-Agent 감지
set $prerender 0;
if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator") {
set $prerender 1;
}
# 크롤러이면 Prerender.io 서버로 프록시
if ($prerender = 1) {
rewrite .* /https://$host$request_uri? break;
proxy_pass https://service.prerender.io;
proxy_set_header X-Prerender-Token YOUR_TOKEN;
}
# 일반 사용자이면 SPA 서빙
try_files $uri /index.html;
}
}
Prerender.io 요금 및 특징
| 항목 | 내용 |
|---|---|
| 무료 플랜 | 월 250 페이지 캐시 |
| 유료 플랜 시작 | $15/월 (2,500 페이지) |
| 캐시 갱신 | Sitemap 기반 자동 RE-캐시 + API로 수동 무효화 |
| 지원 크롤러 | Googlebot, Bingbot, Facebook, Twitter, LinkedIn, Slack 등 30+ 봇 자동 감지 |
| 렌더링 엔진 | 최신 Chrome 기반 |
구현 방법 2: 자체 Pre-renderer 구축 (Puppeteer / Rendertron)
Puppeteer 기반 Pre-render 서버
// prerender-server.js
const express = require('express');
const puppeteer = require('puppeteer');
const app = express();
let browser;
// 브라우저 인스턴스 초기화
(async () => {
browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
})();
app.get('/render', async (req, res) => {
const { url } = req.query;
if (!url) return res.status(400).send('URL required');
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle0', timeout: 10000 });
// 완전히 렌더링된 HTML 추출
const html = await page.content();
await page.close();
// 크롤러에게 응답
res.set('Content-Type', 'text/html');
res.send(html);
} catch (error) {
res.status(500).send('Render failed');
}
});
app.listen(3001, () => console.log('Pre-render server on :3001'));
Rendertron: Google 공식 오픈소스
Rendertron은 Google Chrome 팀이 만든 오픈소스 Dynamic Rendering 솔루션입니다. Docker로 쉽게 배포할 수 있습니다.
# Docker로 Rendertron 배포
docker pull nickreese/rendertron
docker run -p 3001:3000 nickreese/rendertron
# 렌더링 요청
# GET http://localhost:3001/render/https://example.com/page
자체 구축 vs SaaS 비교
| 항목 | Prerender.io (SaaS) | Puppeteer/Rendertron (자체) |
|---|---|---|
| 초기 설정 | ✅ 매우 간단 (미들웨어 1줄) | ⚠️ Puppeteer 서버 + Nginx 프록시 설정 필요 |
| 운영 부담 | ✅ 없음 (SaaS가 관리) | ❌ 높음 (서버 모니터링, Chrome 업데이트, 메모리 관리) |
| 비용 | ⚠️ 페이지 수에 따라 $15~$200+/월 | ✅ 서버 비용만 (소규모 시 저렴) |
| 캐시 관리 | ✅ 자동 (Sitemap 기반) | ⚠️ 직접 구현 (Redis, 파일 시스템) |
| 안정성 | ✅ 높음 | ⚠️ Chromium 메모리 누수, 크래시 관리 필요 |
| 대규모 사이트 | ✅ 확장 용이 | ⚠️ 수평 확장 직접 구현 |
Cloudflare Workers / Vercel Edge Middleware로 구현
Cloudflare Workers 활용
Cloudflare Workers는 CDN 엣지에서 실행되므로 User-Agent 감지와 프록시를 극도로 빠르게 처리합니다.
// cloudflare-worker.js
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
const BOT_AGENTS = [
'googlebot', 'bingbot', 'yandex', 'baiduspider',
'twitterbot', 'facebookexternalhit', 'slackbot',
'linkedinbot', 'pinterestbot', 'discordbot',
];
async function handleRequest(request) {
const ua = (request.headers.get('User-Agent') || '').toLowerCase();
const isBot = BOT_AGENTS.some(bot => ua.includes(bot));
if (isBot) {
// Prerender 서버로 프록시
const prerenderUrl = `https://service.prerender.io/${request.url}`;
return fetch(prerenderUrl, {
headers: {
'X-Prerender-Token': PRERENDER_TOKEN,
},
});
}
// 일반 사용자 → 원본 서버
return fetch(request);
}
Vercel Edge Middleware 활용
// middleware.ts (Next.js / Vercel)
import { NextRequest, NextResponse } from 'next/server';
const BOT_PATTERN = /googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|slackbot/i;
export function middleware(request: NextRequest) {
const ua = request.headers.get('user-agent') || '';
if (BOT_PATTERN.test(ua)) {
// 크롤러이면 프리렌더 서비스로 리라이트
const prerenderUrl = new URL(
`/render/${request.url}`,
'https://your-prerender-server.com'
);
return NextResponse.rewrite(prerenderUrl);
}
return NextResponse.next();
}
Edge 기반 Dynamic Rendering의 장점
- 초저지연 User-Agent 감지: CDN 엣지에서 판별하므로 Origin 서버 부하 없음
- 전역 분산: 전세계 어디서 크롤링해도 가장 가까운 엣지에서 처리
- 기존 인프라 무변경: SPA 서버 코드를 전혀 수정하지 않고 앞단에서 처리
캐시 전략과 SEO 최적화 팁
캐시 전략
| 전략 | 설명 | 적합한 경우 |
|---|---|---|
| 사이트맵 기반 선제 캐시 | sitemap.xml의 URL을 주기적으로 사전 렌더링하여 캐시. 크롤러 첫 방문 시 즉시 캐시된 HTML 응답 | 페이지 수 10,000개 이하인 사이트 |
| On-Demand 캐시 (Lazy) | 크롤러가 실제 방문했을 때 렌더링하고 결과를 캐시. 이후 동일 URL 요청에 캐시 응답 | 대규모 사이트 (전체 사전 렌더링이 비현실적) |
| Webhook 기반 캐시 무효화 | CMS 콘텐츠 변경 시 Webhook으로 해당 URL의 캐시를 무효화하고 재렌더링 | 콘텐츠가 자주 업데이트되는 사이트 |
| TTL 기반 자동 갱신 | 캐시에 TTL(Time To Live) 설정. 만료 후 다음 요청에서 자동 재렌더링 | 업데이트 빈도가 예측 가능한 사이트 |
Dynamic Rendering SEO 최적화 체크리스트
- 메타 태그 완전성 검증
- 렌더링된 HTML에 올바른
<title>,<meta description>, OG 태그가 포함되는지 확인 - SPA의
react-helmet등이 렌더링 완료 후 메타 태그를 올바르게 삽입하는지 테스트
- 렌더링된 HTML에 올바른
- 렌더링 대기 시간 최적화
- Puppeteer의
waitUntil: 'networkidle0'설정으로 모든 네트워크 요청 완료 후 HTML 추출 - 불필요한 리소스(광고 스크립트, 분석 태그) 차단하여 렌더링 속도 향상
- Puppeteer의
- HTTP 상태 코드 전달
- SPA에서 404 페이지를 보여줄 때 프리렌더러도 404 상태 코드를 반환해야 함
- 301/302 리다이렉트도 프리렌더러가 올바르게 전달해야 함
- canonical 태그 일관성
- 사용자에게 보이는 CSR 페이지와 크롤러에게 보이는 사전 렌더링 HTML의 canonical URL이 동일해야 함
- JavaScript 에러 모니터링
- SPA의 JS 에러가 렌더링을 방해하면 크롤러에게 빈 HTML이 전달됨
- Sentry 등으로 프리렌더링 서버에서 발생하는 에러를 모니터링
Dynamic Rendering의 한계와 전환 시점
| 한계 | SSR/SSG 전환이 필요한 시점 |
|---|---|
| 캐시 관리 복잡성 증가 | 페이지 수가 10만 개를 초과할 때 |
| 렌더링 비용 (Puppeteer/SaaS) | 월 비용이 SSR 호스팅보다 높아질 때 |
| Google이 "우회책"으로 분류 | 장기적 투자가 필요한 비즈니스 |
| 비Google 크롤러 커버리지 불완전 | 네이버, AI 검색, SNS 유입이 중요해질 때 |
| Core Web Vitals 사용자 경험은 미개선 | CWV 점수가 비즈니스에 영향을 줄 때 |
자주 묻는 질문 (FAQ)
Q. Google이 Dynamic Rendering을 폐기할 계획이 있나요?
2024년 기준 Google은 Dynamic Rendering을 공식 문서에서 "특정 콘텐츠에 대한 우회 솔루션"으로 유지하고 있습니다. 폐기 계획을 발표하지는 않았지만, Google의 WRS(Web Rendering Service)가 지속적으로 개선되면서 장기적으로는 필요성이 줄어들 것으로 예상됩니다. Google은 2019년부터 SSR/SSG로의 전환을 "더 나은 접근 방식"으로 권장해왔습니다. 새 프로젝트를 시작한다면 Dynamic Rendering보다 SSR 프레임워크를 선택하는 것이 현명합니다.
Q. Dynamic Rendering이 Google 페널티를 받을 수 있나요?
올바르게 구현하면 페널티를 받지 않습니다. Google은 Dynamic Rendering을 공식적으로 허용합니다. 단, 2가지 조건을 반드시 지켜야 합니다: (1) 콘텐츠가 동일해야 합니다 — 크롤러에게 사용자와 다른 콘텐츠를 보여주면 클로킹으로 판정. (2) User-Agent 감지가 정확해야 합니다 — Googlebot을 오감지하여 일반 사용자에게 프리렌더링 HTML을 보여주면 UX 문제. Google의 Rich Result Test, URL 검사 도구로 크롤러가 받는 HTML이 사용자 경험과 동일한 콘텐츠인지 반드시 검증하세요.
Q. 사전 렌더링된 HTML에 JavaScript가 포함되어 있어도 되나요?
네, 포함되어도 됩니다. 프리렌더링의 목적은 콘텐츠가 포함된 완성된 HTML을 제공하는 것입니다. JS 파일 참조가 HTML에 남아 있어도 크롤러는 HTML의 콘텐츠를 직접 파싱하므로 문제없습니다. 다만 불필요한 JS 제거는 성능에 도움: 분석 스크립트(GA, GTM), 광고 스크립트, 채팅 위젯 등은 크롤러에게 불필요하므로 프리렌더링 시 차단하면 렌더링 속도가 향상됩니다.
Q. Dynamic Rendering을 적용하면 Core Web Vitals도 개선되나요?
아닙니다. Dynamic Rendering은 크롤러의 인덱싱만 개선합니다. 실제 사용자는 여전히 기존 SPA(CSR)를 보기 때문에 Core Web Vitals(LCP, CLS, INP)는 변하지 않습니다. Google의 CWV 측정은 실제 사용자 데이터(CrUX)를 기반으로 하므로 Dynamic Rendering으로는 개선되지 않습니다. CWV까지 개선하려면 SSR/SSG로 전환하거나 SPA 자체의 성능 최적화(코드 스플리팅, 지연 로딩, 번들 축소)가 필요합니다.
Q. 이미 Next.js를 사용 중인데 Dynamic Rendering이 필요한가요?
Next.js를 올바르게 사용 중이라면 Dynamic Rendering이 전혀 필요하지 않습니다. Next.js는 기본적으로 SSR/SSG를 제공하므로 서버에서 완성된 HTML을 생성합니다. 이미 크롤러에게 완전한 HTML을 전달하고 있습니다. Dynamic Rendering이 필요한 경우는 오직: (1) 순수 CSR SPA(React+Vite 등)를 운영 중이고, (2) SSR 프레임워크로 마이그레이션할 여력이 없을 때입니다. Next.js, Nuxt.js, Astro 등의 SSR/SSG 프레임워크를 사용 중이라면 Dynamic Rendering은 불필요한 복잡성만 추가합니다.