Next.js i18n 규모별: 30개 언어, 91K 페이지, Vercel ISR
작년에 저는 현재도 약간 신경 쓰이는 Next.js 프로젝트를 배포했습니다. 30개의 언어. 91,000개 이상의 정적으로 생성된 페이지. Vercel ISR이 모든 것을 최신 상태로 유지합니다. 한 가지 아키텍처 결정을 잘못하면 4시간의 빌드 시간, 월 $800의 호스팅 청구서, 또는 가장 나쁜 경우 한국어로 작동하지 않는 사이트를 마주하게 되는 종류의 프로젝트입니다.
이것은 우리가 어떻게 올바르게 수행했는지(그리고 처음에는 제대로 하지 못했던 부분들)에 대한 이야기입니다. 대규모 국제화 Next.js 애플리케이션을 구축하고 있으며 ISR이 실제로 프로덕션에서 이를 처리할 수 있는지 궁금해하고 있다면, 이 글이 당신을 위한 것입니다.

목차
- 문제: 91K 페이지가 다른 이유
- 초기에 내린 아키텍처 결정
- 30개 로케일을 위한 Next.js i18n 설정
- 실제로 작동한 ISR 전략
- 콘텐츠 파이프라인 및 헤드리스 CMS 통합
- 성능 결과 및 Core Web Vitals
- Vercel의 비용 분석
- 우리가 저지른 실수와 해결 방법
- 이 스택을 사용해야 할 때 (그리고 사용하지 말아야 할 때)
- FAQ
문제: 91K 페이지가 다른 이유
상황을 설정하겠습니다. 클라이언트는 30개 시장으로 확장하는 엔터프라이즈 전자상거래 브랜드였습니다. 각 시장에는 다음이 필요했습니다:
- 로컬라이즈된 제품 페이지(~2,800개 제품 × 30개 로케일 = 84,000개 페이지)
- 카테고리 페이지(~120개 카테고리 × 30개 로케일 = 3,600개 페이지)
- CMS 기반 마케팅 페이지(~120 × 30 = 3,600개 페이지)
- 총: 약 91,200개의 고유 URL
일반적인 getStaticPaths 및 완전한 정적 생성으로, 초기 빌드는 3~5시간이 걸릴 예정이었습니다. 오타가 아닙니다. 우리는 초기 프로토타입을 벤치마크했고 수치가 올라가는 것을 지켜봤습니다. 모든 배포는 시간 단위의 다운타임 위험을 의미했으며, 콘텐츠 팀은 하루에 여러 번 업데이트를 게시하기를 원했습니다.
SSR도 선택지가 아니었습니다. 클라이언트의 트래픽 패턴은 판매 행사 중 대규모 스파이크를 보였습니다 -- 동시 사용자 50K를 말하고 있습니다. 그러한 로드 하에서 91K개의 가능한 페이지 변형을 서버 렌더링하려면 심각한 컴퓨팅이 필요하고 전환율을 죽이는 지연시간을 유발할 것입니다.
ISR이 답이었습니다. 하지만 이 규모의 ISR에는 Next.js 문서가 실제로 당신을 준비시키지 못하는 자체 문제 집합이 있습니다.
초기에 내린 아키텍처 결정
i18n 코드 한 줄을 작성하기 전에, 우리는 나중에 몇 개월의 고통을 우리에게 구했을 세 가지 아키텍처 결정을 내렸습니다.
결정 1: 도메인이 아닌 서브경로 라우팅
Next.js는 두 가지 i18n 전략을 지원합니다: 서브경로 라우팅(/fr/products/...) 및 도메인 라우팅(fr.example.com). 우리는 서브경로 라우팅을 선택했습니다. 그 이유는:
| 요소 | 서브경로 라우팅 | 도메인 라우팅 |
|---|---|---|
| DNS/SSL 복잡성 | 단일 도메인 | 30개의 도메인/서브도메인 관리 |
| Vercel 배포 | 하나의 프로젝트 | 하나의 프로젝트 (하지만 도메인 설정 오버헤드) |
| SEO 링크 권한 | 하나의 도메인에 통합 | 도메인 전체로 분할 |
| CDN 캐시 효율성 | 더 좋음 (공유 엣지 캐시) | 단편화됨 |
| 분석 설정 | 더 간단함 | 30개 속성 또는 복잡한 필터링 |
50개 로케일 미만의 대부분의 프로젝트의 경우, 서브경로 라우팅이 최선입니다. 도메인 라우팅은 법적 이유로 국가별 TLD가 필요하거나 시장에 근본적으로 다른 콘텐츠 아키텍처가 있을 때 의미가 있습니다.
결정 2: next-i18next보다 next-intl
우리는 두 라이브러리를 광범위하게 평가했습니다. 2025년에, next-intl (v4.x)은 App Router 프로젝트를 위한 더 강한 선택이 되었습니다. 비록 우리는 이 빌드를 위해 Pages Router에 있었지만 next-intl은 우리에게 다음을 제공했습니다:
- 타입 안전 메시지 키를 사용한 더 나은 TypeScript 지원
- 더 작은 클라이언트 번들 (next-i18next의 ~5KB 대비 약 2.1KB gzipped)
- ICU 메시지 형식에 대한 기본 지원 (복수형, 성별, 숫자 형식)
- ISR 페이지를 위한 더 간단한 구성
결정 3: 부분 정적 생성 + ISR
이것이 큰 것이었습니다. 빌드 시간에 모든 91K 페이지를 정적으로 생성하려고 시도하는 대신, 우리는 가장 높은 트래픽 페이지(약 8,000개)만 미리 구축하고 ISR이 나머지를 온디맨드로 처리하도록 했습니다.
// pages/[locale]/products/[slug].tsx
export async function getStaticPaths() {
// 상위 100개 제품 × 상위 5개 로케일만 미리 생성
const topProducts = await getTopProducts(100);
const primaryLocales = ['en', 'de', 'fr', 'es', 'ja'];
const paths = topProducts.flatMap(product =>
primaryLocales.map(locale => ({
params: { slug: product.slug, locale },
}))
);
return {
paths,
fallback: 'blocking', // ISR이 나머지를 처리합니다
};
}
이것은 빌드 시간을 3시간 이상에서 약 12분으로 단축했습니다. 나머지 83,000개 페이지는 첫 번째 요청에서 생성되고 엣지에서 캐시됩니다.

