내가 Next.js ISR을 선택한 이유: 블로그 SEO, 그 고민의 시작과 해결
들어가며
블로그를 만들 때 어떤 기술 스택을 선택할지 고민이 많았습니다. React를 오래 사용해왔기에 당연히 React로 시작하려 했지만, 곰곰이 생각해보니 블로그에는 React보다 Next.js가 맞겠다는 결론에 도달했습니다. 그 핵심 이유는 바로 ISR(Incremental Static Regeneration)과 SEO 최적화였습니다.
돌이켜 생각해보면, 이 선택은 정말 잘한 결정이었습니다. 이 글에서는 왜 순수 React로는 블로그를 만들기 어려운지, 그리고 Next.js의 ISR이 어떻게 이 문제를 해결하는지 정리해보려 합니다.
왜 React가 아닌 Next.js인가?
React(CSR)의 SEO 한계
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를 실행하지 않음
결과: 빈 미리보기 또는 기본 이미지만 표시
Next.js가 해결하는 문제들
문제 | React (CSR) | Next.js (SSG/SSR/ISR) |
|---|---|---|
초기 HTML | 비어있음 | 완전한 콘텐츠 포함 |
검색 엔진 인덱싱 | 지연/불완전 | 즉시 인덱싱 |
소셜 미리보기 | 실패 | 완벽 지원 |
첫 페이지 로드 | 느림 (JS 다운로드 후) | 빠름 (HTML 즉시 표시) |
Core Web Vitals | 낮은 점수 | 높은 점수 |
ISR: 최선의 선택
Next.js는 세 가지 렌더링 방식을 제공합니다:
SSG (Static Site Generation): 빌드 시 정적 생성 → 빠르지만 업데이트 어려움
SSR (Server Side Rendering): 매 요청마다 렌더링 → 최신 데이터지만 느림
ISR (Incremental Static Regeneration): SSG + 주기적 재생성 → 두 장점 모두 달성
여러 방식을 비교해본 결과, 블로그에는 ISR이 최적의 선택이라는 결론에 도달했습니다. 정적 사이트의 속도를 유지하면서도 콘텐츠 업데이트를 자동으로 반영할 수 있기 때문입니다.
ISR이란?
ISR은 점진적 정적 재생성을 의미합니다. 빌드 시 정적 페이지를 생성하고, 설정된 시간이 지나면 백그라운드에서 페이지를 재생성하는 방식입니다.
핵심 특징
정적 페이지의 속도: CDN에서 즉시 제공 (50ms 이하)
SSR의 실시간성: 설정된 주기로 콘텐츠 자동 갱신
서버 부하 최소화: 캐시된 페이지를 재사용
사용자 대기 없음: Stale-While-Revalidate 전략
3단계 캐싱 아키텍처
Next.js ISR은 3단계 캐싱 레이어로 동작합니다. 이 구조를 이해하는 것이 중요합니다.
Layer 1: 브라우저 캐시
Cache-Control 헤더를 통해 브라우저에서 직접 캐싱합니다.
Layer 2: CDN/Edge 캐시
s-maxage와 stale-while-revalidate 헤더로 CDN에서 캐싱합니다.
Layer 3: Next.js ISR 캐시
서버의 .next/cache/ 디렉토리에 HTML과 RSC Payload를 저장합니다.
// 페이지 레벨 ISR 설정
export const revalidate = 3600; // 1시간
// Fetch 레벨 ISR 설정
const res = await fetch(url, {
next: { revalidate: 300 } // 5분
});Stale-While-Revalidate 전략
ISR의 핵심은 사용자를 절대 기다리게 하지 않는다는 것입니다. 이 점이 정말 마음에 들었습니다.
동작 흐름
캐시 유효: 즉시 반환 (50ms)
캐시 만료: 스테일 캐시 즉시 반환 + 백그라운드 재생성
재생성 완료: 다음 요청부터 새 페이지 제공
시간 0s 300s 350s 600s
│ │ │ │
▼ ▼ ▼ ▼
┌─────┬──────────┬──────────┬──────────┐
│FRESH│ STALE │ REFRESH │ FRESH │
└─────┴──────────┴──────────┴──────────┘
요청1 → 캐시 생성
요청2 → 스테일 캐시 반환 (즉시) + 백그라운드 재생성
요청3 → 새 캐시 반환실무 적용 예시
1. 페이지 레벨 설정
// 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} />;
}2. On-Demand Revalidation
콘텐츠 수정 시 즉시 캐시를 무효화할 수 있습니다. 이 기능 덕분에 글을 수정하면 바로 반영됩니다:
// 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 });
}3. Backend 연동
// 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 | 최소 | 설정 주기 |
권장 revalidate 설정
다양한 페이지 유형에 따른 권장 설정값입니다:
페이지 유형 | 권장값 | 이유 |
|---|---|---|
블로그 상세 | 3600초 (1시간) | 자주 변경되지 않음 |
홈페이지 | 300초 (5분) | 최신 글 빠르게 반영 |
태그/카테고리 | 600초 (10분) | 중간 빈도 업데이트 |
프로필/설정 | 0 (비활성화) | 실시간 데이터 필요 |
주의사항
1. 언어별 독립 캐시
다국어 사이트의 경우, 각 언어별로 별도 캐시가 생성됩니다:
/ko/blog/my-post → 한국어 캐시
/en/blog/my-post → 영어 캐시 (독립)2. revalidatePath 호출
On-Demand Revalidation 시 모든 언어 경로를 무효화해야 합니다:
revalidatePath('/ko/blog/my-post');
revalidatePath('/en/blog/my-post');3. SKIP_STATIC_GENERATION
Coolify 등 일부 배포 환경에서는 빌드 시 API 호출이 실패할 수 있습니다. 이 경우 환경변수로 스킵하고 런타임 ISR로 대체합니다:
SKIP_STATIC_GENERATION=true pnpm buildX-Nextjs-Cache 헤더
응답 헤더에서 캐시 상태를 확인할 수 있습니다. 디버깅할 때 유용합니다:
값 | 의미 |
|---|---|
HIT | 캐시 적중 |
STALE | 스테일 캐시 반환 중 |
MISS | 캐시 미스, 새로 생성 |
REVALIDATED | On-Demand 재생성 후 |
마치며
이 블로그를 만들 때 React(CRA)로 시작할 수도 있었습니다. 하지만 다음과 같은 이유로 Next.js + ISR을 선택했고, 지금도 잘한 결정이었다는 생각이 듭니다:
SEO 필수: 블로그는 검색 엔진에서 발견되어야 의미가 있습니다. React CSR로는 이를 달성하기 어렵습니다.
소셜 공유: 카카오톡, 트위터에 글을 공유할 때 미리보기가 제대로 나와야 합니다.
성능: 50ms 응답 속도는 사용자 경험을 크게 향상시킵니다.
유지보수: ISR 덕분에 글을 수정해도 전체 사이트를 재빌드할 필요가 없습니다.
결론적으로, 콘텐츠 중심 웹사이트(블로그, 뉴스, 문서 사이트)를 만든다면 React가 아닌 Next.js를 선택해야 합니다. ISR은 정적 사이트의 속도와 동적 사이트의 유연성을 동시에 제공하는 현대 웹 개발의 핵심 기술입니다.
관련 글
개인 블로그에 AI 검색 달기 (1) - 왜 하이브리드 검색인가
블로그 검색 기능을 개선하면서 키워드 검색의 한계를 느꼈습니다. 벡터 검색과 키워드 검색을 결합한 하이브리드 검색을 선택한 이유와 아키텍처 설계 과정을 공유합니다.
개인 블로그에 AI 검색 달기 (2) - MongoDB Atlas Vector Search 구현
MongoDB Atlas Vector Search 인덱스 설정부터 NestJS에서 하이브리드 검색을 구현하는 과정. $vectorSearch의 null 필터 제한사항과 RRF 알고리즘, 유사도 임계값 튜닝까지.
Next.js 배포 시 빈 캐시 문제 해결: 런타임 워밍에서 빌드 타임 정적 생성으로
Next.js + Coolify 환경에서 배포 직후 빈 캐시와 no available server 에러가 발생하는 문제를 해결한 경험. 런타임 캐시 워밍의 한계를 겪고, 빌드 타임 정적 생성으로 전환하여 근본적으로 해결했습니다.