블로그를 만들 때 어떤 기술 스택을 선택할지 고민이 많았습니다. React를 오래 사용해왔기에 당연히 React로 시작하려 했지만, 곰곰이 생각해보니 블로그에는 React보다 Next.js가 맞겠다는 결론에 도달했습니다. 그 핵심 이유는 바로 ISR(Incremental Static Regeneration)과 SEO 최적화였습니다.
돌이켜 생각해보면, 이 선택은 정말 잘한 결정이었습니다. 이 글에서는 왜 순수 React로는 블로그를 만들기 어려운지, 그리고 Next.js의 ISR이 어떻게 이 문제를 해결하는지 정리해보려 합니다.
React로 만든 SPA(Single Page Application)는 클라이언트 사이드 렌더링(CSR)을 사용합니다. 처음에는 이게 무슨 문제가 되는지 잘 몰랐는데, 직접 블로그를 운영해보니 치명적인 문제가 있다는 것을 깨달았습니다.
1. 빈 HTML 문제
React 앱의 초기 HTML은 이렇게 생겼습니다:
<!DOCTYPE html>
<html>
<head>
<title>My Blog</title>
</head>
<body>
<div id="root"></div> <!-- 비어있음! -->
<script src="bundle.js"></script>
</body>
</html>검색 엔진 크롤러가 이 페이지를 방문하면 빈 페이지를 보게 됩니다. JavaScript가 실행되어야 콘텐츠가 나타나기 때문입니다. 결국 열심히 글을 써도 검색에 노출되지 않는 상황이 발생할 수밖에 없습니다.
2. 검색 엔진 크롤링 문제
Google은 JavaScript를 실행할 수 있지만, 몇 가지 문제가 있습니다:
지연된 인덱싱: JS 실행은 별도 큐에서 처리되어 인덱싱이 늦어짐
렌더링 비용: Google도 모든 페이지의 JS를 실행하지 않음
다른 검색 엔진: Naver, Bing 등은 JS 실행 능력이 제한적
3. 소셜 미디어 미리보기 실패
카카오톡이나 페이스북에 링크를 공유할 때 미리보기가 제대로 표시되지 않았습니다:
Open Graph 메타 태그가 동적으로 생성됨
소셜 크롤러는 JavaScript를 실행하지 않음
결과: 빈 미리보기 또는 기본 이미지만 표시
문제 | React (CSR) | Next.js (SSG/SSR/ISR) |
|---|---|---|
초기 HTML | 비어있음 | 완전한 콘텐츠 포함 |
검색 엔진 인덱싱 | 지연/불완전 | 즉시 인덱싱 |
소셜 미리보기 | 실패 | 완벽 지원 |
첫 페이지 로드 | 느림 (JS 다운로드 후) | 빠름 (HTML 즉시 표시) |
Core Web Vitals | 낮은 점수 | 높은 점수 |
Next.js는 세 가지 렌더링 방식을 제공합니다:
SSG (Static Site Generation): 빌드 시 정적 생성 → 빠르지만 업데이트 어려움
SSR (Server Side Rendering): 매 요청마다 렌더링 → 최신 데이터지만 느림
ISR (Incremental Static Regeneration): SSG + 주기적 재생성 → 두 장점 모두 달성
여러 방식을 비교해본 결과, 블로그에는 ISR이 최적의 선택이라는 결론에 도달했습니다. 정적 사이트의 속도를 유지하면서도 콘텐츠 업데이트를 자동으로 반영할 수 있기 때문입니다.
ISR은 점진적 정적 재생성을 의미합니다. 빌드 시 정적 페이지를 생성하고, 설정된 시간이 지나면 백그라운드에서 페이지를 재생성하는 방식입니다.
정적 페이지의 속도: CDN에서 즉시 제공 (50ms 이하)
SSR의 실시간성: 설정된 주기로 콘텐츠 자동 갱신
서버 부하 최소화: 캐시된 페이지를 재사용
사용자 대기 없음: Stale-While-Revalidate 전략
Next.js ISR은 3단계 캐싱 레이어로 동작합니다. 이 구조를 이해하는 것이 중요합니다.
Cache-Control 헤더를 통해 브라우저에서 직접 캐싱합니다.
s-maxage와 stale-while-revalidate 헤더로 CDN에서 캐싱합니다.
서버의 .next/cache/ 디렉토리에 HTML과 RSC Payload를 저장합니다.
// 페이지 레벨 ISR 설정
export const revalidate = 3600; // 1시간
// Fetch 레벨 ISR 설정
const res = await fetch(url, {
next: { revalidate: 300 } // 5분
});ISR의 핵심은 사용자를 절대 기다리게 하지 않는다는 것입니다. 이 점이 정말 마음에 들었습니다.
캐시 유효: 즉시 반환 (50ms)
캐시 만료: 스테일 캐시 즉시 반환 + 백그라운드 재생성
재생성 완료: 다음 요청부터 새 페이지 제공
시간 0s 300s 350s 600s
│ │ │ │
▼ ▼ ▼ ▼
┌─────┬──────────┬──────────┬──────────┐
│FRESH│ STALE │ REFRESH │ FRESH │
└─────┴──────────┴──────────┴──────────┘
요청1 → 캐시 생성
요청2 → 스테일 캐시 반환 (즉시) + 백그라운드 재생성
요청3 → 새 캐시 반환// app/blog/[slug]/page.tsx
// ISR: 1시간마다 재검증
export const revalidate = 3600;
// 빌드 시 모든 페이지 미리 생성
export async function generateStaticParams() {
const posts = await fetchBlogPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPost({ params }) {
const post = await fetchBlogPost(params.slug);
return <Article post={post} />;
}콘텐츠 수정 시 즉시 캐시를 무효화할 수 있습니다. 이 기능 덕분에 글을 수정하면 바로 반영됩니다:
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
export async function POST(request) {
const { slug, secret } = await request.json();
// 보안 검증
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Invalid' }, { status: 401 });
}
// 캐시 무효화
revalidatePath(`/blog/${slug}`);
return Response.json({ revalidated: true });
}// NestJS Service
async update(slug: string, dto: UpdateDto) {
const updated = await this.model.update(slug, dto);
// ISR 캐시 무효화 트리거
await fetch(`${NEXTJS_URL}/api/revalidate`, {
method: 'POST',
body: JSON.stringify({ slug, secret: REVALIDATE_SECRET }),
});
return updated;
}방식 | 응답 시간 | 서버 부하 | 데이터 실시간성 |
|---|---|---|---|
SSR | 800-1500ms | 높음 | 항상 최신 |
SSG | 50ms | 없음 | 빌드 시점 |
ISR | 50ms | 최소 | 설정 주기 |
다양한 페이지 유형에 따른 권장 설정값입니다:
페이지 유형 | 권장값 | 이유 |
|---|---|---|
블로그 상세 | 3600초 (1시간) | 자주 변경되지 않음 |
홈페이지 | 300초 (5분) | 최신 글 빠르게 반영 |
태그/카테고리 | 600초 (10분) | 중간 빈도 업데이트 |
프로필/설정 | 0 (비활성화) | 실시간 데이터 필요 |
다국어 사이트의 경우, 각 언어별로 별도 캐시가 생성됩니다:
/ko/blog/my-post → 한국어 캐시
/en/blog/my-post → 영어 캐시 (독립)On-Demand Revalidation 시 모든 언어 경로를 무효화해야 합니다:
revalidatePath('/ko/blog/my-post');
revalidatePath('/en/blog/my-post');Coolify 등 일부 배포 환경에서는 빌드 시 API 호출이 실패할 수 있습니다. 이 경우 환경변수로 스킵하고 런타임 ISR로 대체합니다:
SKIP_STATIC_GENERATION=true pnpm build응답 헤더에서 캐시 상태를 확인할 수 있습니다. 디버깅할 때 유용합니다:
값 | 의미 |
|---|---|
HIT | 캐시 적중 |
STALE | 스테일 캐시 반환 중 |
MISS | 캐시 미스, 새로 생성 |
REVALIDATED | On-Demand 재생성 후 |
이 블로그를 만들 때 React(CRA)로 시작할 수도 있었습니다. 하지만 다음과 같은 이유로 Next.js + ISR을 선택했고, 지금도 잘한 결정이었다는 생각이 듭니다:
SEO 필수: 블로그는 검색 엔진에서 발견되어야 의미가 있습니다. React CSR로는 이를 달성하기 어렵습니다.
소셜 공유: 카카오톡, 트위터에 글을 공유할 때 미리보기가 제대로 나와야 합니다.
성능: 50ms 응답 속도는 사용자 경험을 크게 향상시킵니다.
유지보수: ISR 덕분에 글을 수정해도 전체 사이트를 재빌드할 필요가 없습니다.
결론적으로, 콘텐츠 중심 웹사이트(블로그, 뉴스, 문서 사이트)를 만든다면 React가 아닌 Next.js를 선택해야 합니다. ISR은 정적 사이트의 속도와 동적 사이트의 유연성을 동시에 제공하는 현대 웹 개발의 핵심 기술입니다.