홈

© 2025 Ki Chang. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

소개개인정보처리방침이용약관

© 2025 Ki Chang. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

소개|개인정보처리방침|이용약관

내가 Next.js ISR을 선택한 이유: 블로그 SEO, 그 고민의 시작과 해결

정기창·2025년 12월 27일

들어가며

블로그를 만들 때 어떤 기술 스택을 선택할지 고민이 많았습니다. 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의 핵심은 사용자를 절대 기다리게 하지 않는다는 것입니다. 이 점이 정말 마음에 들었습니다.

동작 흐름

  1. 캐시 유효: 즉시 반환 (50ms)

  2. 캐시 만료: 스테일 캐시 즉시 반환 + 백그라운드 재생성

  3. 재생성 완료: 다음 요청부터 새 페이지 제공

시간  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 build

X-Nextjs-Cache 헤더

응답 헤더에서 캐시 상태를 확인할 수 있습니다. 디버깅할 때 유용합니다:

값

의미

HIT

캐시 적중

STALE

스테일 캐시 반환 중

MISS

캐시 미스, 새로 생성

REVALIDATED

On-Demand 재생성 후

마치며

이 블로그를 만들 때 React(CRA)로 시작할 수도 있었습니다. 하지만 다음과 같은 이유로 Next.js + ISR을 선택했고, 지금도 잘한 결정이었다는 생각이 듭니다:

  • SEO 필수: 블로그는 검색 엔진에서 발견되어야 의미가 있습니다. React CSR로는 이를 달성하기 어렵습니다.

  • 소셜 공유: 카카오톡, 트위터에 글을 공유할 때 미리보기가 제대로 나와야 합니다.

  • 성능: 50ms 응답 속도는 사용자 경험을 크게 향상시킵니다.

  • 유지보수: ISR 덕분에 글을 수정해도 전체 사이트를 재빌드할 필요가 없습니다.

결론적으로, 콘텐츠 중심 웹사이트(블로그, 뉴스, 문서 사이트)를 만든다면 React가 아닌 Next.js를 선택해야 합니다. ISR은 정적 사이트의 속도와 동적 사이트의 유연성을 동시에 제공하는 현대 웹 개발의 핵심 기술입니다.

Next.jsISRSEOReact블로그웹 개발성능 최적화