@intlayer/docs 6.1.4 → 6.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/blog/ar/next-i18next_vs_next-intl_vs_intlayer.md +1135 -75
  2. package/blog/ar/nextjs-multilingual-seo-comparison.md +364 -0
  3. package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +1139 -72
  4. package/blog/de/nextjs-multilingual-seo-comparison.md +362 -0
  5. package/blog/en/next-i18next_vs_next-intl_vs_intlayer.md +224 -240
  6. package/blog/en/nextjs-multilingual-seo-comparison.md +360 -0
  7. package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +1134 -37
  8. package/blog/en-GB/nextjs-multilingual-seo-comparison.md +360 -0
  9. package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +1122 -64
  10. package/blog/es/nextjs-multilingual-seo-comparison.md +363 -0
  11. package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +1132 -75
  12. package/blog/fr/nextjs-multilingual-seo-comparison.md +362 -0
  13. package/blog/hi/nextjs-multilingual-seo-comparison.md +363 -0
  14. package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +1120 -55
  15. package/blog/it/nextjs-multilingual-seo-comparison.md +363 -0
  16. package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +1140 -76
  17. package/blog/ja/nextjs-multilingual-seo-comparison.md +362 -0
  18. package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +1129 -73
  19. package/blog/ko/nextjs-multilingual-seo-comparison.md +362 -0
  20. package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +1133 -76
  21. package/blog/pt/nextjs-multilingual-seo-comparison.md +362 -0
  22. package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +1142 -74
  23. package/blog/ru/nextjs-multilingual-seo-comparison.md +370 -0
  24. package/blog/tr/nextjs-multilingual-seo-comparison.md +362 -0
  25. package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +1142 -75
  26. package/blog/zh/nextjs-multilingual-seo-comparison.md +394 -0
  27. package/dist/cjs/generated/blog.entry.cjs +16 -0
  28. package/dist/cjs/generated/blog.entry.cjs.map +1 -1
  29. package/dist/esm/generated/blog.entry.mjs +16 -0
  30. package/dist/esm/generated/blog.entry.mjs.map +1 -1
  31. package/dist/types/generated/blog.entry.d.ts +1 -0
  32. package/dist/types/generated/blog.entry.d.ts.map +1 -1
  33. package/docs/en/interest_of_intlayer.md +2 -2
  34. package/package.json +10 -10
  35. package/src/generated/blog.entry.ts +16 -0
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  createdAt: 2025-08-23
3
- updatedAt: 2025-08-23
3
+ updatedAt: 2025-09-29
4
4
  title: next-i18next vs next-intl vs Intlayer
5
- description: Next.js 앱의 국제화(i18n)를 위해 next-i18next next-intl, Intlayer를 비교합니다
5
+ description: Next.js 앱의 국제화(i18n)를 위해 next-i18next, next-intl, Intlayer를 비교합니다.
6
6
  keywords:
7
7
  - next-intl
8
8
  - next-i18next
@@ -17,9 +17,12 @@ slugs:
17
17
  - next-i18next-vs-next-intl-vs-intlayer
18
18
  ---
19
19
 
20
- # next-i18next VS next-intl VS intlayer | Next.js 국제화(i18n)
20
+ # next-i18next VS next-intl VS intlayer | Next.js 국제화 (i18n)
21
+
22
+ Next.js를 위한 세 가지 i18n 옵션인 next-i18next, next-intl, Intlayer의 유사점과 차이점을 살펴보겠습니다.
23
+
24
+ 이 문서는 완전한 튜토리얼이 아니라 선택에 도움을 주기 위한 비교입니다.
21
25
 
22
- 이 가이드는 **Next.js**에서 널리 사용되는 세 가지 i18n 옵션인 **next-intl**, **next-i18next**, 그리고 **Intlayer**를 비교합니다.
23
26
  우리는 **Next.js 13+ App Router** (및 **React Server Components**)에 중점을 두고 다음 항목들을 평가합니다:
24
27
 
25
28
  1. **아키텍처 및 콘텐츠 구성**
@@ -30,133 +33,1186 @@ slugs:
30
33
  6. **개발자 경험(DX), 도구 및 유지보수**
31
34
  7. **SEO 및 대규모 프로젝트 확장성**
32
35
 
