SPA의 서버 사이드 렌더링(SSR): 검색 엔진 인덱싱 문제 해결 가이드
구글봇(Googlebot)이나 얀덱스봇(YandexBot) 같은 검색 크롤러는 CSR 기반 SPA를 크롤링할 때 종종 빈 HTML만 감지합니다. 서버 사이드 렌더링(SSR)이 없으면 크롤러는 자바스크립트 실행을 기다리지 않으며, 결과적으로 페이지가 인덱싱되지 않습니다. SSR은 클라이언트로 전송하기 전에 서버에서 완전히 렌더링된 HTML을 생성함으로써 이 문제를 해결합니다. 이를 통해 FCP, LCP, TTFB가 빨라지고 코어 웹 바이탈스(Core Web Vitals)와 유기적 검색 노출률이 크게 향상됩니다.
렌더링 방식 비교
| 항목 | CSR | SSR | 프리렌더링 | 정적 사이트 생성(SSG) |
|------|-----|-----|-------------|------------------------|
| 렌더링 위치 | 브라우저 | 서버(요청 시마다) | 사전 빌드(특정 URL 대상) | 사전 빌드(모든 페이지) |
| FCP/LCP 속도 | 느림 | 빠름 | 빠름 | 매우 빠름 |
| SEO 적합성 | 낮음 | 우수 | 양호 | 우수 |
| 데이터 최신성 | 실시간 | 실시간 | 오래됨 | 오래됨 |
| 서버 부하 | 최소 | 높음 | 낮음 | 최소 |
SSR은 제품 카탈로그, 뉴스 피드, 대시보드 등 동적 콘텐츠가 많은 페이지에 가장 적합합니다. 프리렌더링은 마케팅 페이지처럼 업데이트 빈도가 낮은 URL에 적합하며, SSG는 문서나 랜딩 페이지처럼 정적 콘텐츠 중심의 사이트에 이상적입니다.
React + Next.js: 단계별 구현 가이드
새 프로젝트 생성:
npx create-next-app@latest my-ssr-app
cd my-ssr-app
npm run dev
동적 페이지의 경우 getServerSideProps를 사용하세요:
// pages/product/[id].jsx
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://api.example.com/products/${id}`);
const product = await res.json();
if (!res.ok) {
return { notFound: true };
}
return {
props: { product },
};
}
export default function ProductPage({ product }) {
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
</main>
);
}
SEO 친화적인 메타 태그 추가:
import Head from 'next/head';
export default function ProductPage({ product }) {
return (
<>
<Head>
<title>{product.name} | 쇼핑몰</title>
<meta name="description" content={product.shortDescription} />
<meta property="og:title" content={product.name} />
<meta property="og:image" content={product.imageUrl} />
</Head>
<main>
<h1>{product.name}</h1>
</main>
</>
);
}
배포: Vercel, Netlify 또는 PM2 기반 Node.js 서버.
에러 없는 하이드레이션
하이드레이션은 클라이언트 측 React가 서버에서 렌더링된 HTML을 ‘살려내는’ 과정입니다. 렌더링 불일치를 피하려면 다음을 준수하세요:
- 서버와 클라이언트의 데이터 상태를 항상 동기화하세요.
window나document객체를 사용하기 전에 실행 환경을 반드시 확인하세요.- 브라우저 콘솔 경고를 주기적으로 모니터링하세요.
const isBrowser = typeof window !== 'undefined';
Vue + Nuxt.js: 서버 사이드 렌더링 구현
프로젝트 초기화:
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev
서버 측 데이터 조회:
<!-- pages/product/[id].vue -->
<script setup>
const route = useRoute();
const { data: product, error } = await useFetch(
`/api/products/${route.params.id}`
);
if (error.value) {
throw createError({ statusCode: 404, message: '페이지를 찾을 수 없습니다' });
}
useHead({
title: product.value?.name,
meta: [
{ name: 'description', content: product.value?.shortDescription },
],
});
</script>
<template>
<main>
<h1>{{ product?.name }}</h1>
<p>{{ product?.description }}</p>
</main>
</template>
nuxt.config.ts 설정 예시:
export default defineNuxtConfig({
ssr: true,
app: {
head: {
htmlAttrs: { lang: 'ko' },
link: [{ rel: 'canonical', href: 'https://example.com' }],
},
},
nitro: {
preset: 'node-server',
},
});
Angular Universal: SSR 통합 방법
Angular 17+ 기준:
ng add @angular/ssr
server.ts에서 서버 엔드포인트 정의:
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
// ... 기타 import
const server = express();
// ...
server.get('*', (req, res, next) => {
commonEngine
.render({
bootstrap: AppServerModule,
documentFilePath: join(browserDistFolder, 'index.html'),
url: req.originalUrl,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
})
.then((html) => res.send(html))
.catch(next);
});
빌드 및 실행:
npm run build:ssr
npm run serve:ssr
플랫폼 안전하게 감지하기:
import { isPlatformBrowser } from '@angular/common';
import { inject, PLATFORM_ID } from '@angular/core';
const platformId = inject(PLATFORM_ID);
if (isPlatformBrowser(platformId)) {
console.log(window.location.href);
}
헤드리스 CMS 연동 사례
Next.js + Contentful GraphQL 연동 예시:
export async function getServerSideProps({ params }) {
const query = `
query GetArticle($slug: String!) {
articleCollection(where: { slug: $slug }) {
items {
title
body
seo {
title
description
}
}
}
}
`;
const res = await fetch(
`https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.CONTENTFUL_TOKEN}`,
},
body: JSON.stringify({ query, variables: { slug: params.slug } }),
}
);
const { data } = await res.json();
// ...
}
핵심 요약
- SSR은 검색 크롤러에게 완전히 렌더링된 HTML을 제공해 CSR 앱의 근본적인 인덱싱 문제를 해결합니다.
- Next.js, Nuxt.js, Angular Universal은 실제 서비스 환경에서 검증된 프로덕션급 SSR 프레임워크입니다.
- 하이드레이션 불일치는 전체 재렌더링을 유발하므로, 런타임 환경을 항상 검사해야 합니다.
- SSR 도입 후 FCP, LCP, TTFB 등 코어 웹 바이탈스 지표는 50–80% 개선됩니다.
- 트래픽 급증 시 TTFB를 200ms 이하로 유지하려면 Redis 캐싱을 반드시 적용하세요.
— Editorial Team
아직 댓글이 없습니다.