30개 로케일을 위한 Next.js i18n 설정
next.config.js의 Next.js 내장 i18n 설정이 로케일 감지 및 라우팅을 처리합니다. 우리의 설정은 다음과 같았습니다 (축약):
// next.config.js
const nextConfig = {
i18n: {
locales: [
'en', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'pl', 'cs', 'da',
'sv', 'fi', 'nb', 'hu', 'ro', 'bg', 'hr', 'sk', 'sl', 'el',
'tr', 'ja', 'ko', 'zh-CN', 'zh-TW', 'th', 'vi', 'id', 'ms', 'ar'
],
defaultLocale: 'en',
localeDetection: false, // 우리가 이것을 직접 처리합니다
},
};
여기서 주목해야 할 몇 가지가 있습니다. 우리는 localeDetection을 비활성화했습니다. 왜냐하면 내장 감지 (Accept-Language 헤더 기반)가 ISR 캐싱과 문제를 일으키고 있었기 때문입니다. Vercel의 CDN이 페이지를 캐시할 때, 로케일은 헤더가 아닌 URL에서 결정적이어야 합니다. 브라우저 언어를 기반으로 자동 리다이렉트를 허용하는 것은 캐시 미스와 불일치한 동작을 의미했습니다.
대신, 우리는 루트 경로에서만 실행되는 사용자 정의 로케일 감지 미들웨어를 구축했습니다:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const SUPPORTED_LOCALES = ['en', 'de', 'fr', /* ... */];
const DEFAULT_LOCALE = 'en';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 루트 경로에서만 리다이렉트
if (pathname === '/') {
const acceptLanguage = request.headers.get('accept-language') || '';
const preferred = acceptLanguage.split(',')[0]?.split('-')[0] || DEFAULT_LOCALE;
const locale = SUPPORTED_LOCALES.includes(preferred) ? preferred : DEFAULT_LOCALE;
return NextResponse.redirect(new URL(`/${locale}`, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/'],
};
번역 파일 구조
30개 언어로, 번역 파일 관리는 실제 우려 사항이 됩니다. 우리는 번역을 네임스페이스별로 구성했습니다:
messages/
├── en/
│ ├── common.json
│ ├── product.json
│ ├── checkout.json
│ └── marketing.json
├── de/
│ ├── common.json
│ ├── product.json
│ └── ...
└── ar/
└── ...
모든 언어에 걸친 총 번역 페이로드는 약 4.2MB였습니다. 하지만 getStaticProps를 사용해 페이지별 번역을 로드하기 때문에, 각 개별 페이지는 로케일 및 네임스페이스에 대한 15-40KB의 번역 데이터만 로드합니다. 이것이 중요합니다 -- 클라이언트에 30개 로케일을 모두 보내고 싶지 않습니다.
export async function getStaticProps({ locale }: GetStaticPropsContext) {
return {
props: {
messages: {
...(await import(`../../messages/${locale}/common.json`)).default,
...(await import(`../../messages/${locale}/product.json`)).default,
},
},
revalidate: 300, // ISR: 5분마다 재검증
};
}
아랍어에 대한 RTL 지원
아랍어는 우리 세트에서 유일한 RTL 언어였습니다. 우리는 간단한 레이아웃 래퍼로 처리했습니다:
const direction = locale === 'ar' ? 'rtl' : 'ltr';
return (
<html lang={locale} dir={direction}>
<body className={direction === 'rtl' ? 'font-arabic' : 'font-sans'}>
{children}
</body>
</html>
);
그리고 간격 및 레이아웃 조정을 위해 Tailwind의 rtl: 변형을 더했습니다. 이것은 놀랍도록 잘 작동했습니다 -- 우리 CSS의 약 5%만 RTL 특정 재정의가 필요했습니다.
실제로 작동한 ISR 전략
ISR (증분 정적 재생성)은 이 이야기의 영웅이지만, 규모에 따라 잘 사용하려면 Vercel의 인프라가 실제로 어떻게 작동하는지 이해해야 합니다.
재검증 타이밍
우리는 콘텐츠 유형에 따라 다른 재검증 기간을 사용했습니다:
| 페이지 유형 | 재검증 기간 | 이유 |
|---|---|---|
| 제품 페이지 | 300초 (5분) | 가격/재고가 자주 변경됨 |
| 카테고리 페이지 | 900초 (15분) | 제품 목록이 덜 자주 업데이트됨 |
| 마케팅/CMS 페이지 | 3600초 (1시간) | 콘텐츠 변경은 계획됨 |
| 로케일별 홈페이지 | 600초 (10분) | 신선함과 캐싱의 균형 |
온디맨드 재검증
중요한 업데이트 (가격 변경, 재고 부족)의 경우, 우리는 헤드리스 CMS에서 웹훅을 통해 온디맨드 재검증을 설정했습니다:
// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { secret, slug, locales } = req.body;
if (secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid secret' });
}
try {
const targetLocales = locales || ['en']; // 지정되지 않으면 영어로 기본값
const revalidations = targetLocales.map((locale: string) =>
res.revalidate(`/${locale}/products/${slug}`)
);
await Promise.all(revalidations);
return res.json({ revalidated: true, paths: targetLocales.length });
} catch (err) {
return res.status(500).json({ message: 'Error revalidating' });
}
}
한 가지 함정: 30개 로케일에 존재하는 제품을 재검증할 때, 당신은 30개의 재검증 호출을 하고 있습니다. 100개 제품의 대량 업데이트의 경우, 그것은 3,000개의 재검증 요청입니다. 우리는 Vercel의 API 제한에 도달하는 것을 피하기 위해 속도 제한을 추가하고 서버리스 함수를 통해 이들을 대기열에 추가해야 했습니다.
Stale-While-Revalidate 패턴
ISR의 아름다움은 배경에서 재생성하면서 진부한 콘텐츠를 제공한다는 것입니다. 이 프로젝트의 경우, 데이터가 최대 5분 오래되었더라도 사용자는 항상 빠른 응답 (Vercel의 엣지에서 캐시된 HTML)을 얻었습니다. 전자상거래 사이트의 경우, 이것은 수용할 수 있는 트레이드오프였습니다 -- 장바구니 및 체크아웃 흐름은 항상 실시간 재고/가격에 대해 라이브 API를 히트했습니다.
콘텐츠 파이프라인 및 헤드리스 CMS 통합
콘텐츠는 헤드리스 CMS (Contentful, 이 경우, 비록 우리는 다른 클라이언트의 경우 Sanity 및 Storyblok로 유사한 설정을 수행했습니다 -- 자세한 내용은 헤드리스 CMS 개발 서비스를 참조하십시오)에 생존했습니다.
Contentful의 로컬라이제이션 모델은 30개 로케일에 대해 잘 작동했습니다. 각 항목에는 로케일별 필드 값이 있으며, 해당 API는 로케일별 쿼리를 지원합니다. 하지만 성능 고려 사항이 있습니다: 모든 30개 로케일의 데이터가 있는 제품을 가져오는 것이 하나의 로케일을 가져오는 것보다 훨씬 더 큽니다.
우리는 항상 getStaticProps에서 단일 로케일에 대해 쿼리했습니다:
const product = await contentfulClient.getEntry(productId, {
locale: mapToContentfulLocale(locale), // 'en-US', 'de-DE', 등
include: 2, // 연결된 항목 2개 레벨 해결
});
이것은 복수 참조가 있는 복잡한 제품 항목에 대해서도 API 응답 시간을 200ms 미만으로 유지했습니다.
번역 관리
UI 번역 (버튼, 라벨, 오류 메시지)의 경우, 우리는 우리의 Git 저장소와 통합된 Crowdin을 사용했습니다. 워크플로우:
- 개발자가 새로운 영어 문자열을
messages/en/*.json에 추가 - Crowdin이 동기화되고 번역자에게 알림
- 번역이 PR로 다시 옵니다
- CI가 JSON 구조 및 완전성 검증
- 누락된 번역은 영어로 폴백됩니다
폴백 전략이 중요합니다. 프로덕션 페이지에 product.add_to_cart와 같은 번역 키가 표시되기를 원하지 않습니다. 우리의 폴백 체인은: 요청된 로케일 → 언어 계족 (예: pt-BR → pt) → 영어입니다.
성능 결과 및 Core Web Vitals
배포 후, 30개 로케일 전체에서 측정한 내용은 다음과 같습니다:
| 메트릭 | 목표 | 실제 (P75) | 참고 |
|---|---|---|---|
| LCP | < 2.5초 | 1.8초 | ISR 캐시 히트 |
| FID | < 100ms | 45ms | 최소 클라이언트 측 JS |
| CLS | < 0.1 | 0.03 | 글꼴 로딩 전략이 도움이 됨 |
| TTFB | < 800ms | 120ms | Vercel 엣지, 캐시된 페이지 |
| TTFB (캐시 미스) | < 2초 | 1.4초 | ISR이 첫 번째 요청에서 생성 |
| 빌드 시간 | < 20분 | 11분 40초 | 8K 페이지만 미리 생성 |
TTFB 수치가 별입니다. 캐시된 페이지에 대해 120ms는 도쿄, 상파울루 및 프랑크푸르트의 사용자 모두가 가까운 엣지 노드에서 빠른 응답을 받는다는 의미입니다. 캐시 미스에 대한 1.4초는 ISR 생성 시간입니다 -- 재검증 기간마다 한 번만 발생하므로 허용됩니다.
30개 언어에 대한 글꼴 로딩
다국어 사이트에 특정한 한 가지 성능 과제: 글꼴. 30개 언어에 단일 글꼴 계족을 사용할 수 없습니다. 우리는 다음이 필요했습니다:
- 라틴/키릴 문자: Inter (대부분의 유럽 언어)
- 아랍어: Noto Sans Arabic
- CJK: Noto Sans JP/KR/SC/TC
- 태국어: Noto Sans Thai
next/font를 로케일별 글꼴 로딩으로 사용하는 것은 불필요한 글꼴 다운로드를 방지했습니다. 일본 사이트를 방문하는 사용자는 아랍어 또는 태국어 글꼴이 아닌 Noto Sans JP만 다운로드합니다.
Vercel의 비용 분석
돈을 말해봅시다. 대규모 ISR이 비용 측면에서 흥미로워지는 곳이기 때문입니다. 2025년 월간 Vercel 청구서 분석:
| 항목 | 월 비용 | 참고 |
|---|---|---|
| Vercel Pro 플랜 | $20/인당 × 4 | 기본 팀 플랜 |
| 대역폭 (8TB/월) | ~$320 | 첫 1TB 이후 $40/TB |
| 서버리스 함수 실행 | ~$180 | ISR 재생성 + API 경로 |
| 엣지 미들웨어 실행 | ~$45 | 로케일 감지 |
| ISR 쓰기 | ~$90 | 캐시 쓰기 작업 |
| 합계 | ~$715/월 |
30개 로케일에 걸쳐 월 2M+ 페이지뷰를 처리하는 사이트의 경우, $715는 매우 합리적입니다. 대체 -- 전용 인프라에서 SSR 실행 -- 동등한 성능 및 안정성에 대해 월 $2,000-4,000이 소요되었습니다.
한 가지 주목할 점: ISR 캐시 쓰기 비용은 대량 재검증을 트리거하면 급증할 수 있습니다. 우리는 CMS 대량 게시가 동시에 15,000개 페이지의 재검증을 트리거한 사건이 있었습니다. 그 단일 이벤트는 추가 함수 실행 비용으로 약 $40이 들었습니다. 우리는 이제 재검증 호출을 그들 사이에 100ms 지연으로 배치합니다.
우리가 저지른 실수와 해결 방법
나는 이것이 첫날부터 순조롭게 진행되었다고 거짓말을 할 것입니다. 여기 가장 큰 실수들이 있습니다:
실수 1: 빌드 시간에 모든 로케일 생성
우리의 첫 번째 접근 방식은 모든 로케일에서 모든 페이지를 미리 생성하려고 시도했습니다. 빌드는 3시간 47분 동안 실행되었습니다. 그러면 Vercel의 빌드 타임아웃 (Pro에서)이 45분이기 때문에 실패했습니다. 사용자 정의 빌드 서버로 이동한 후에도, 배포 프로세스는 비참했습니다.
해결책: fallback: 'blocking'을 사용한 부분 사전 생성. 가장 중요한 페이지만 빌드하고 ISR이 긴 꼬리를 처리하도록 합니다.
실수 2: `fallback`을 올바르게 설정하지 않음
우리는 처음에 fallback: true 대신 fallback: 'blocking'을 사용했습니다. 차이는 중요합니다: true는 첫 번째 요청에서 스켈레톤/로딩 상태를 제공하는 반면, blocking은 페이지가 생성될 때까지 기다립니다. true로, 우리는 하이드레이션 오류를 받고 있었습니다. 왜냐하면 우리의 제품 컴포넌트는 폴백 렌더링 중에 아직 없는 데이터를 예상했기 때문입니다.
해결책: fallback: 'blocking'으로 전환했습니다. 캐시되지 않은 페이지의 첫 번째 방문자는 1-2초를 기다리지만, 그 이후 모든 사람은 캐시된 버전을 즉시 받습니다.
실수 3: SEO Hreflang 태그가 잘못됨
이것은 엉망으로 만들기 쉬운 것입니다. Google은 로컬라이즈된 페이지 간의 관계를 이해하기 위해 hreflang 태그를 필요로 합니다. 우리의 초기 구현은 x-default 태그를 놓치고 있었고 <link> 태그와 XML 사이트맵 간에 불일치가 있었습니다.
// 올바른 hreflang 구현
<Head>
{locales.map(loc => (
<link
key={loc}
rel="alternate"
hrefLang={loc}
href={`https://example.com/${loc}${path}`}
/>
))}
<link rel="alternate" hrefLang="x-default" href={`https://example.com/en${path}`} />
</Head>
실수 4: 사이트맵 생성
91K URL로, 단일 사이트맵 XML 파일은 작동하지 않습니다 (Google의 제한은 사이트맵당 50,000 URL). 우리는 로케일별로 분할된 여러 자식 사이트맵이 있는 사이트맵 인덱스가 필요했습니다:
<!-- sitemap-index.xml -->
<sitemapindex>
<sitemap><loc>https://example.com/sitemaps/en.xml</loc></sitemap>
<sitemap><loc>https://example.com/sitemaps/de.xml</loc></sitemap>
<!-- ... 28개 더 -->
</sitemapindex>
우리는 사용자 정의 구성으로 next-sitemap을 사용해 이들을 생성했으며, 각 빌드에서 재생성됩니다.
이 스택을 사용해야 할 때 (그리고 사용하지 말아야 할 때)
이 아키텍처 -- Vercel에서 Next.js + i18n + ISR -- 강력하지만 모든 것에 올바른 선택은 아닙니다.
다음과 같은 경우 이를 사용하십시오:
- 10개 이상의 로케일과 수천 개의 페이지가 있습니다
- 콘텐츠 업데이트는 자주 이루어지지만 실시간이 아닙니다
- 성능 및 Core Web Vitals가 SEO에 중요합니다
- 당신의 팀은 React/Next.js를 잘 알고 있습니다
다음과 같은 경우 대체를 고려하십시오:
- 5개 미만의 로케일과 1,000개 미만의 페이지가 있습니다 (일반 SSG가 더 간단할 수 있습니다)
- 콘텐츠가 진정으로 실시간입니다 (주식 거래, 라이브 점수) -- SSR 또는 클라이언트 측 가져오기를 사용합니다
- 호스팅 예산이 제약적입니다 -- 비용의 일부로 순수 다국어 사이트의 경우 Astro를 고려하십시오
- 당신의 팀이 작고 React의 상호성이 필요하지 않습니다 -- i18n이 있는 정적 사이트 생성기가 유지 관리할 사항이 적을 수 있습니다
이와 같은 프로젝트를 고려하는 팀을 위해, 우리는 여러 엔터프라이즈 클라이언트가 대규모 Next.js 애플리케이션을 아키텍처화하고 구축하는 것을 도왔습니다. 처음 2주의 아키텍처 결정이 프로젝트가 성공하는지 또는 유지 관리 악몽이 되는지를 결정합니다. 당신의 특정 상황을 논의하고 싶다면 연락처로 연락하세요.
FAQ
Next.js i18n 라우팅이 ISR과 어떻게 작동합니까?
Next.js i18n 라우팅은 URL에 로케일 접두사를 추가합니다 (예: /fr/products/shoes). ISR과 결합하면, 각 로케일 + 페이지 조합이 Vercel의 엣지에서 독립적으로 캐시됩니다. 그래서 /en/products/shoes와 /fr/products/shoes는 각각 자체 재검증 타이머가 있는 별도의 캐시 항목입니다. getStaticProps 함수는 로케일을 컨텍스트에서 받고, 당신은 거기서 적절한 번역 및 로컬라이즈된 콘텐츠를 가져옵니다.
Next.js ISR이 Vercel에서 처리할 수 있는 최대 페이지 수는 얼마입니까?
Vercel이 제공할 수 있는 ISR 페이지 수에는 기술적인 하드 제한이 없습니다. 우리는 91K+ 페이지를 성공적으로 실행했으며, 500K+ 페이지가 있는 프로젝트를 들었습니다. 실질적인 제한은 빌드 시간 (미리 생성된 페이지의 경우), 재검증 처리량 및 비용입니다. Vercel의 엣지 캐시는 이 규모용으로 설계되었습니다 -- 본질적으로 스마트 무효화가 있는 CDN입니다.
ISR이 다국어 사이트의 SEO에 영향을 줍니까?
아니요, ISR 페이지는 캐시에서 제공되는 경우 완전히 렌더링된 HTML이며, 이것이 검색 엔진 크롤러가 보는 것입니다. 핵심 SEO 고려 사항은 적절한 hreflang 태그, 로케일별 사이트맵이 있는 잘 구조화된 사이트맵 인덱스, 그리고 fallback: 'blocking' 설정이 크롤러가 불완전한 페이지를 보는 것을 방지하는지 확인하는 것입니다. Google은 ISR/캐시된 페이지가 전통적인 정적 HTML과 동일하게 취급된다고 확인했습니다.
재배포 없이 번역 업데이트를 어떻게 처리합니까?
CMS 관리 콘텐츠 (제품 설명, 마케팅 사본)의 경우, 번역은 ISR 재검증을 통해 자동으로 업데이트됩니다 -- 타이머 또는 온디맨드 재검증 웹훅을 통해 어느 쪽이든 가능합니다. UI 문자열 번역 (버튼 라벨, 양식 유효성 메시지)의 경우, 빌드 시간에 번들되므로 재배포가 필요합니다. 우리는 의도적으로 이들을 분리합니다: 콘텐츠 변경은 절대 배포를 요구해서는 안 되지만 UI 문자열 변경은 코드 검토를 거칩니다.
30개 로케일에 걸쳐 다른 날짜, 숫자 및 통화 형식을 어떻게 처리합니까?
우리는 next-intl의 형식 유틸리티를 통해 브라우저의 기본 Intl API를 사용합니다. 이것은 각 로케일에 대해 날짜 형식 (Intl.DateTimeFormat), 숫자 형식 (Intl.NumberFormat) 및 통화 표시를 올바르게 처리합니다. ICU 메시지 형식을 통해 이러한 형식화기를 번역 문자열에 직접 포함시킬 수 있습니다: "price": "From {amount, number, ::currency/EUR}". 이것은 ISR 생성 중 서버 측 및 동적 값에 대해 클라이언트 측에서 작동합니다.
대규모 i18n을 위해 App Router 또는 Pages Router를 사용해야 합니까?
Next.js 15 (2025년 중반)의 현재 기준으로, App Router의 i18n 이야기는 상당히 성숙했으며, next-intl v4는 탁월한 App Router 지원을 가지고 있습니다. 새로운 프로젝트의 경우, 나는 App Router를 추천할 것입니다. 더 나은 스트리밍, React 서버 컴포넌트 (클라이언트 측 JavaScript를 줄임) 및 더 세분화된 캐싱 제어를 제공합니다. 우리의 프로젝트는 Pages Router를 사용했습니다. 왜냐하면 App Router i18n이 덜 안정적이었을 때 2024년에 시작되었기 때문입니다만, 오늘날 그린필드 프로젝트는 App Router로 가야 합니다.
ISR 재검증이 실패하면 어떻게 됩니까? 사용자가 오류 페이지를 보입니까?
아니요, 그리고 이것이 ISR의 최고의 기능 중 하나입니다. 재검증이 실패하면 (아마도 CMS API가 다운되었거나 getStaticProps에 코드 오류가 있음), Vercel은 페이지의 마지막으로 성공적으로 생성된 버전을 계속 제공합니다. 사용자는 절대 오류를 보지 않습니다 -- 그들은 조금 진부한 콘텐츠를 보기만 합니다. 실패한 재검증이 기록되고 다음 재검증 시도는 다시 시도합니다. 이것은 ISR을 API 중단이 즉시 사용자 대면 중단이 되는 SSR에 비해 불가사의하게 복원력 있게 만듭니다.