33
- > **요약**: 세 가지 모두 Next.js 앱을 현지화할 수 있습니다. 만약 **컴포넌트 범위 콘텐츠**, **엄격한 TypeScript 타입**, **빌드 타임 누락 키 검사**, **트리 쉐이킹된 사전**, 그리고 **최고급 App Router + SEO 도우미**를 원한다면, **Intlayer**가 가장 완전하고 현대적인 선택입니다.
36
+ > **요약**: 세 가지 모두 Next.js 앱을 현지화할 수 있습니다. 만약 **컴포넌트 범위 콘텐츠**, **엄격한 TypeScript 타입**, **빌드 누락 키 검사**, **트리 쉐이킹된 사전**, 그리고 **최고급 App Router + SEO 도우미**를 원한다면, **Intlayer**가 가장 완전하고 현대적인 선택입니다.
37
+
38
+ > 개발자들이 자주 혼동하는 점 중 하나는 `next-intl`이 `react-intl`의 Next.js 버전이라고 생각하는 것입니다. 그렇지 않습니다—`next-intl`은 [Amann](https://github.com/amannn)이 유지 관리하며, `react-intl`은 [FormatJS](https://github.com/formatjs/formatjs)가 유지 관리합니다.
34
39
 
35
40
  ---
36
41
 
37
- ## 상위 수준 포지셔닝
42
+ ## 요약
38
43
 
39
- - **next-intl** - 가볍고 직관적인 메시지 포맷팅을 제공하며 Next.js 지원이 탄탄합니다. 중앙 집중식 카탈로그가 일반적이며, 개발자 경험은 단순하지만 안정성과 대규모 유지보수는 주로 사용자의 책임입니다.
40
- - **next-i18next** - Next.js 환경에 맞춘 i18next입니다. 성숙한 생태계와 플러그인(예: ICU)을 통한 기능을 제공하지만, 설정이 장황할 수 있고 프로젝트가 커질수록 카탈로그가 중앙 집중화되는 경향이 있습니다.
41
- - **Intlayer** - Next.js를 위한 컴포넌트 중심 콘텐츠 모델로, **엄격한 TS 타입 지정**, **빌드 타임 검사**, **트리 쉐이킹**, **내장 미들웨어 및 SEO 도우미**, 선택적 **비주얼 에디터/CMS**, 그리고 **AI 지원 번역**을 제공합니다.
44
+ - **next-intl** - 가볍고 직관적인 메시지 포맷팅을 제공하며, 견고한 Next.js 지원을 갖추고 있습니다. 중앙 집중식 카탈로그가 일반적이며, 개발자 경험(DX)은 간단하지만 안전성과 대규모 유지 관리는 주로 사용자 책임입니다.
45
+ - **next-i18next** - Next.js 환경에 맞춘 i18next입니다. 성숙한 생태계와 플러그인(예: ICU)을 통한 기능을 제공하지만, 설정이 장황할 수 있으며 프로젝트가 커질수록 카탈로그가 중앙 집중화되는 경향이 있습니다.
46
+ - **Intlayer** - Next.js를 위한 컴포넌트 중심 콘텐츠 모델, **엄격한 TS 타이핑**, **빌드 검사**, **트리 쉐이킹**, **내장 미들웨어 및 SEO 도우미**, 선택적 **비주얼 에디터/CMS**, 그리고 **AI 지원 번역**을 제공합니다.
42
47
 
43
48
  ---
44
49
 
45
- ## 나란히 비교한 기능 비교 (Next.js 중심)
50
+ | Library | GitHub Stars | Total Commits | Last Commit | First Version | NPM Version | NPM Downloads |
51
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
52
+ | `aymericzip/intlayer` | [![GitHub Repo stars](https://img.shields.io/github/stars/aymericzip/intlayer?style=for-the-badge&label=%E2%AD%90%20stars)](https://github.com/aymericzip/intlayer/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/aymericzip/intlayer?style=for-the-badge&label=commits)](https://github.com/aymericzip/intlayer/commits) | [![Last Commit](https://img.shields.io/github/last-commit/aymericzip/intlayer?style=for-the-badge)](https://github.com/aymericzip/intlayer/commits) | April 2024 | [![npm](https://img.shields.io/npm/v/intlayer?style=for-the-badge)](https://www.npmjs.com/package/intlayer) | [![npm downloads](https://img.shields.io/npm/dm/intlayer?style=for-the-badge)](https://www.npmjs.com/package/intlayer) |
53
+ | `amannn/next-intl` | [![GitHub Repo stars](https://img.shields.io/github/stars/amannn/next-intl?style=for-the-badge&label=%E2%AD%90%20stars)](https://github.com/amannn/next-intl/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/amannn/next-intl?style=for-the-badge&label=commits)](https://github.com/amannn/next-intl/commits) | [![Last Commit](https://img.shields.io/github/last-commit/amannn/next-intl?style=for-the-badge)](https://github.com/amannn/next-intl/commits) | Nov 2020 | [![npm](https://img.shields.io/npm/v/next-intl?style=for-the-badge)](https://www.npmjs.com/package/next-intl) | [![npm downloads](https://img.shields.io/npm/dm/next-intl?style=for-the-badge)](https://www.npmjs.com/package/next-intl) |
54
+ | `i18next/i18next` | [![GitHub Repo stars](https://img.shields.io/github/stars/i18next/i18next?style=for-the-badge&label=%E2%AD%90%20stars)](https://github.com/i18next/i18next/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/i18next/i18next?style=for-the-badge&label=commits)](https://github.com/i18next/i18next/commits) | [![Last Commit](https://img.shields.io/github/last-commit/i18next/i18next?style=for-the-badge)](https://github.com/i18next/i18next/commits) | Jan 2012 | [![npm](https://img.shields.io/npm/v/i18next?style=for-the-badge)](https://www.npmjs.com/package/i18next) | [![npm downloads](https://img.shields.io/npm/dm/i18next?style=for-the-badge)](https://www.npmjs.com/package/i18next) |
55
+ | `i18next/next-i18next` | [![GitHub Repo stars](https://img.shields.io/github/stars/i18next/next-i18next?style=for-the-badge&label=%E2%AD%90%20stars)](https://github.com/i18next/next-i18next/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/i18next/next-i18next?style=for-the-badge&label=commits)](https://github.com/i18next/next-i18next/commits) | [![Last Commit](https://img.shields.io/github/last-commit/i18next/next-i18next?style=for-the-badge)](https://github.com/i18next/next-i18next/commits) | Nov 2018 | [![npm](https://img.shields.io/npm/v/next-i18next?style=for-the-badge)](https://www.npmjs.com/package/next-i18next) | [![npm downloads](https://img.shields.io/npm/dm/next-i18next?style=for-the-badge)](https://www.npmjs.com/package/next-i18next) |
46
56
 
47
- | 기능 | `next-intlayer` (Intlayer) | `next-intl` | `next-i18next` |
48
- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
49
- | **컴포넌트 근처 번역** | ✅ 예, 각 컴포넌트와 함께 콘텐츠가 배치됨 | ❌ 아니요 | ❌ 아니요 |
50
- | **TypeScript 통합** | ✅ 고급, 자동 생성된 엄격한 타입 | ✅ 우수 | ⚠️ 기본 |
51
- | **누락된 번역 감지** | ✅ TypeScript 오류 하이라이트 및 빌드 시 오류/경고 | ⚠️ 런타임 폴백 | ⚠️ 런타임 폴백 |
52
- | **리치 콘텐츠 (JSX/Markdown/컴포넌트)** | ✅ 직접 지원 | ❌ 리치 노드용으로 설계되지 않음 | ⚠️ 제한적 지원 |
53
- | **AI 기반 번역** | ✅ 예, 여러 AI 제공자를 지원합니다. 자체 API 키를 사용하여 사용할 수 있습니다. 애플리케이션 및 콘텐츠 범위의 컨텍스트를 고려합니다. | ❌ 아니요 | ❌ 아니요 |
54
- | **비주얼 에디터** | ✅ 예, 로컬 비주얼 에디터 + 선택적 CMS; 코드베이스 콘텐츠를 외부화할 수 있으며, 임베드 가능 | ❌ 아니요 / 외부 현지화 플랫폼을 통해 사용 가능 | ❌ 아니요 / 외부 현지화 플랫폼을 통해 사용 가능 |
55
- | **로컬라이즈된 라우팅** | ✅ 예, 기본적으로 로컬라이즈된 경로를 지원합니다 (Next.js 및 Vite와 함께 작동) | ✅ 내장, App Router가 `[locale]` 세그먼트를 지원합니다 | ✅ 내장 |
56
- | **동적 라우트 생성** | ✅ 예 | ✅ 예 | ✅ 예 |
57
- | **복수형 처리** | ✅ 열거형 기반 패턴 | ✅ 우수함 | ✅ 우수함 |
58
- | **형식 지정 (날짜, 숫자, 통화)** | ✅ 최적화된 포매터 (내부적으로 Intl 사용) | ✅ 우수함 (Intl 헬퍼) | ✅ 우수함 (Intl 헬퍼) |
59
- | **콘텐츠 형식** | ✅ .tsx, .ts, .js, .json, .md, .txt, (.yaml 작업 중) | ✅ .json, .js, .ts | ⚠️ .json |
60
- | **ICU 지원** | ⚠️ 작업 중 | ✅ 예 | ⚠️ 플러그인 통해 (`i18next-icu`) |
61
- | **SEO 도우미 (hreflang, 사이트맵)** | ✅ 내장 도구: 사이트맵, robots.txt, 메타데이터 도우미 | ✅ 우수 | ✅ 우수 |
62
- | **생태계 / 커뮤니티** | ⚠️ 규모는 작지만 빠르게 성장하고 반응성이 좋음 | ✅ 중간 규모, Next.js 중심 | ✅ 중간 규모, Next.js 중심 |
63
- | **서버 사이드 렌더링 및 서버 컴포넌트** | ✅ 예, SSR 및 React 서버 컴포넌트에 최적화됨 | ⚠️ 페이지 단위로 지원되나 자식 서버 컴포넌트에 t-함수를 컴포넌트 트리를 통해 전달해야 함 | ⚠️ 페이지 단위로 지원되나 자식 서버 컴포넌트에 t-함수를 컴포넌트 트리를 통해 전달해야 함 |
64
- | **트리 쉐이킹 (사용된 콘텐츠만 로드)** | ✅ 예, Babel/SWC 플러그인을 통한 빌드 시 컴포넌트별 적용 | ⚠️ 부분적 지원 | ⚠️ 부분적 지원 |
65
- | **지연 로딩 (Lazy loading)** | ✅ 예, 로케일별 / 사전별 | ✅ 예 (경로별/로케일별), 네임스페이스 관리 필요 | ✅ 예 (경로별/로케일별), 네임스페이스 관리 필요 |
66
- | **사용하지 않는 콘텐츠 정리 (Purge unused content)** | ✅ 예, 빌드 시 사전별 | ❌ 아니요, 네임스페이스 관리를 통해 수동으로 관리 가능 | ❌ 아니요, 네임스페이스 관리를 통해 수동으로 관리 가능 |
67
- | **대규모 프로젝트 관리** | ✅ 모듈화 권장, 디자인 시스템에 적합 | ✅ 설정과 함께 모듈화 | ✅ 설정과 함께 모듈화 |
57
+ > 배지는 자동으로 업데이트됩니다. 스냅샷은 시간이 지남에 따라 달라질 수 있습니다.
68
58
 
69
59
  ---
70
60
 
71
- ## 심층 비교
61
+ ## 나란히 기능 비교 (Next.js 중심)
72
62
 
73
- ### 1) 아키텍처 및 확장성
63
+ | 기능 | `next-intlayer` (Intlayer) | `next-intl` | `next-i18next` |
64
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
65
+ | **컴포넌트 근처 번역** | ✅ 예, 각 컴포넌트와 내용이 함께 위치 | ❌ 아니요 | ❌ 아니요 |
66
+ | **TypeScript 통합** | ✅ 고급, 자동 생성 엄격한 타입 | ✅ 좋음 | ⚠️ 기본 |
67
+ | **번역 누락 감지** | ✅ TypeScript 오류 하이라이트 및 빌드 타임 오류/경고 | ⚠️ 런타임 폴백 | ⚠️ 런타임 폴백 |
68
+ | **풍부한 콘텐츠 (JSX/Markdown/컴포넌트)** | ✅ 직접 지원 | ❌ 풍부한 노드를 위해 설계되지 않음 | ⚠️ 제한적 |
69
+ | **AI 기반 번역** | ✅ 예, 여러 AI 제공자를 지원합니다. 자체 API 키를 사용하여 이용 가능하며, 애플리케이션과 콘텐츠 범위의 맥락을 고려합니다 | ❌ 아니요 | ❌ 아니요 |
70
+ | **비주얼 에디터** | ✅ 예, 로컬 비주얼 에디터 + 선택적 CMS; 코드베이스 콘텐츠 외부화 가능; 임베드 가능 | ❌ 아니요 / 외부 현지화 플랫폼을 통해 사용 가능 | ❌ 아니요 / 외부 현지화 플랫폼을 통해 사용 가능 |
71
+ | **현지화된 라우팅** | ✅ 예, 기본적으로 현지화된 경로 지원 (Next.js 및 Vite와 호환) | ✅ 내장, App Router가 `[locale]` 세그먼트 지원 | ✅ 내장 |
72
+ | **동적 라우트 생성** | ✅ 예 | ✅ 예 | ✅ 예 |
73
+ | **복수형 처리** | ✅ 열거형 기반 패턴 | ✅ 우수 | ✅ 우수 |
74
+ | **형식 지정 (날짜, 숫자, 통화)** | ✅ 최적화된 포매터 (내부적으로 Intl 사용) | ✅ 우수함 (Intl 헬퍼) | ✅ 우수함 (Intl 헬퍼) |
75
+ | **콘텐츠 형식** | ✅ .tsx, .ts, .js, .json, .md, .txt, (.yaml 작업 중) | ✅ .json, .js, .ts | ⚠️ .json |
76
+ | **ICU 지원** | ⚠️ 작업 중 | ✅ 예 | ⚠️ 플러그인 통해 (`i18next-icu`) |
77
+ | **SEO 도우미 (hreflang, 사이트맵)** | ✅ 내장 도구: 사이트맵, robots.txt, 메타데이터 도우미 | ✅ 우수 | ✅ 우수 |
78
+ | **에코시스템 / 커뮤니티** | ⚠️ 작지만 빠르게 성장하고 반응성이 좋음 | ✅ 좋음 | ✅ 좋음 |
79
+ | **서버 사이드 렌더링 & 서버 컴포넌트** | ✅ 예, SSR / React 서버 컴포넌트에 최적화됨 | ⚠️ 페이지 단위로 지원되나 자식 서버 컴포넌트에 t-함수를 컴포넌트 트리를 통해 전달해야 함 | ⚠️ 페이지 단위로 지원되나 자식 서버 컴포넌트에 t-함수를 컴포넌트 트리를 통해 전달해야 함 |
80
+ | **트리 쉐이킹 (사용된 콘텐츠만 로드)** | ✅ 예, Babel/SWC 플러그인을 통한 빌드 시 컴포넌트별 | ⚠️ 부분적 지원 | ⚠️ 부분적 지원 |
81
+ | **지연 로딩** | ✅ 예, 로케일별 / 사전별 | ✅ 예 (경로별/로케일별), 네임스페이스 관리 필요 | ✅ 예 (경로별/로케일별), 네임스페이스 관리 필요 |
82
+ | **사용하지 않는 콘텐츠 정리** | ✅ 예, 빌드 시 사전별로 | ❌ 아니요, 네임스페이스 관리를 통해 수동으로 관리 가능 | ❌ 아니요, 네임스페이스 관리를 통해 수동으로 관리 가능 |
83
+ | **대규모 프로젝트 관리** | ✅ 모듈화 권장, 디자인 시스템에 적합 | ✅ 설정과 함께 모듈화 | ✅ 설정과 함께 모듈화 |
84
+ | **누락된 번역 테스트 (CLI/CI)** | ✅ CLI: `npx intlayer content test` (CI 친화적 감사) | ⚠️ 내장되어 있지 않음; 문서에서는 `npx @lingual/i18n-check` 사용 권장 | ⚠️ 내장되어 있지 않음; i18next 도구 또는 런타임 `saveMissing`에 의존 |
85
+
86
+ ---
74
87
 
75
- - **next-intl / next-i18next**: 기본적으로 로케일별 **중앙 집중식 카탈로그** (그리고 i18next의 **네임스페이스**)를 사용합니다. 초기에는 잘 작동하지만, 시간이 지남에 따라 결합도가 높아지고 키 변경이 빈번해지면서 큰 공유 영역이 되는 경우가 많습니다.
76
- - **Intlayer**: 코드와 함께 **컴포넌트별**(또는 기능별) 사전을 **같은 위치에 배치**하도록 권장합니다. 이는 인지 부하를 줄이고, UI 조각의 복제/이동을 용이하게 하며, 팀 간 충돌을 줄여줍니다. 사용하지 않는 콘텐츠는 자연스럽게 쉽게 발견되고 제거할 수 있습니다.
88
+ ## 소개
77
89
 
78
- **중요한 이유:** 대규모 코드베이스나 디자인 시스템 환경에서는 **모듈화된 콘텐츠**가 단일 카탈로그보다 확장됩니다.
90
+ Next.js는 국제화된 라우팅(예: 로케일 세그먼트)을 기본적으로 지원합니다. 하지만 기능만으로는 번역을 수행하지 않습니다. 사용자에게 현지화된 콘텐츠를 렌더링하려면 여전히 라이브러리가 필요합니다.
91
+
92
+ 많은 i18n 라이브러리가 존재하지만, 현재 Next.js 환경에서는 next-i18next, next-intl, 그리고 Intlayer 세 가지가 주목받고 있습니다.
79
93
 
80
94
  ---
81
95
 
82
- ### 2) TypeScript 안정성
96
+ ## 아키텍처확장성
83
97
 
84
- - **next-intl**: 견고한 TypeScript 지원을 제공하지만, **키가 기본적으로 엄격하게 타입 지정되지 않으므로** 안전성 패턴을 수동으로 유지해야 합니다.
85
- - **next-i18next**: 훅에 대한 기본 타입 정의를 제공하지만, **엄격한 타입 지정은 추가 도구/구성이 필요합니다**.
86
- - **Intlayer**: 콘텐츠에서 **엄격한 타입을 생성**합니다. **IDE 자동완성**과 **컴파일 타임 오류**가 배포 전에 오타와 누락된 키를 잡아냅니다.
98
+ - **next-intl / next-i18next**: 기본적으로 로케일별 **중앙 집중식 카탈로그**(그리고 i18next의 **네임스페이스**)를 사용합니다. 초기에는 작동하지만, 점차 결합도가 높아지고 키 변경이 잦아지면서 큰 공유 영역이 되는 경우가 많습니다.
99
+ - **Intlayer**: 코드와 **공간적으로 함께 위치한** 컴포넌트별(또는 기능별) 사전을 권장합니다. 이는 인지 부하를 줄이고, UI 조각의 복제/이전을 쉽게 하며, 팀 간 충돌을 줄여줍니다. 사용하지 않는 콘텐츠는 자연스럽게 더 쉽게 발견하고 제거할 수 있습니다.
87
100
 
88
- **중요한 이유:** 강력한 타입 지정은 실패를 **오른쪽(런타임)**이 아닌 **왼쪽(CI/빌드)**으로 이동시킵니다.
101
+ **중요한 이유:** 대규모 코드베이스나 디자인 시스템 설정에서는 **모듈화된 콘텐츠**가 단일 카탈로그보다 더 잘 확장됩니다.
89
102
 
90
103
  ---
91
104
 
92
- ### 3) 누락된 번역 처리
105
+ ## 번들 크기 의존성
106
+
107
+ 애플리케이션을 빌드한 후, 번들은 브라우저가 페이지를 렌더링하기 위해 로드하는 자바스크립트입니다. 따라서 번들 크기는 애플리케이션 성능에 매우 중요합니다.
108
+
109
+ 다국어 애플리케이션 번들 맥락에서 중요한 두 가지 구성 요소는 다음과 같습니다:
110
+
111
+ - 애플리케이션 코드
112
+ - 브라우저가 로드하는 콘텐츠
113
+
114
+ ## 애플리케이션 코드
115
+
116
+ 이 경우 애플리케이션 코드의 중요성은 미미합니다. 세 가지 솔루션 모두 트리 쉐이킹(tree-shakable)이 가능하여, 사용하지 않는 코드 부분은 번들에 포함되지 않습니다.
117
+
118
+ 다음은 세 가지 솔루션을 사용한 다국어 애플리케이션에서 브라우저가 로드하는 자바스크립트 번들 크기 비교입니다.
119
+
120
+ 애플리케이션에서 포매터가 필요하지 않은 경우, 트리 쉐이킹 후 내보내지는 함수 목록은 다음과 같습니다:
121
+
122
+ - **next-intlayer**: `useIntlayer`, `useLocale`, `NextIntlClientProvider`, (번들 크기 180.6 kB -> 78.6 kB (gzip))
123
+ - **next-intl**: `useTranslations`, `useLocale`, `NextIntlClientProvider`, (번들 크기 101.3 kB -> 31.4 kB (gzip))
124
+ - **next-i18next**: `useTranslation`, `useI18n`, `I18nextProvider`, (번들 크기 80.7 kB -> 25.5 kB (gzip))
125
+
126
+ 이 함수들은 React 컨텍스트/상태를 감싸는 래퍼에 불과하므로, i18n 라이브러리가 번들 크기에 미치는 전체 영향은 미미합니다.
127
+
128
+ > Intlayer는 `useIntlayer` 함수에 더 많은 로직이 포함되어 있어 `next-intl` 및 `next-i18next`보다 약간 더 큽니다. 이는 마크다운과 `intlayer-editor` 통합과 관련이 있습니다.
129
+
130
+ ## 콘텐츠 및 번역
131
+
132
+ 이 부분은 개발자들이 종종 무시하지만, 10개의 페이지와 10개의 언어로 구성된 애플리케이션의 경우를 생각해 봅시다. 계산을 단순화하기 위해 각 페이지가 100% 고유한 콘텐츠를 통합한다고 가정해 보겠습니다(실제로는 페이지 간에 중복되는 콘텐츠가 많습니다. 예: 페이지 제목, 헤더, 푸터 등).
133
+
134
+ `/fr/about` 페이지를 방문하려는 사용자는 특정 언어로 된 한 페이지의 콘텐츠를 로드하게 됩니다. 콘텐츠 최적화를 무시하면 애플리케이션 콘텐츠의 8,200% `((1 + (((10 페이지 - 1) × (10 언어 - 1)))) × 100)`를 불필요하게 로드하는 셈입니다. 문제를 이해하셨나요? 이 콘텐츠가 텍스트로만 남아 있더라도, 아마 사이트의 이미지 최적화를 더 신경 쓰고 싶겠지만, 전 세계에 불필요한 콘텐츠를 전송하고 사용자의 컴퓨터가 아무 쓸모 없이 이를 처리하게 만드는 것입니다.
135
+
136
+ 두 가지 중요한 문제:
137
+
138
+ - **라우트별 분할:**
139
+
140
+ > 내가 `/about` 페이지에 있다면, `/home` 페이지의 콘텐츠를 로드하고 싶지 않습니다.
141
+
142
+ - **로케일별 분할:**
143
+
144
+ > 내가 `/fr/about` 페이지에 있다면, `/en/about` 페이지의 콘텐츠를 로드하고 싶지 않습니다.
145
+
146
+ 다시 말하지만, 세 가지 솔루션 모두 이러한 문제를 인지하고 있으며 이러한 최적화를 관리할 수 있도록 합니다. 세 솔루션 간의 차이는 DX(개발자 경험)에 있습니다.
147
+
148
+ `next-intl`과 `next-i18next`는 중앙 집중식 접근 방식을 사용하여 번역을 관리하며, 로케일별 및 하위 파일별로 JSON을 분할할 수 있습니다. `next-i18next`에서는 JSON 파일을 '네임스페이스(namespaces)'라고 부르고, `next-intl`은 메시지를 선언할 수 있게 합니다. `intlayer`에서는 JSON 파일을 '사전(dictionaries)'이라고 부릅니다.
149
+
150
+ - `next-intl`의 경우, `next-i18next`와 마찬가지로, 콘텐츠가 페이지/레이아웃 수준에서 로드된 후 이 콘텐츠가 컨텍스트 프로바이더에 로드됩니다. 이는 개발자가 각 페이지에 로드될 JSON 파일을 수동으로 관리해야 함을 의미합니다.
151
+
152
+ > 실제로는 개발자들이 이 최적화를 종종 건너뛰고, 단순성을 위해 페이지의 컨텍스트 프로바이더에 모든 콘텐츠를 로드하는 방식을 선호합니다.
153
+
154
+ - `intlayer`의 경우, 모든 콘텐츠가 애플리케이션 내에서 로드됩니다. 그런 다음 플러그인(`@intlayer/babel` / `@intlayer/swc`)이 페이지에서 사용되는 콘텐츠만 로드하도록 번들을 최적화합니다. 따라서 개발자는 로드될 사전을 수동으로 관리할 필요가 없습니다. 이는 더 나은 최적화, 더 나은 유지보수성, 그리고 개발 시간 단축을 가능하게 합니다.
155
+
156
+ 애플리케이션이 커질수록(특히 여러 개발자가 함께 작업할 때) 더 이상 사용되지 않는 콘텐츠를 JSON 파일에서 제거하는 것을 잊는 경우가 흔합니다.
157
+
158
+ > 모든 경우에 JSON이 모두 로드된다는 점에 유의하세요 (next-intl, next-i18next, intlayer).
159
+
160
+ 이것이 Intlayer의 접근 방식이 더 성능이 좋은 이유입니다: 컴포넌트가 더 이상 사용되지 않으면 해당 사전은 번들에 로드되지 않습니다.
161
+
162
+ 라이브러리가 폴백(fallback)을 처리하는 방식도 중요합니다. 애플리케이션이 기본적으로 영어로 설정되어 있고 사용자가 `/fr/about` 페이지를 방문한다고 가정해 봅시다. 프랑스어 번역이 없는 경우 영어 폴백을 고려합니다.
93
163
 
94
- - **next-intl / next-i18next**: **런타임 폴백**에 의존합니다(예: 또는 기본 로케일 표시). 빌드는 실패하지 않습니다.
95
- - **Intlayer**: 누락된 로케일이나 키에 대해 **빌드 타임 감지**와 **경고/오류**를 제공합니다.
164
+ `next-intl` `next-i18next`의 경우, 라이브러리는 현재 로케일과 폴백 로케일에 관련된 JSON을 모두 로드해야 합니다. 따라서 모든 콘텐츠가 번역되었다고 가정하면, 각 페이지는 100% 불필요한 콘텐츠를 로드하게 됩니다. **반면에, `intlayer`는 사전 빌드 시점에 폴백을 처리합니다. 따라서 각 페이지는 사용된 콘텐츠만 로드합니다.**
96
165
 
97
- **중요한 이유:** 빌드 중에 누락을 잡으면 프로덕션에서 “미스터리 문자열”이 발생하는 것을 방지하고 엄격한 릴리스 게이트와 일치합니다.
166
+ 다음은 vite + react 애플리케이션에서 `intlayer`를 사용한 번들 크기 최적화의 영향 예시입니다:
167
+
168
+ | 최적화된 번들 | 최적화되지 않은 번들 |
169
+ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
170
+ | ![최적화된 번들](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle.png) | ![최적화되지 않은 번들](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png) |
98
171
 
99
172
  ---
100
173
 
101
- ### 4) 라우팅, 미들웨어 URL 전략
174
+ ## TypeScript안전성
175
+
176
+ <Columns>
177
+ <Column>
178
+
179
+ **next-intl**
180
+
181
+ - 견고한 TypeScript 지원을 제공하지만, **키는 기본적으로 엄격하게 타입 지정되지 않음**; 안전성 패턴을 수동으로 유지해야 합니다.
182
+
183
+ </Column>
184
+ <Column>
185
+
186
+ **next-i18next**
187
+
188
+ - 훅에 대한 기본 타입 정의 제공; **엄격한 키 타입 지정은 추가 도구/구성이 필요**합니다.
189
+
190
+ </Column>
191
+ <Column>
192
+
193
+ **intlayer**
102
194
 
103
- - 가지 모두 App Router에서 **Next.js 지역화 라우팅**을 지원합니다.
104
- - **Intlayer**는 **i18n 미들웨어**(헤더/쿠키를 통한 로케일 감지)와 **로컬라이즈된 URL 및 `<link rel="alternate" hreflang="…">` 태그 생성을 위한 헬퍼**를 제공하여 한 단계 더 나아갑니다.
195
+ - **콘텐츠에서 엄격한 타입을 생성합니다.** **IDE 자동완성**과 **컴파일 타임 오류**가 배포 전에 오타와 누락된 키를 잡아냅니다.
105
196
 
106
- **중요한 이유:** 커스텀 연결 계층이 줄어들고, **일관된 사용자 경험(UX)**과 **깔끔한 SEO**가 로케일 전반에 걸쳐 유지됩니다.
197
+ </Column>
198
+ </Columns>
199
+
200
+ **중요한 이유:** 강력한 타입 지정은 실패를 **오른쪽(런타임)**이 아닌 **왼쪽(CI/빌드)**으로 이동시킵니다.
107
201
 
108
202
  ---
109
203
 
110
- ### 5) 서버 컴포넌트(RSC) 정렬
204
+ ## 누락된 번역 처리
205
+
206
+ **next-intl**
207
+
208
+ - **런타임 폴백**에 의존합니다(예: 키 또는 기본 로케일 표시). 빌드는 실패하지 않습니다.
111
209
 
112
- - **모두** Next.js 13+를 지원합니다.
113
- - **Intlayer**는 RSC를 위해 설계된 일관된 API와 프로바이더를 통해 **서버/클라이언트 경계**를 매끄럽게 처리하여, 포매터나 t-함수를 컴포넌트 트리를 통해 전달할 필요가 없습니다.
210
+ **next-i18next**
114
211
 
115
- **중요한 이유:** 깔끔한 정신 모델과 하이브리드 트리에서 발생하는 예외 상황이 줄어듭니다.
212
+ - **런타임 폴백**에 의존합니다(예: 또는 기본 로케일 표시). 빌드는 실패하지 않습니다.
213
+
214
+ **intlayer**
215
+
216
+ - 누락된 로케일이나 키에 대해 **빌드 타임 감지**와 **경고/오류**를 제공합니다.
217
+
218
+ **중요한 이유:** 빌드 중에 누락을 잡으면 프로덕션에서 “미스터리 문자열”을 방지하고 엄격한 릴리스 게이트와 일치합니다.
116
219
 
117
220
  ---
118
221
 
119
- ### 6) 성능로딩 동작
222
+ ## 라우팅, 미들웨어URL 전략
223
+
224
+ <Columns>
225
+ <Column>
120
226
 
121
- - **next-intl / next-i18next**: **네임스페이스**와 **라우트 수준 분할**을 통해 부분적인 제어가 가능하지만, 규율이 느슨해지면 사용하지 않는 문자열이 번들에 포함될 위험이 있습니다.
122
- - **Intlayer**: 빌드 시 **트리 쉐이킹**을 수행하고, 사전/로케일별로 **지연 로딩**합니다. 사용하지 않는 콘텐츠는 포함되지 않습니다.
227
+ **next-intl**
123
228
 
124
- **중요한 이유:** 특히 다국어 사이트에서 작은 번들 크기와 더 빠른 시작 속도를 제공합니다.
229
+ - App Router에서 **Next.js 지역화 라우팅**과 함께 작동합니다.
230
+
231
+ </Column>
232
+ <Column>
233
+
234
+ **next-i18next**
235
+
236
+ - App Router에서 **Next.js 지역화 라우팅**과 함께 작동합니다.
237
+
238
+ </Column>
239
+ <Column>
240
+
241
+ **intlayer**
242
+
243
+ - 위의 모든 기능에 더해, **i18n 미들웨어**(헤더/쿠키를 통한 로케일 감지)와 지역화된 URL 및 `<link rel="alternate" hreflang="…">` 태그 생성을 위한 **도우미**를 제공합니다.
244
+
245
+ </Column>
246
+ </Columns>
247
+
248
+ **중요한 이유:** 커스텀 연결 계층이 줄어들고, **일관된 사용자 경험(UX)**과 **깔끔한 SEO**를 지역별로 유지할 수 있습니다.
125
249
 
126
250
  ---
127
251
 
128
- ### 7) 개발자 경험(DX), 도구 및 유지보수
252
+ ## 서버 컴포넌트(RSC) 정렬
253
+
254
+ <Columns>
255
+ <Column>
256
+
257
+ **next-intl**
258
+
259
+ - Next.js 13+를 지원합니다. 하이브리드 설정에서 컴포넌트 트리를 통해 t-함수/포매터를 전달해야 하는 경우가 많습니다.
260
+
261
+ </Column>
262
+ <Column>
263
+
264
+ **next-i18next**
265
+
266
+ - Next.js 13+를 지원합니다. 경계 간에 번역 유틸리티를 전달하는 데 유사한 제약이 있습니다.
267
+
268
+ </Column>
269
+ <Column>
270
+
271
+ **intlayer**
272
+
273
+ - Next.js 13+를 지원하며, 일관된 API와 RSC 지향 프로바이더를 통해 **서버/클라이언트 경계**를 원활하게 처리하여 포매터나 t-함수를 전달하는 것을 방지합니다.
274
+
275
+ </Column>
276
+ </Columns>
277
+
278
+ **중요한 이유:** 더 깔끔한 정신 모델과 하이브리드 트리에서의 예외 상황 감소.
279
+
280
+ ---
281
+
282
+ ## 개발자 경험(DX), 도구 및 유지보수
283
+
284
+ <Columns>
285
+ <Column>
286
+
287
+ **next-intl**
288
+
289
+ - 외부 지역화 플랫폼 및 편집 워크플로우와 자주 함께 사용됩니다.
290
+
291
+ </Column>
292
+ <Column>
293
+
294
+ **next-i18next**
295
+
296
+ - 외부 지역화 플랫폼 및 편집 워크플로우와 자주 함께 사용됩니다.
297
+
298
+ </Column>
299
+ <Column>
300
+
301
+ **intlayer**
302
+
303
+ - **무료 비주얼 에디터**와 **선택적 CMS**(Git 친화적이거나 외부화 가능)를 제공하며, **VSCode 확장**과 자체 제공자 키를 사용하는 **AI 지원 번역**도 포함됩니다.
304
+
305
+ </Column>
306
+ </Columns>
307
+
308
+ **중요한 이유:** 운영 비용을 낮추고 개발자와 콘텐츠 작성자 간의 피드백 주기를 단축합니다.
309
+
310
+ ## 현지화 플랫폼(TMS)과의 통합
311
+
312
+ 대규모 조직은 종종 **Crowdin**, **Phrase**, **Lokalise**, **Localizely**, 또는 **Localazy**와 같은 번역 관리 시스템(TMS)에 의존합니다.
313
+
314
+ - **기업이 관심을 가지는 이유**
315
+ - **협업 및 역할 분담**: 개발자, 제품 관리자, 번역가, 검토자, 마케팅 팀 등 여러 이해관계자가 참여합니다.
316
+ - **규모 및 효율성**: 지속적인 현지화와 맥락 내 리뷰가 가능합니다.
317
+
318
+ - **next-intl / next-i18next**
319
+ - 일반적으로 **중앙 집중식 JSON 카탈로그**를 사용하므로 TMS와의 내보내기/가져오기가 간단합니다.
320
+ - 위 플랫폼들에 대한 성숙한 생태계와 예제/통합 기능이 존재합니다.
321
+
322
+ - **Intlayer**
323
+ - **분산형, 컴포넌트별 사전**을 권장하며 **TypeScript/TSX/JS/JSON/MD** 콘텐츠를 지원합니다.
324
+ - 이는 코드의 모듈성을 향상시키지만, 도구가 중앙 집중식 평면 JSON 파일을 기대할 경우 플러그 앤 플레이 방식의 TMS 통합을 어렵게 만들 수 있습니다.
325
+ - Intlayer는 대안으로 **AI 지원 번역**(자체 제공자 키 사용), **비주얼 에디터/CMS**, 그리고 **CLI/CI** 워크플로우를 제공하여 누락된 부분을 찾아 채울 수 있도록 합니다.
326
+
327
+ > 참고: `next-intl`과 `i18next`도 TypeScript 카탈로그를 지원합니다. 만약 팀에서 메시지를 `.ts` 파일에 저장하거나 기능별로 분산 관리한다면, 유사한 TMS 마찰을 겪을 수 있습니다. 하지만 많은 `next-intl` 설정은 여전히 `locales/` 폴더에 중앙 집중화되어 있어 TMS용 JSON으로 리팩토링하기가 조금 더 쉽습니다.
328
+
329
+ ## 개발자 경험
330
+
331
+ 이 부분에서는 세 가지 솔루션을 깊이 비교합니다. 각 솔루션의 '시작하기' 문서에 설명된 간단한 사례를 고려하기보다는, 실제 프로젝트와 더 유사한 실제 사용 사례를 고려할 것입니다.
332
+
333
+ ### 앱 구조
334
+
335
+ 앱 구조는 코드베이스의 유지보수성을 보장하는 데 중요합니다.
336
+
337
+ <Tab defaultTab="next-intl" group='techno'>
338
+
339
+ <TabItem label="next-i18next" value="next-i18next">
340
+
341
+ ```bash
342
+ .
343
+ ├── public
344
+ │ └── locales
345
+ │ ├── en
346
+ │ │ ├── home.json
347
+ │ │ └── navbar.json
348
+ │ ├── fr
349
+ │ │ ├── home.json
350
+ │ │ └── navbar.json
351
+ │ └── es
352
+ │ ├── home.json
353
+ │ └── navbar.json
354
+ ├── next-i18next.config.js
355
+ └── src
356
+ ├── middleware.ts
357
+ ├── app
358
+ │ └── home.tsx
359
+ └── components
360
+ └── Navbar
361
+ └── index.tsx
362
+ ```
363
+
364
+ </TabItem>
365
+ <TabItem label="next-intl" value="next-intl">
366
+
367
+ ```bash
368
+ .
369
+ ├── locales
370
+ │ ├── en
371
+ │ │ ├── home.json
372
+ │ │ └── navbar.json
373
+ │ ├── fr
374
+ │ │ ├── home.json
375
+ │ │ └── navbar.json
376
+ │ └── es
377
+ │ ├── home.json
378
+ │ └── navbar.json
379
+ ├── i18n.ts
380
+ └── src
381
+ ├── middleware.ts
382
+ ├── app
383
+ │ └── home.tsx
384
+ └── components
385
+ └── Navbar
386
+ └── index.tsx
387
+ ```
388
+
389
+ </TabItem>
390
+ <TabItem label="intlayer" value="intlayer">
391
+
392
+ ```bash
393
+ .
394
+ ├── intlayer.config.ts
395
+ └── src
396
+ ├── middleware.ts
397
+ ├── app
398
+ │ └── home
399
+ │ └── index.tsx
400
+ │ └── index.content.ts
401
+ └── components
402
+ └── Navbar
403
+ ├── index.tsx
404
+ └── index.content.ts
405
+ ```
406
+
407
+ </TabItem>
408
+ </Tab>
409
+
410
+ #### 비교
411
+
412
+ - **next-intl / next-i18next**: 중앙 집중식 카탈로그(JSON; 네임스페이스/메시지). 명확한 구조, 번역 플랫폼과 잘 통합되지만, 앱이 커질수록 여러 파일을 동시에 수정해야 할 가능성이 높아집니다.
413
+ - **Intlayer**: 컴포넌트별 `.content.{ts|js|json}` 사전을 컴포넌트와 함께 배치. 컴포넌트 재사용과 로컬 단위 이해가 용이하며, 파일이 추가되고 빌드 타임 도구에 의존합니다.
414
+
415
+ #### 설정 및 콘텐츠 로딩
416
+
417
+ 앞서 언급했듯이, 각 JSON 파일이 코드에 어떻게 임포트되는지 최적화해야 합니다.
418
+ 라이브러리가 콘텐츠 로딩을 처리하는 방식이 중요합니다.
419
+
420
+ <Tab defaultTab="next-intl" group='techno'>
421
+ <TabItem label="next-i18next" value="next-i18next">
422
+
423
+ ```tsx fileName="next-i18next.config.js"
424
+ module.exports = {
425
+ i18n: {
426
+ locales: ["en", "fr", "es"],
427
+ defaultLocale: "en",
428
+ },
429
+ };
430
+ ```
431
+
432
+ ```tsx fileName="src/app/_app.tsx"
433
+ import { appWithTranslation } from "next-i18next";
434
+
435
+ const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;
436
+
437
+ export default appWithTranslation(MyApp);
438
+ ```
439
+
440
+ ```tsx fileName="src/app/[locale]/about/page.tsx"
441
+ import type { GetStaticProps } from "next";
442
+ import { serverSideTranslations } from "next-i18next/serverSideTranslations";
443
+ import { useTranslation } from "next-i18next";
444
+ import { I18nextProvider, initReactI18next } from "react-i18next";
445
+ import { createInstance } from "i18next";
446
+ import { ClientComponent, ServerComponent } from "@components";
447
+
448
+ export default function HomePage({ locale }: { locale: string }) {
449
+ // 이 컴포넌트에서 사용하는 네임스페이스를 명시적으로 선언합니다
450
+ const resources = await loadMessagesFor(locale); // 로더 (JSON 등)
451
+
452
+ const i18n = createInstance();
453
+ i18n.use(initReactI18next).init({
454
+ lng: locale,
455
+ fallbackLng: "en",
456
+ resources,
457
+ ns: ["common", "about"],
458
+ defaultNS: "common",
459
+ interpolation: { escapeValue: false },
460
+ });
461
+
462
+ const { t } = useTranslation("about");
463
+
464
+ return (
465
+ <I18nextProvider i18n={i18n}>
466
+ <main>
467
+ <h1>{t("title")}</h1>
468
+ <ClientComponent />
469
+ <ServerComponent />
470
+ </main>
471
+ </I18nextProvider>
472
+ );
473
+ }
474
+
475
+ export const getStaticProps: GetStaticProps = async ({ locale }) => {
476
+ // 이 페이지에 필요한 네임스페이스만 미리 로드합니다
477
+ return {
478
+ props: {
479
+ ...(await serverSideTranslations(locale ?? "en", ["common", "about"])),
480
+ },
481
+ };
482
+ };
483
+ ```
484
+
485
+ </TabItem>
486
+ <TabItem label="next-intl" value="next-intl">
487
+
488
+ ```tsx fileName="i18n.ts"
489
+ import { getRequestConfig } from "next-intl/server";
490
+ import { notFound } from "next/navigation";
491
+
492
+ // 공통 설정에서 가져올 수 있습니다
493
+ const locales = ["en", "fr", "es"];
494
+
495
+ export default getRequestConfig(async ({ locale }) => {
496
+ // 들어오는 `locale` 매개변수가 유효한지 검증합니다
497
+ if (!locales.includes(locale as any)) notFound();
498
+
499
+ return {
500
+ messages: (await import(`../messages/${locale}.json`)).default,
501
+ };
502
+ });
503
+ ```
504
+
505
+ ```tsx fileName="src/app/[locale]/about/layout.tsx"
506
+ import { NextIntlClientProvider } from "next-intl";
507
+ import { getMessages, unstable_setRequestLocale } from "next-intl/server";
508
+ import pick from "lodash/pick";
509
+
510
+ export default async function LocaleLayout({
511
+ children,
512
+ params,
513
+ }: {
514
+ children: React.ReactNode;
515
+ params: { locale: string };
516
+ }) {
517
+ const { locale } = params;
518
+
519
+ // 이 서버 렌더링(RSC)을 위한 활성 요청 로케일 설정
520
+ unstable_setRequestLocale(locale);
521
+
522
+ // 메시지는 src/i18n/request.ts를 통해 서버 측에서 로드됩니다
523
+ // (next-intl 문서 참조). 여기서는 클라이언트 컴포넌트에 필요한
524
+ // 일부 메시지만 클라이언트로 전달합니다 (페이로드 최적화).
525
+ const messages = await getMessages();
526
+ const clientMessages = pick(messages, ["common", "about"]);
527
+
528
+ return (
529
+ <html lang={locale}>
530
+ <body>
531
+ <NextIntlClientProvider locale={locale} messages={clientMessages}>
532
+ {children}
533
+ </NextIntlClientProvider>
534
+ </body>
535
+ </html>
536
+ );
537
+ }
538
+ ```
539
+
540
+ ```tsx fileName="src/app/[locale]/about/page.tsx"
541
+ import { getTranslations } from "next-intl/server";
542
+ import { ClientComponent, ServerComponent } from "@components";
543
+
544
+ export default async function LandingPage({
545
+ params,
546
+ }: {
547
+ params: { locale: string };
548
+ }) {
549
+ // 엄격하게 서버 측에서만 로드됨 (클라이언트에 하이드레이션되지 않음)
550
+ const t = await getTranslations("about");
551
+
552
+ return (
553
+ <main>
554
+ <h1>{t("title")}</h1>
555
+ <ClientComponent />
556
+ <ServerComponent />
557
+ </main>
558
+ );
559
+ }
560
+ ```
561
+
562
+ </TabItem>
563
+ <TabItem label="intlayer" value="intlayer">
564
+
565
+ ```tsx fileName="intlayer.config.ts"
566
+ export default {
567
+ internationalization: {
568
+ locales: ["en", "fr", "es"],
569
+ defaultLocale: "en",
570
+ },
571
+ };
572
+ ```
573
+
574
+ ```tsx fileName="src/app/[locale]/layout.tsx"
575
+ import { getHTMLTextDir } from "intlayer";
576
+ import {
577
+ IntlayerClientProvider,
578
+ generateStaticParams,
579
+ type NextLayoutIntlayer,
580
+ } from "next-intlayer";
581
+
582
+ export const dynamic = "force-static";
583
+
584
+ const LandingLayout: NextLayoutIntlayer = async ({ children, params }) => {
585
+ const { locale } = await params;
129
586
 
130
- - **next-intl / next-i18next**: 일반적으로 번역 및 편집 워크플로우를 위해 외부 플랫폼과 연동해야 합니다.
131
- - **Intlayer**: **무료 비주얼 에디터**와 **선택적 CMS**(Git 친화적이거나 외부화 가능)를 제공합니다. 또한 콘텐츠 작성용 **VSCode 확장**과 자체 제공자 키를 사용하는 **AI 지원 번역** 기능도 포함되어 있습니다.
587
+ return (
588
+ <html lang={locale} dir={getHTMLTextDir(locale)}>
589
+ <IntlayerClientProvider locale={locale}>
590
+ {children}
591
+ </IntlayerClientProvider>
592
+ </html>
593
+ );
594
+ };
132
595
 
133
- **중요한 이유:** 운영 비용을 낮추고 개발자와 콘텐츠 작성자 간의 피드백 루프를 단축합니다.
596
+ export default LandingLayout;
597
+ ```
598
+
599
+ ```tsx fileName="src/app/[locale]/about/page.tsx"
600
+ import { PageContent } from "@components/PageContent";
601
+ import type { NextPageIntlayer } from "next-intlayer";
602
+ import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";
603
+ import { ClientComponent, ServerComponent } from "@components";
604
+
605
+ const LandingPage: NextPageIntlayer = async ({ params }) => {
606
+ const { locale } = await params;
607
+ const { title } = useIntlayer("about", locale);
608
+
609
+ return (
610
+ <IntlayerServerProvider locale={locale}>
611
+ <main>
612
+ <h1>{title}</h1>
613
+ <ClientComponent />
614
+ <ServerComponent />
615
+ </main>
616
+ </IntlayerServerProvider>
617
+ );
618
+ };
619
+
620
+ export default LandingPage;
621
+ ```
622
+
623
+ </TabItem>
624
+ </Tab>
625
+
626
+ #### 비교
627
+
628
+ 세 가지 모두 로케일별 콘텐츠 로딩과 프로바이더를 지원합니다.
629
+
630
+ - **next-intl/next-i18next**를 사용할 경우, 일반적으로 경로별로 선택된 메시지/네임스페이스를 로드하고 필요한 위치에 프로바이더를 배치합니다.
631
+
632
+ - **Intlayer**는 빌드 시점 분석을 추가하여 사용을 추론하므로 수동 연결을 줄이고 단일 루트 프로바이더를 허용할 수 있습니다.
633
+
634
+ 팀의 선호도에 따라 명시적 제어와 자동화 중에서 선택하세요.
635
+
636
+ ### 클라이언트 컴포넌트에서의 사용법
637
+
638
+ 카운터를 렌더링하는 클라이언트 컴포넌트 예제를 살펴보겠습니다.
639
+
640
+ <Tab defaultTab="next-intl" group='techno'>
641
+ <TabItem label="next-i18next" value="next-i18next">
642
+
643
+ **번역 (실제 JSON이어야 하며 `public/locales/...`에 위치해야 합니다)**
644
+
645
+ ```json fileName="public/locales/en/about.json"
646
+ {
647
+ "counter": {
648
+ "label": "Counter",
649
+ "increment": "Increment"
650
+ }
651
+ }
652
+ ```
653
+
654
+ ```json fileName="public/locales/fr/about.json"
655
+ {
656
+ "counter": {
657
+ "label": "카운터",
658
+ "increment": "증가"
659
+ }
660
+ }
661
+ ```
662
+
663
+ **클라이언트 컴포넌트**
664
+
665
+ ```tsx fileName="src/components/ClientComponentExample.tsx"
666
+ "use client";
667
+
668
+ import React, { useMemo, useState } from "react";
669
+ import { useTranslation } from "next-i18next";
670
+
671
+ const ClientComponentExample = () => {
672
+ const { t, i18n } = useTranslation("about");
673
+ const [count, setCount] = useState(0);
674
+
675
+ // next-i18next는 useNumber를 제공하지 않으므로 Intl.NumberFormat 사용
676
+ const numberFormat = new Intl.NumberFormat(i18n.language);
677
+
678
+ return (
679
+ <div>
680
+ <p>{numberFormat.format(count)}</p>
681
+ <button
682
+ aria-label={t("counter.label")}
683
+ onClick={() => setCount((count) => count + 1)}
684
+ >
685
+ {t("counter.increment")}
686
+ </button>
687
+ </div>
688
+ );
689
+ };
690
+ ```
691
+
692
+ > 페이지 serverSideTranslations에 "about" 네임스페이스를 추가하는 것을 잊지 마세요
693
+ > 여기서는 react 19.x.x 버전을 사용하지만, 하위 버전에서는 useMemo를 사용하여 포매터 인스턴스를 저장해야 합니다. 이는 무거운 함수이기 때문입니다.
694
+
695
+ </TabItem>
696
+ <TabItem label="next-intl" value="next-intl">
697
+
698
+ **번역 (형식 재사용; 원하는 대로 next-intl 메시지에 로드하세요)**
699
+
700
+ ```json fileName="locales/en/about.json"
701
+ {
702
+ "counter": {
703
+ "label": "Counter",
704
+ "increment": "Increment"
705
+ }
706
+ }
707
+ ```
708
+
709
+ ```json fileName="locales/fr/about.json"
710
+ {
711
+ "counter": {
712
+ "label": "Compteur",
713
+ "increment": "Incrémenter"
714
+ }
715
+ }
716
+ ```
717
+
718
+ **클라이언트 컴포넌트**
719
+
720
+ ```tsx fileName="src/components/ClientComponentExample.tsx"
721
+ "use client";
722
+
723
+ import React, { useState } from "react";
724
+ import { useTranslations, useFormatter } from "next-intl";
725
+
726
+ const ClientComponentExample = () => {
727
+ // 중첩된 객체에 직접 범위 지정
728
+ const t = useTranslations("about.counter");
729
+ const format = useFormatter();
730
+ const [count, setCount] = useState(0);
731
+
732
+ return (
733
+ <div>
734
+ <p>{format.number(count)}</p>
735
+ <button
736
+ aria-label={t("label")}
737
+ onClick={() => setCount((count) => count + 1)}
738
+ >
739
+ {t("increment")}
740
+ </button>
741
+ </div>
742
+ );
743
+ };
744
+ ```
745
+
746
+ > 페이지 클라이언트 메시지에 "about" 메시지를 추가하는 것을 잊지 마세요
747
+
748
+ </TabItem>
749
+ <TabItem label="intlayer" value="intlayer">
750
+
751
+ **내용**
752
+
753
+ ```ts fileName="src/components/ClientComponentExample/index.content.ts"
754
+ import { t, type Dictionary } from "intlayer";
755
+
756
+ const counterContent = {
757
+ key: "counter",
758
+ content: {
759
+ label: t({ ko: "카운터", en: "Counter", fr: "Compteur" }),
760
+ increment: t({ ko: "증가", en: "Increment", fr: "Incrémenter" }),
761
+ },
762
+ } satisfies Dictionary;
763
+
764
+ export default counterContent;
765
+ ```
766
+
767
+ **클라이언트 컴포넌트**
768
+
769
+ ```tsx fileName="src/components/ClientComponentExample/index.tsx"
770
+ "use client";
771
+
772
+ import React, { useState } from "react";
773
+ import { useNumber, useIntlayer } from "next-intlayer";
774
+
775
+ const ClientComponentExample = () => {
776
+ const [count, setCount] = useState(0);
777
+ const { label, increment } = useIntlayer("counter"); // 문자열을 반환합니다
778
+ const { number } = useNumber();
779
+
780
+ return (
781
+ <div>
782
+ <p>{number(count)}</p>
783
+ <button aria-label={label} onClick={() => setCount((count) => count + 1)}>
784
+ {increment}
785
+ </button>
786
+ </div>
787
+ );
788
+ };
789
+ ```
790
+
791
+ </TabItem>
792
+ </Tab>
793
+
794
+ #### 비교
795
+
796
+ - **숫자 포맷팅**
797
+ - **next-i18next**: `useNumber` 없음; `Intl.NumberFormat` (또는 i18next-icu) 사용.
798
+ - **next-intl**: `useFormatter().number(value)`.
799
+ - **Intlayer**: 내장된 `useNumber()` 사용.
800
+
801
+ - **키**
802
+ - 중첩 구조 유지 (`about.counter.label`) 및 훅 범위 지정 (`useTranslation("about")` + `t("counter.label")` 또는 `useTranslations("about.counter")` + `t("label")`).
803
+
804
+ - **파일 위치**
805
+ - **next-i18next**는 `public/locales/{lng}/{ns}.json`에 JSON을 기대.
806
+ - **next-intl**는 유연하며, 설정에 따라 메시지 로드 가능.
807
+ - **Intlayer**는 TS/JS 딕셔너리에 콘텐츠를 저장하고 키로 해석.
134
808
 
135
809
  ---
136
810
 
137
- ## 언제 어떤 것을 선택해야 할까요?
811
+ ### 서버 컴포넌트에서의 사용
812
+
813
+ UI 컴포넌트의 경우를 살펴보겠습니다. 이 컴포넌트는 서버 컴포넌트이며, 클라이언트 컴포넌트의 자식으로 삽입될 수 있어야 합니다. (페이지 (서버 컴포넌트) -> 클라이언트 컴포넌트 -> 서버 컴포넌트). 이 컴포넌트가 클라이언트 컴포넌트의 자식으로 삽입될 수 있으므로 비동기(async)일 수 없습니다.
814
+
815
+ <Tab defaultTab="next-intl" group='techno'>
816
+ <TabItem label="next-i18next" value="next-i18next">
817
+
818
+ ```tsx fileName="src/pages/about.tsx"
819
+ import type { GetStaticProps } from "next";
820
+ import { useTranslation } from "next-i18next";
821
+
822
+ type ServerComponentProps = {
823
+ count: number;
824
+ };
825
+
826
+ const ServerComponent = ({ count }: ServerComponentProps) => {
827
+ const { t, i18n } = useTranslation("about");
828
+ const formatted = new Intl.NumberFormat(i18n.language).format(count);
829
+
830
+ return (
831
+ <div>
832
+ <p>{formatted}</p>
833
+ <button aria-label={t("counter.label")}>{t("counter.increment")}</button>
834
+ </div>
835
+ );
836
+ };
837
+ ```
838
+
839
+ </TabItem>
840
+ <TabItem label="next-intl" value="next-intl">
841
+
842
+ ```tsx fileName="src/components/ServerComponent.tsx"
843
+ type ServerComponentProps = {
844
+ count: number;
845
+ t: (key: string) => string;
846
+ };
847
+
848
+ const ServerComponent = ({ t, count }: ServerComponentProps) => {
849
+ const formatted = new Intl.NumberFormat(i18n.language).format(count);
850
+
851
+ return (
852
+ <div>
853
+ <p>{formatted}</p>
854
+ <button aria-label={t("label")}>{t("increment")}</button>
855
+ </div>
856
+ );
857
+ };
858
+ ```
859
+
860
+ > 서버 컴포넌트는 비동기(async)일 수 없으므로, 번역 함수와 포맷터 함수를 props로 전달해야 합니다.
861
+ >
862
+ > - `const t = await getTranslations("about.counter");`
863
+ > - `const format = await getFormatter();`
864
+
865
+ </TabItem>
866
+ <TabItem label="intlayer" value="intlayer">
867
+
868
+ ```tsx fileName="src/components/ServerComponent.tsx"
869
+ import { useIntlayer, useNumber } from "next-intlayer/server";
870
+
871
+ const ServerComponent = ({ count }: { count: number }) => {
872
+ const { label, increment } = useIntlayer("counter");
873
+ const { number } = useNumber();
874
+
875
+ return (
876
+ <div>
877
+ <p>{number(count)}</p>
878
+ <button aria-label={label}>{increment}</button>
879
+ </div>
880
+ );
881
+ };
882
+ ```
883
+
884
+ </TabItem>
885
+ </Tab>
886
+
887
+ > Intlayer는 `next-intlayer/server`를 통해 **서버 안전** 훅을 제공합니다. 작동을 위해 `useIntlayer`와 `useNumber`는 클라이언트 훅과 유사한 훅 형태의 문법을 사용하지만, 내부적으로는 서버 컨텍스트(`IntlayerServerProvider`)에 의존합니다.
888
+
889
+ ### 메타데이터 / 사이트맵 / 로봇
890
+
891
+ 콘텐츠 번역은 훌륭합니다. 하지만 사람들은 보통 국제화의 주요 목표가 웹사이트를 전 세계에 더 잘 보이게 하는 것임을 잊어버립니다. I18n은 웹사이트 가시성을 향상시키는 놀라운 수단입니다.
892
+
893
+ 다국어 SEO와 관련된 좋은 실천 목록은 다음과 같습니다.
138
894
 
139
- - **next-intl**를 선택하세요, 만약 **최소한의** 솔루션을 원하고, 중앙 집중식 카탈로그에 익숙하며, 앱이 **작거나 중간 규모**인 경우.
140
- - **next-i18next**를 선택하세요, 만약 **i18next의 플러그인 생태계**(예: 플러그인을 통한 고급 ICU 규칙)가 필요하고, 팀이 이미 i18next를 알고 있으며, 유연성을 위해 **더 많은 설정**을 감수할 수 있는 경우.
141
- - **Intlayer**를 선택하세요, 만약 **컴포넌트 범위의 콘텐츠**, **엄격한 TypeScript**, **빌드 타임 보장**, **트리 쉐이킹**, 그리고 **내장된 라우팅/SEO/에디터 도구**를 중요하게 생각한다면 - 특히 **Next.js App Router**와 **대규모 모듈식 코드베이스**에 적합합니다.
895
+ - `<head>` 태그에 hreflang 메타 태그 설정
896
+ > 검색 엔진이 페이지에서 어떤 언어가 사용 가능한지 이해하는 도움을 줍니다
897
+ - `http://www.w3.org/1999/xhtml` XML 스키마를 사용하여 sitemap.xml에 모든 페이지 번역을 나열하세요.
898
+ >
899
+ - robots.txt에서 접두사가 붙은 페이지를 제외하는 것을 잊지 마세요 (예: `/dashboard`, `/fr/dashboard`, `/es/dashboard`)
900
+ >
901
+ - 가장 현지화된 페이지로 리디렉션하기 위해 커스텀 Link 컴포넌트를 사용하세요 (예: 프랑스어의 경우 `<a href="/fr/about">A propos</a>`)
902
+ >
903
+
904
+ 개발자들은 종종 여러 로케일에 걸쳐 페이지를 올바르게 참조하는 것을 잊어버립니다.
905
+
906
+ <Tab defaultTab="next-intl" group='techno'>
907
+
908
+ <TabItem label="next-i18next" value="next-i18next">
909
+
910
+ ```ts fileName="i18n.config.ts"
911
+ export const locales = ["en", "fr"] as const;
912
+ export type Locale = (typeof locales)[number];
913
+ export const defaultLocale: Locale = "en";
914
+
915
+ export function localizedPath(locale: string, path: string) {
916
+ return locale === defaultLocale ? path : "/" + locale + path;
917
+ }
918
+
919
+ const ORIGIN = "https://example.com";
920
+ export function abs(locale: string, path: string) {
921
+ return ORIGIN + localizedPath(locale, path);
922
+ }
923
+ ```
924
+
925
+ ```tsx fileName="src/app/[locale]/about/layout.tsx"
926
+ import type { Metadata } from "next";
927
+ import { locales, defaultLocale, localizedPath } from "@/i18n.config";
928
+
929
+ export async function generateMetadata({
930
+ params,
931
+ }: {
932
+ params: { locale: string };
933
+ }): Promise<Metadata> {
934
+ const { locale } = params;
935
+
936
+ // 올바른 JSON 파일을 동적으로 가져옵니다
937
+ const messages = (
938
+ await import("@/../public/locales/" + locale + "/about.json")
939
+ ).default;
940
+
941
+ const languages = Object.fromEntries(
942
+ locales.map((locale) => [locale, localizedPath(locale, "/about")])
943
+ );
944
+
945
+ return {
946
+ title: messages.title,
947
+ description: messages.description,
948
+ alternates: {
949
+ canonical: localizedPath(locale, "/about"),
950
+ languages: { ...languages, "x-default": "/about" },
951
+ },
952
+ };
953
+ }
954
+
955
+ export default async function AboutPage() {
956
+ return <h1>소개</h1>; // 페이지 제목을 한국어로 번역
957
+ }
958
+ ```
959
+
960
+ ```ts fileName="src/app/sitemap.ts"
961
+ import type { MetadataRoute } from "next";
962
+ import { locales, defaultLocale, abs } from "@/i18n.config";
963
+
964
+ export default function sitemap(): MetadataRoute.Sitemap {
965
+ const languages = Object.fromEntries(
966
+ locales.map((locale) => [locale, abs(locale, "/about")])
967
+ );
968
+ return [
969
+ {
970
+ url: abs(defaultLocale, "/about"),
971
+ lastModified: new Date(), // 마지막 수정 날짜
972
+ changeFrequency: "monthly", // 변경 빈도: 매월
973
+ priority: 0.7, // 우선순위
974
+ alternates: { languages }, // 대체 언어 경로
975
+ },
976
+ ];
977
+ }
978
+ ```
979
+
980
+ ```ts fileName="src/app/robots.ts"
981
+ import type { MetadataRoute } from "next";
982
+ import { locales, defaultLocale, localizedPath } from "@/i18n.config";
983
+
984
+ const ORIGIN = "https://example.com";
985
+
986
+ const expandAllLocales = (path: string) => [
987
+ localizedPath(defaultLocale, path),
988
+ ...locales
989
+ .filter((locale) => locale !== defaultLocale)
990
+ .map((locale) => localizedPath(locale, path)),
991
+ ];
992
+
993
+ export default function robots(): MetadataRoute.Robots {
994
+ const disallow = [
995
+ ...expandAllLocales("/dashboard"),
996
+ ...expandAllLocales("/admin"),
997
+ ];
998
+
999
+ return {
1000
+ rules: { userAgent: "*", allow: ["/"], disallow },
1001
+ host: ORIGIN,
1002
+ sitemap: ORIGIN + "/sitemap.xml",
1003
+ };
1004
+ }
1005
+ ```
1006
+
1007
+ </TabItem>
1008
+ <TabItem label="next-intl" value="next-intl">
1009
+
1010
+ ```tsx fileName="src/app/[locale]/about/layout.tsx"
1011
+ import type { Metadata } from "next";
1012
+ import { locales, defaultLocale } from "@/i18n";
1013
+ import { getTranslations } from "next-intl/server";
1014
+
1015
+ function localizedPath(locale: string, path: string) {
1016
+ return locale === defaultLocale ? path : "/" + locale + path;
1017
+ }
1018
+
1019
+ export async function generateMetadata({
1020
+ params,
1021
+ }: {
1022
+ params: { locale: string };
1023
+ }): Promise<Metadata> {
1024
+ const { locale } = params;
1025
+ const t = await getTranslations({ locale, namespace: "about" });
1026
+
1027
+ const url = "/about";
1028
+ const languages = Object.fromEntries(
1029
+ locales.map((locale) => [locale, localizedPath(locale, url)])
1030
+ );
1031
+
1032
+ return {
1033
+ title: t("title"),
1034
+ description: t("description"),
1035
+ alternates: {
1036
+ canonical: localizedPath(locale, url),
1037
+ languages: { ...languages, "x-default": url },
1038
+ },
1039
+ };
1040
+ }
1041
+
1042
+ // ... 페이지 코드의 나머지 부분
1043
+ ```
1044
+
1045
+ ```tsx fileName="src/app/sitemap.ts"
1046
+ import type { MetadataRoute } from "next";
1047
+ import { locales, defaultLocale } from "@/i18n";
1048
+
1049
+ const origin = "https://example.com";
1050
+
1051
+ const formatterLocalizedPath = (locale: string, path: string) =>
1052
+ locale === defaultLocale ? origin + path : origin + "/" + locale + path;
1053
+
1054
+ export default function sitemap(): MetadataRoute.Sitemap {
1055
+ const aboutLanguages = Object.fromEntries(
1056
+ locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
1057
+ );
1058
+
1059
+ return [
1060
+ {
1061
+ url: formatterLocalizedPath(defaultLocale, "/about"),
1062
+ lastModified: new Date(),
1063
+ changeFrequency: "monthly",
1064
+ priority: 0.7,
1065
+ alternates: { languages: aboutLanguages },
1066
+ },
1067
+ ];
1068
+ }
1069
+ ```
1070
+
1071
+ ```tsx fileName="src/app/robots.ts"
1072
+ import type { MetadataRoute } from "next";
1073
+ import { locales, defaultLocale } from "@/i18n";
1074
+
1075
+ const origin = "https://example.com";
1076
+ const withAllLocales = (path: string) => [
1077
+ path,
1078
+ ...locales
1079
+ .filter((locale) => locale !== defaultLocale)
1080
+ .map((locale) => "/" + locale + path),
1081
+ ];
1082
+
1083
+ export default function robots(): MetadataRoute.Robots {
1084
+ const disallow = [
1085
+ ...withAllLocales("/dashboard"),
1086
+ ...withAllLocales("/admin"),
1087
+ ];
1088
+
1089
+ return {
1090
+ rules: { userAgent: "*", allow: ["/"], disallow },
1091
+ host: origin,
1092
+ sitemap: origin + "/sitemap.xml",
1093
+ };
1094
+ }
1095
+ ```
1096
+
1097
+ </TabItem>
1098
+ <TabItem label="intlayer" value="intlayer">
1099
+
1100
+ ```typescript fileName="src/app/[locale]/about/layout.tsx"
1101
+ import { getIntlayer, getMultilingualUrls } from "intlayer";
1102
+ import type { Metadata } from "next";
1103
+ import type { LocalPromiseParams } from "next-intlayer";
1104
+
1105
+ export const generateMetadata = async ({
1106
+ params,
1107
+ }: LocalPromiseParams): Promise<Metadata> => {
1108
+ const { locale } = await params;
1109
+
1110
+ const metadata = getIntlayer("page-metadata", locale);
1111
+
1112
+ const multilingualUrls = getMultilingualUrls("/about");
1113
+
1114
+ return {
1115
+ ...metadata,
1116
+ alternates: {
1117
+ canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
1118
+ languages: { ...multilingualUrls, "x-default": "/about" },
1119
+ },
1120
+ };
1121
+ };
1122
+
1123
+ // ... 페이지 나머지 코드
1124
+ ```
1125
+
1126
+ ```tsx fileName="src/app/sitemap.ts"
1127
+ import { getMultilingualUrls } from "intlayer";
1128
+ import type { MetadataRoute } from "next";
1129
+
1130
+ const sitemap = (): MetadataRoute.Sitemap => [
1131
+ {
1132
+ url: "https://example.com/about",
1133
+ alternates: {
1134
+ languages: { ...getMultilingualUrls("https://example.com/about") },
1135
+ },
1136
+ },
1137
+ ];
1138
+ ```
1139
+
1140
+ ```tsx fileName="src/app/robots.ts"
1141
+ import { getMultilingualUrls } from "intlayer";
1142
+ import type { MetadataRoute } from "next";
1143
+
1144
+ const getAllMultilingualUrls = (urls: string[]) =>
1145
+ urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
1146
+
1147
+ const robots = (): MetadataRoute.Robots => ({
1148
+ rules: {
1149
+ userAgent: "*",
1150
+ allow: ["/"],
1151
+ disallow: getAllMultilingualUrls(["/dashboard"]),
1152
+ },
1153
+ host: "https://example.com",
1154
+ sitemap: "https://example.com/sitemap.xml",
1155
+ });
1156
+
1157
+ export default robots;
1158
+ ```
1159
+
1160
+ </TabItem>
1161
+ </Tab>
1162
+
1163
+ > Intlayer는 사이트맵을 위해 다국어 URL을 생성하는 `getMultilingualUrls` 함수를 제공합니다.
1164
+
1165
+ ---
142
1166
 
143
1167
  ---
144
1168
 
145
- ## 실용적인 마이그레이션 노트 (next-intl / next-i18next → Intlayer)
1169
+ ## 그리고 승자는…
1170
+
1171
+ 간단하지 않습니다. 각 옵션마다 장단점이 있습니다. 제가 보는 관점은 다음과 같습니다:
1172
+
1173
+ <Columns>
1174
+ <Column>
1175
+
1176
+ **next-intl**
1177
+
1178
+ - 가장 간단하고 가벼우며, 강요되는 결정이 적습니다. **최소한의** 솔루션을 원하고 중앙 집중식 카탈로그에 익숙하며 앱이 **소규모에서 중간 규모**인 경우 적합합니다.
1179
+
1180
+ </Column>
1181
+ <Column>
1182
+
1183
+ **next-i18next**
1184
+
1185
+ - 성숙하고 기능이 풍부하며 커뮤니티 플러그인이 많지만 설정 비용이 더 높습니다. **i18next의 플러그인 생태계**(예: 플러그인을 통한 고급 ICU 규칙)가 필요하고 팀이 이미 i18next를 알고 있으며 유연성을 위해 **더 많은 설정**을 감수할 수 있다면 적합합니다.
1186
+
1187
+ </Column>
1188
+ <Column>
1189
+
1190
+ **Intlayer**
1191
+
1192
+ - 모듈식 콘텐츠, 타입 안전성, 도구 지원, 그리고 보일러플레이트가 적은 현대적인 Next.js를 위해 설계되었습니다. 특히 **Next.js App Router**, 디자인 시스템, 그리고 **대규모 모듈식 코드베이스**에 대해 **컴포넌트 범위 콘텐츠**, **엄격한 TypeScript**, **빌드 타임 보장**, **트리 쉐이킹**, 그리고 **기본 제공** 라우팅/SEO/에디터 도구를 중요하게 생각한다면 적합합니다.
1193
+
1194
+ </Column>
1195
+ </Columns>
1196
+
1197
+ 최소한의 설정을 선호하고 일부 수동 연결을 감수할 수 있다면 next-intl이 좋은 선택입니다. 모든 기능이 필요하고 복잡성을 감수할 수 있다면 next-i18next가 적합합니다. 하지만 현대적이고 확장 가능하며 모듈식 솔루션과 내장 도구를 원한다면 Intlayer가 바로 그 요구를 충족시키고자 합니다.
1198
+
1199
+ > **기업 팀을 위한 대안**: **Crowdin**, **Phrase**와 같은 검증된 현지화 플랫폼이나 기타 전문 번역 관리 시스템과 완벽하게 작동하는 검증된 솔루션이 필요하다면, 성숙한 생태계와 검증된 통합 기능을 갖춘 **next-intl** 또는 **next-i18next**를 고려해 보세요.
1200
+
1201
+ > **향후 로드맵**: Intlayer는 또한 **i18next** 및 **next-intl** 솔루션 위에서 작동하는 플러그인 개발을 계획하고 있습니다. 이를 통해 자동화, 구문, 콘텐츠 관리 측면에서 Intlayer의 장점을 제공하면서도, 애플리케이션 코드에서 이러한 검증된 솔루션들이 제공하는 보안성과 안정성을 유지할 수 있습니다.
1202
+
1203
+ ## GitHub STARs
1204
+
1205
+ GitHub 스타는 프로젝트의 인기, 커뮤니티 신뢰도, 그리고 장기적인 관련성을 강력하게 나타내는 지표입니다. 기술적 품질의 직접적인 척도는 아니지만, 얼마나 많은 개발자가 해당 프로젝트를 유용하다고 생각하고, 진행 상황을 팔로우하며, 채택할 가능성이 있는지를 반영합니다. 프로젝트의 가치를 평가할 때, 스타는 대안들 간의 관심도를 비교하고 생태계 성장에 대한 통찰을 제공하는 데 도움이 됩니다.
146
1206
 
147
- - **기능별로 시작하기**: 한 번에 하나의 라우트나 컴포넌트를 **로컬 사전**으로 옮기세요.
148
- - **기존 카탈로그 병행 유지**: 마이그레이션 중에 다리를 놓듯 사용하세요; 한꺼번에 전환하는 것은 피하세요.
149
- - **엄격한 검사 활성화**: 빌드 타임에 누락된 부분을 조기에 발견할 수 있게 하세요.
150
- - **미들웨어 및 헬퍼 도입**: 사이트 전반에 걸쳐 로케일 감지와 SEO 태그를 표준화하세요.
151
- - **번들 크기 측정**: 사용하지 않는 콘텐츠가 제거되면서 **번들 크기 감소**를 기대하세요.
1207
+ [![스타 히스토리 차트](https://api.star-history.com/svg?repos=i18next/next-i18next&repos=amannn/next-intl&repos=aymericzip/intlayer&type=Date)](https://www.star-history.com/#i18next/next-i18next&amannn/next-intl&aymericzip/intlayer)
152
1208
 
153
1209
  ---
154
1210
 
155
1211
  ## 결론
156
1212
 
157
- 세 라이브러리 모두 핵심 로컬라이제이션에서는 성공적입니다. 차이점은 **현대적인 Next.js 환경에서 견고하고 확장 가능한 설정을 위해 얼마나 많은 작업을 해야 하는가**에 있습니다:
1213
+ 가지 라이브러리 모두 핵심 로컬라이제이션에서는 성공적입니다. 차이점은 **현대적인 Next.js에서 견고하고 확장 가능한 설정을 달성하기 위해 얼마나 많은 작업을 해야 하는가**에 있습니다:
158
1214
 
159
- - **Intlayer**를 사용하면, **모듈화된 콘텐츠**, **엄격한 TS(타입스크립트)**, **빌드 타임 안전성**, **트리 쉐이킹된 번들**, 그리고 **최고급 App Router SEO 도구**가 **기본값**으로 제공되며, 번거로운 작업이 아닙니다.
1215
+ - **Intlayer**를 사용하면, **모듈화된 콘텐츠**, **엄격한 TS(타입스크립트)**, **빌드 타임 안전성**, **트리 쉐이킹된 번들**, 그리고 **일류 App Router + SEO 도구**가 **기본값**으로 제공되며, 번거로운 작업이 아닙니다.
160
1216
  - 다국어, 컴포넌트 기반 앱에서 **유지보수성과 속도**를 중요시하는 팀이라면, Intlayer가 오늘날 가장 **완벽한** 경험을 제공합니다.
161
1217
 
162
1218
  자세한 내용은 ['Why Intlayer?' 문서](https://intlayer.org/doc/why)를 참조하세요.