---
createdAt: 2025-08-23
updatedAt: 2025-09-29
title: next-i18next vs next-intl vs Intlayer
description: Next.js ์ฑ์ ๊ตญ์ ํ(i18n)๋ฅผ ์ํด next-i18next, next-intl, Intlayer๋ฅผ ๋น๊ตํฉ๋๋ค.
keywords:
- next-intl
- next-i18next
- Intlayer
- ๊ตญ์ ํ
- ๋ธ๋ก๊ทธ
- Next.js
- ์๋ฐ์คํฌ๋ฆฝํธ
- ๋ฆฌ์กํธ
slugs:
- blog
- next-i18next-vs-next-intl-vs-intlayer
---
# next-i18next VS next-intl VS intlayer | Next.js ๊ตญ์ ํ (i18n)
<TOC/>

Next.js๋ฅผ ์ํ ์ธ ๊ฐ์ง i18n ์ต์
์ธ next-i18next, next-intl, Intlayer์ ์ ์ฌ์ ๊ณผ ์ฐจ์ด์ ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
์ด ๋ฌธ์๋ ์์ ํ ํํ ๋ฆฌ์ผ์ด ์๋๋ผ ์ ํ์ ๋์์ ์ฃผ๊ธฐ ์ํ ๋น๊ต์
๋๋ค.
์ฐ๋ฆฌ๋ **Next.js 13+ App Router** (๋ฐ **React Server Components**)์ ์ค์ ์ ๋๊ณ ๋ค์ ํญ๋ชฉ๋ค์ ํ๊ฐํฉ๋๋ค:
1. **์ํคํ
์ฒ ๋ฐ ์ฝํ
์ธ ๊ตฌ์ฑ**
2. **TypeScript ๋ฐ ์์ ์ฑ**
3. **๋๋ฝ๋ ๋ฒ์ญ ์ฒ๋ฆฌ**
4. **๋ผ์ฐํ
๋ฐ ๋ฏธ๋ค์จ์ด**
5. **์ฑ๋ฅ ๋ฐ ๋ก๋ฉ ๋์**
6. **๊ฐ๋ฐ์ ๊ฒฝํ(DX), ๋๊ตฌ ๋ฐ ์ ์ง๋ณด์**
7. **SEO ๋ฐ ๋๊ท๋ชจ ํ๋ก์ ํธ ํ์ฅ์ฑ**
> **์์ฝ**: ์ธ ๊ฐ์ง ๋ชจ๋ Next.js ์ฑ์ ํ์งํํ ์ ์์ต๋๋ค. ๋ง์ฝ **์ปดํฌ๋ํธ ๋ฒ์ ์ฝํ
์ธ **, **์๊ฒฉํ TypeScript ํ์
**, **๋น๋ ์ ๋๋ฝ ํค ๊ฒ์ฌ**, **ํธ๋ฆฌ ์์ดํน๋ ์ฌ์ **, ๊ทธ๋ฆฌ๊ณ **์ต๊ณ ๊ธ App Router + SEO ๋์ฐ๋ฏธ**๋ฅผ ์ํ๋ค๋ฉด, **Intlayer**๊ฐ ๊ฐ์ฅ ์์ ํ๊ณ ํ๋์ ์ธ ์ ํ์
๋๋ค.
> ๊ฐ๋ฐ์๋ค์ด ์์ฃผ ํผ๋ํ๋ ์ ์ค ํ๋๋ `next-intl`์ด `react-intl`์ Next.js ๋ฒ์ ์ด๋ผ๊ณ ์๊ฐํ๋ ๊ฒ์
๋๋ค. ๊ทธ๋ ์ง ์์ต๋๋คโ`next-intl`์ [Amann](https://github.com/amannn)์ด ์ ์ง ๊ด๋ฆฌํ๋ฉฐ, `react-intl`์ [FormatJS](https://github.com/formatjs/formatjs)๊ฐ ์ ์ง ๊ด๋ฆฌํฉ๋๋ค.
---
## ์์ฝ
- **next-intl** - ๊ฐ๋ณ๊ณ ์ง๊ด์ ์ธ ๋ฉ์์ง ํฌ๋งทํ
์ ์ ๊ณตํ๋ฉฐ, ๊ฒฌ๊ณ ํ Next.js ์ง์์ ๊ฐ์ถ๊ณ ์์ต๋๋ค. ์ค์ ์ง์ค์ ์นดํ๋ก๊ทธ๊ฐ ์ผ๋ฐ์ ์ด๋ฉฐ, ๊ฐ๋ฐ์ ๊ฒฝํ(DX)์ ๊ฐ๋จํ์ง๋ง ์์ ์ฑ๊ณผ ๋๊ท๋ชจ ์ ์ง ๊ด๋ฆฌ๋ ์ฃผ๋ก ์ฌ์ฉ์ ์ฑ
์์
๋๋ค.
- **next-i18next** - Next.js ํ๊ฒฝ์ ๋ง์ถ i18next์
๋๋ค. ์ฑ์ํ ์ํ๊ณ์ ํ๋ฌ๊ทธ์ธ(์: ICU)์ ํตํ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง๋ง, ์ค์ ์ด ์ฅํฉํ ์ ์์ผ๋ฉฐ ํ๋ก์ ํธ๊ฐ ์ปค์ง์๋ก ์นดํ๋ก๊ทธ๊ฐ ์ค์ ์ง์คํ๋๋ ๊ฒฝํฅ์ด ์์ต๋๋ค.
- **Intlayer** - Next.js๋ฅผ ์ํ ์ปดํฌ๋ํธ ์ค์ฌ ์ฝํ
์ธ ๋ชจ๋ธ, **์๊ฒฉํ TS ํ์ดํ**, **๋น๋ ์ ๊ฒ์ฌ**, **ํธ๋ฆฌ ์์ดํน**, **๋ด์ฅ ๋ฏธ๋ค์จ์ด ๋ฐ SEO ๋์ฐ๋ฏธ**, ์ ํ์ **๋น์ฃผ์ผ ์๋ํฐ/CMS**, ๊ทธ๋ฆฌ๊ณ **AI ์ง์ ๋ฒ์ญ**์ ์ ๊ณตํฉ๋๋ค.
---
| Library | GitHub Stars | Total Commits | Last Commit | First Version | NPM Version | NPM Downloads |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `aymericzip/intlayer` | [](https://github.com/aymericzip/intlayer/stargazers) | [](https://github.com/aymericzip/intlayer/commits) | [](https://github.com/aymericzip/intlayer/commits) | April 2024 | [](https://www.npmjs.com/package/intlayer) | [](https://www.npmjs.com/package/intlayer) |
| `amannn/next-intl` | [](https://github.com/amannn/next-intl/stargazers) | [](https://github.com/amannn/next-intl/commits) | [](https://github.com/amannn/next-intl/commits) | Nov 2020 | [](https://www.npmjs.com/package/next-intl) | [](https://www.npmjs.com/package/next-intl) |
| `i18next/i18next` | [](https://github.com/i18next/i18next/stargazers) | [](https://github.com/i18next/i18next/commits) | [](https://github.com/i18next/i18next/commits) | Jan 2012 | [](https://www.npmjs.com/package/i18next) | [](https://www.npmjs.com/package/i18next) |
| `i18next/next-i18next` | [](https://github.com/i18next/next-i18next/stargazers) | [](https://github.com/i18next/next-i18next/commits) | [](https://github.com/i18next/next-i18next/commits) | Nov 2018 | [](https://www.npmjs.com/package/next-i18next) | [](https://www.npmjs.com/package/next-i18next) |
> ๋ฐฐ์ง๋ ์๋์ผ๋ก ์
๋ฐ์ดํธ๋ฉ๋๋ค. ์ค๋
์ท์ ์๊ฐ์ด ์ง๋จ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง ์ ์์ต๋๋ค.
---
## ๋๋ํ ๊ธฐ๋ฅ ๋น๊ต (Next.js ์ค์ฌ)
| ๊ธฐ๋ฅ | `next-intlayer` (Intlayer) | `next-intl` | `next-i18next` |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **์ปดํฌ๋ํธ ๊ทผ์ฒ ๋ฒ์ญ** | โ
์, ๊ฐ ์ปดํฌ๋ํธ์ ๋ด์ฉ์ด ํจ๊ป ์์น | โ ์๋์ | โ ์๋์ |
| **TypeScript ํตํฉ** | โ
๊ณ ๊ธ, ์๋ ์์ฑ ์๊ฒฉํ ํ์
| โ
์ข์ | โ ๏ธ ๊ธฐ๋ณธ |
| **๋ฒ์ญ ๋๋ฝ ๊ฐ์ง** | โ
TypeScript ์ค๋ฅ ํ์ด๋ผ์ดํธ ๋ฐ ๋น๋ ํ์ ์ค๋ฅ/๊ฒฝ๊ณ | โ ๏ธ ๋ฐํ์ ํด๋ฐฑ | โ ๏ธ ๋ฐํ์ ํด๋ฐฑ |
| **ํ๋ถํ ์ฝํ
์ธ (JSX/Markdown/์ปดํฌ๋ํธ)** | โ
์ง์ ์ง์ | โ ํ๋ถํ ๋
ธ๋๋ฅผ ์ํด ์ค๊ณ๋์ง ์์ | โ ๏ธ ์ ํ์ |
| **AI ๊ธฐ๋ฐ ๋ฒ์ญ** | โ
์, ์ฌ๋ฌ AI ์ ๊ณต์๋ฅผ ์ง์ํฉ๋๋ค. ์์ฒด API ํค๋ฅผ ์ฌ์ฉํ์ฌ ์ด์ฉ ๊ฐ๋ฅํ๋ฉฐ, ์ ํ๋ฆฌ์ผ์ด์
๊ณผ ์ฝํ
์ธ ๋ฒ์์ ๋งฅ๋ฝ์ ๊ณ ๋ คํฉ๋๋ค | โ ์๋์ | โ ์๋์ |
| **๋น์ฃผ์ผ ์๋ํฐ** | โ
์, ๋ก์ปฌ ๋น์ฃผ์ผ ์๋ํฐ + ์ ํ์ CMS; ์ฝ๋๋ฒ ์ด์ค ์ฝํ
์ธ ์ธ๋ถํ ๊ฐ๋ฅ; ์๋ฒ ๋ ๊ฐ๋ฅ | โ ์๋์ / ์ธ๋ถ ํ์งํ ํ๋ซํผ์ ํตํด ์ฌ์ฉ ๊ฐ๋ฅ | โ ์๋์ / ์ธ๋ถ ํ์งํ ํ๋ซํผ์ ํตํด ์ฌ์ฉ ๊ฐ๋ฅ |
| **ํ์งํ๋ ๋ผ์ฐํ
** | โ
์, ๊ธฐ๋ณธ์ ์ผ๋ก ํ์งํ๋ ๊ฒฝ๋ก ์ง์ (Next.js ๋ฐ Vite์ ํธํ) | โ
๋ด์ฅ, App Router๊ฐ `[locale]` ์ธ๊ทธ๋จผํธ ์ง์ | โ
๋ด์ฅ |
| **๋์ ๋ผ์ฐํธ ์์ฑ** | โ
์ | โ
์ | โ
์ |
| **๋ณต์ํ ์ฒ๋ฆฌ** | โ
์ด๊ฑฐํ ๊ธฐ๋ฐ ํจํด | โ
์ฐ์ | โ
์ฐ์ |
| **ํ์ ์ง์ (๋ ์ง, ์ซ์, ํตํ)** | โ
์ต์ ํ๋ ํฌ๋งคํฐ (๋ด๋ถ์ ์ผ๋ก Intl ์ฌ์ฉ) | โ
์ฐ์ํจ (Intl ํฌํผ) | โ
์ฐ์ํจ (Intl ํฌํผ) |
| **์ฝํ
์ธ ํ์** | โ
.tsx, .ts, .js, .json, .md, .txt, (.yaml ์์
์ค) | โ
.json, .js, .ts | โ ๏ธ .json |
| **ICU ์ง์** | โ ๏ธ ์์
์ค | โ
์ | โ ๏ธ ํ๋ฌ๊ทธ์ธ ํตํด (`i18next-icu`) |
| **SEO ๋์ฐ๋ฏธ (hreflang, ์ฌ์ดํธ๋งต)** | โ
๋ด์ฅ ๋๊ตฌ: ์ฌ์ดํธ๋งต, robots.txt, ๋ฉํ๋ฐ์ดํฐ ๋์ฐ๋ฏธ | โ
์ฐ์ | โ
์ฐ์ |
| **์์ฝ์์คํ
/ ์ปค๋ฎค๋ํฐ** | โ ๏ธ ์์ง๋ง ๋น ๋ฅด๊ฒ ์ฑ์ฅํ๊ณ ๋ฐ์์ฑ์ด ์ข์ | โ
์ข์ | โ
์ข์ |
| **์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง & ์๋ฒ ์ปดํฌ๋ํธ** | โ
์, SSR / React ์๋ฒ ์ปดํฌ๋ํธ์ ์ต์ ํ๋จ | โ ๏ธ ํ์ด์ง ๋จ์๋ก ์ง์๋๋ ์์ ์๋ฒ ์ปดํฌ๋ํธ์ t-ํจ์๋ฅผ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ํตํด ์ ๋ฌํด์ผ ํจ | โ ๏ธ ํ์ด์ง ๋จ์๋ก ์ง์๋๋ ์์ ์๋ฒ ์ปดํฌ๋ํธ์ t-ํจ์๋ฅผ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ํตํด ์ ๋ฌํด์ผ ํจ |
| **ํธ๋ฆฌ ์์ดํน (์ฌ์ฉ๋ ์ฝํ
์ธ ๋ง ๋ก๋)** | โ
์, Babel/SWC ํ๋ฌ๊ทธ์ธ์ ํตํ ๋น๋ ์ ์ปดํฌ๋ํธ๋ณ | โ ๏ธ ๋ถ๋ถ์ ์ง์ | โ ๏ธ ๋ถ๋ถ์ ์ง์ |
| **์ง์ฐ ๋ก๋ฉ** | โ
์, ๋ก์ผ์ผ๋ณ / ์ฌ์ ๋ณ | โ
์ (๊ฒฝ๋ก๋ณ/๋ก์ผ์ผ๋ณ), ๋ค์์คํ์ด์ค ๊ด๋ฆฌ ํ์ | โ
์ (๊ฒฝ๋ก๋ณ/๋ก์ผ์ผ๋ณ), ๋ค์์คํ์ด์ค ๊ด๋ฆฌ ํ์ |
| **์ฌ์ฉํ์ง ์๋ ์ฝํ
์ธ ์ ๋ฆฌ** | โ
์, ๋น๋ ์ ์ฌ์ ๋ณ๋ก | โ ์๋์, ๋ค์์คํ์ด์ค ๊ด๋ฆฌ๋ฅผ ํตํด ์๋์ผ๋ก ๊ด๋ฆฌ ๊ฐ๋ฅ | โ ์๋์, ๋ค์์คํ์ด์ค ๊ด๋ฆฌ๋ฅผ ํตํด ์๋์ผ๋ก ๊ด๋ฆฌ ๊ฐ๋ฅ |
| **๋๊ท๋ชจ ํ๋ก์ ํธ ๊ด๋ฆฌ** | โ
๋ชจ๋ํ ๊ถ์ฅ, ๋์์ธ ์์คํ
์ ์ ํฉ | โ
์ค์ ๊ณผ ํจ๊ป ๋ชจ๋ํ | โ
์ค์ ๊ณผ ํจ๊ป ๋ชจ๋ํ |
| **๋๋ฝ๋ ๋ฒ์ญ ํ
์คํธ (CLI/CI)** | โ
CLI: `npx intlayer content test` (CI ์นํ์ ๊ฐ์ฌ) | โ ๏ธ ๋ด์ฅ๋์ด ์์ง ์์; ๋ฌธ์์์๋ `npx @lingual/i18n-check` ์ฌ์ฉ ๊ถ์ฅ | โ ๏ธ ๋ด์ฅ๋์ด ์์ง ์์; i18next ๋๊ตฌ ๋๋ ๋ฐํ์ `saveMissing`์ ์์กด |
---
## ์๊ฐ
Next.js๋ ๊ตญ์ ํ๋ ๋ผ์ฐํ
(์: ๋ก์ผ์ผ ์ธ๊ทธ๋จผํธ)์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ง์ํฉ๋๋ค. ํ์ง๋ง ์ด ๊ธฐ๋ฅ๋ง์ผ๋ก๋ ๋ฒ์ญ์ ์ํํ์ง ์์ต๋๋ค. ์ฌ์ฉ์์๊ฒ ํ์งํ๋ ์ฝํ
์ธ ๋ฅผ ๋ ๋๋งํ๋ ค๋ฉด ์ฌ์ ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค.
๋ง์ i18n ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์กด์ฌํ์ง๋ง, ํ์ฌ Next.js ํ๊ฒฝ์์๋ next-i18next, next-intl, ๊ทธ๋ฆฌ๊ณ Intlayer ์ธ ๊ฐ์ง๊ฐ ์ฃผ๋ชฉ๋ฐ๊ณ ์์ต๋๋ค.
---
## ์ํคํ
์ฒ ๋ฐ ํ์ฅ์ฑ
- **next-intl / next-i18next**: ๊ธฐ๋ณธ์ ์ผ๋ก ๋ก์ผ์ผ๋ณ **์ค์ ์ง์ค์ ์นดํ๋ก๊ทธ**(๊ทธ๋ฆฌ๊ณ i18next์ **๋ค์์คํ์ด์ค**)๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์ด๊ธฐ์๋ ์ ์๋ํ์ง๋ง, ์ ์ฐจ ๊ฒฐํฉ๋๊ฐ ๋์์ง๊ณ ํค ๋ณ๊ฒฝ์ด ์ฆ์์ง๋ฉด์ ํฐ ๊ณต์ ์์ญ์ด ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
- **Intlayer**: ์ฝ๋์ **๊ณต๊ฐ์ ์ผ๋ก ํจ๊ป ์์นํ** ์ปดํฌ๋ํธ๋ณ(๋๋ ๊ธฐ๋ฅ๋ณ) ์ฌ์ ์ ๊ถ์ฅํฉ๋๋ค. ์ด๋ ์ธ์ง ๋ถํ๋ฅผ ์ค์ด๊ณ , UI ์กฐ๊ฐ์ ๋ณต์ /์ด์ ์ ์ฝ๊ฒ ํ๋ฉฐ, ํ ๊ฐ ์ถฉ๋์ ์ค์ฌ์ค๋๋ค. ์ฌ์ฉํ์ง ์๋ ์ฝํ
์ธ ๋ ์์ฐ์ค๋ฝ๊ฒ ๋ ์ฝ๊ฒ ๋ฐ๊ฒฌํ๊ณ ์ ๊ฑฐํ ์ ์์ต๋๋ค.
**์ค์ํ ์ด์ :** ๋๊ท๋ชจ ์ฝ๋๋ฒ ์ด์ค๋ ๋์์ธ ์์คํ
์ค์ ์์๋ **๋ชจ๋ํ๋ ์ฝํ
์ธ **๊ฐ ๋จ์ผ ์นดํ๋ก๊ทธ๋ณด๋ค ๋ ์ ํ์ฅ๋ฉ๋๋ค.
---
## ๋ฒ๋ค ํฌ๊ธฐ ๋ฐ ์์กด์ฑ
์ ํ๋ฆฌ์ผ์ด์
์ ๋น๋ํ ํ, ๋ฒ๋ค์ ๋ธ๋ผ์ฐ์ ๊ฐ ํ์ด์ง๋ฅผ ๋ ๋๋งํ๊ธฐ ์ํด ๋ก๋ํ๋ ์๋ฐ์คํฌ๋ฆฝํธ์
๋๋ค. ๋ฐ๋ผ์ ๋ฒ๋ค ํฌ๊ธฐ๋ ์ ํ๋ฆฌ์ผ์ด์
์ฑ๋ฅ์ ๋งค์ฐ ์ค์ํฉ๋๋ค.
๋ค๊ตญ์ด ์ ํ๋ฆฌ์ผ์ด์
๋ฒ๋ค ๋งฅ๋ฝ์์ ์ค์ํ ๋ ๊ฐ์ง ๊ตฌ์ฑ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- ์ ํ๋ฆฌ์ผ์ด์
์ฝ๋
- ๋ธ๋ผ์ฐ์ ๊ฐ ๋ก๋ํ๋ ์ฝํ
์ธ
## ์ ํ๋ฆฌ์ผ์ด์
์ฝ๋
์ด ๊ฒฝ์ฐ ์ ํ๋ฆฌ์ผ์ด์
์ฝ๋์ ์ค์์ฑ์ ๋ฏธ๋ฏธํฉ๋๋ค. ์ธ ๊ฐ์ง ์๋ฃจ์
๋ชจ๋ ํธ๋ฆฌ ์์ดํน(tree-shakable)์ด ๊ฐ๋ฅํ์ฌ, ์ฌ์ฉํ์ง ์๋ ์ฝ๋ ๋ถ๋ถ์ ๋ฒ๋ค์ ํฌํจ๋์ง ์์ต๋๋ค.
๋ค์์ ์ธ ๊ฐ์ง ์๋ฃจ์
์ ์ฌ์ฉํ ๋ค๊ตญ์ด ์ ํ๋ฆฌ์ผ์ด์
์์ ๋ธ๋ผ์ฐ์ ๊ฐ ๋ก๋ํ๋ ์๋ฐ์คํฌ๋ฆฝํธ ๋ฒ๋ค ํฌ๊ธฐ ๋น๊ต์
๋๋ค.
์ ํ๋ฆฌ์ผ์ด์
์์ ํฌ๋งคํฐ๊ฐ ํ์ํ์ง ์์ ๊ฒฝ์ฐ, ํธ๋ฆฌ ์์ดํน ํ ๋ด๋ณด๋ด์ง๋ ํจ์ ๋ชฉ๋ก์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- **next-intlayer**: `useIntlayer`, `useLocale`, `NextIntlClientProvider`, (๋ฒ๋ค ํฌ๊ธฐ 180.6 kB -> 78.6 kB (gzip))
- **next-intl**: `useTranslations`, `useLocale`, `NextIntlClientProvider`, (๋ฒ๋ค ํฌ๊ธฐ 101.3 kB -> 31.4 kB (gzip))
- **next-i18next**: `useTranslation`, `useI18n`, `I18nextProvider`, (๋ฒ๋ค ํฌ๊ธฐ 80.7 kB -> 25.5 kB (gzip))
์ด ํจ์๋ค์ React ์ปจํ
์คํธ/์ํ๋ฅผ ๊ฐ์ธ๋ ๋ํผ์ ๋ถ๊ณผํ๋ฏ๋ก, i18n ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋ฒ๋ค ํฌ๊ธฐ์ ๋ฏธ์น๋ ์ ์ฒด ์ํฅ์ ๋ฏธ๋ฏธํฉ๋๋ค.
> Intlayer๋ `useIntlayer` ํจ์์ ๋ ๋ง์ ๋ก์ง์ด ํฌํจ๋์ด ์์ด `next-intl` ๋ฐ `next-i18next`๋ณด๋ค ์ฝ๊ฐ ๋ ํฝ๋๋ค. ์ด๋ ๋งํฌ๋ค์ด๊ณผ `intlayer-editor` ํตํฉ๊ณผ ๊ด๋ จ์ด ์์ต๋๋ค.
## ์ฝํ
์ธ ๋ฐ ๋ฒ์ญ
์ด ๋ถ๋ถ์ ๊ฐ๋ฐ์๋ค์ด ์ข
์ข
๋ฌด์ํ์ง๋ง, 10๊ฐ์ ํ์ด์ง์ 10๊ฐ์ ์ธ์ด๋ก ๊ตฌ์ฑ๋ ์ ํ๋ฆฌ์ผ์ด์
์ ๊ฒฝ์ฐ๋ฅผ ์๊ฐํด ๋ด
์๋ค. ๊ณ์ฐ์ ๋จ์ํํ๊ธฐ ์ํด ๊ฐ ํ์ด์ง๊ฐ 100% ๊ณ ์ ํ ์ฝํ
์ธ ๋ฅผ ํตํฉํ๋ค๊ณ ๊ฐ์ ํด ๋ณด๊ฒ ์ต๋๋ค(์ค์ ๋ก๋ ํ์ด์ง ๊ฐ์ ์ค๋ณต๋๋ ์ฝํ
์ธ ๊ฐ ๋ง์ต๋๋ค. ์: ํ์ด์ง ์ ๋ชฉ, ํค๋, ํธํฐ ๋ฑ).
`/fr/about` ํ์ด์ง๋ฅผ ๋ฐฉ๋ฌธํ๋ ค๋ ์ฌ์ฉ์๋ ํน์ ์ธ์ด๋ก ๋ ํ ํ์ด์ง์ ์ฝํ
์ธ ๋ฅผ ๋ก๋ํ๊ฒ ๋ฉ๋๋ค. ์ฝํ
์ธ ์ต์ ํ๋ฅผ ๋ฌด์ํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์
์ฝํ
์ธ ์ 8,200% `((1 + (((10 ํ์ด์ง - 1) ร (10 ์ธ์ด - 1)))) ร 100)`๋ฅผ ๋ถํ์ํ๊ฒ ๋ก๋ํ๋ ์
์
๋๋ค. ๋ฌธ์ ๋ฅผ ์ดํดํ์
จ๋์? ์ด ์ฝํ
์ธ ๊ฐ ํ
์คํธ๋ก๋ง ๋จ์ ์๋๋ผ๋, ์๋ง ์ฌ์ดํธ์ ์ด๋ฏธ์ง ์ต์ ํ๋ฅผ ๋ ์ ๊ฒฝ ์ฐ๊ณ ์ถ๊ฒ ์ง๋ง, ์ ์ธ๊ณ์ ๋ถํ์ํ ์ฝํ
์ธ ๋ฅผ ์ ์กํ๊ณ ์ฌ์ฉ์์ ์ปดํจํฐ๊ฐ ์๋ฌด ์ธ๋ชจ ์์ด ์ด๋ฅผ ์ฒ๋ฆฌํ๊ฒ ๋ง๋๋ ๊ฒ์
๋๋ค.
๋ ๊ฐ์ง ์ค์ํ ๋ฌธ์ :
- **๋ผ์ฐํธ๋ณ ๋ถํ :**
> ๋ด๊ฐ `/about` ํ์ด์ง์ ์๋ค๋ฉด, `/home` ํ์ด์ง์ ์ฝํ
์ธ ๋ฅผ ๋ก๋ํ๊ณ ์ถ์ง ์์ต๋๋ค.
- **๋ก์ผ์ผ๋ณ ๋ถํ :**
> ๋ด๊ฐ `/fr/about` ํ์ด์ง์ ์๋ค๋ฉด, `/en/about` ํ์ด์ง์ ์ฝํ
์ธ ๋ฅผ ๋ก๋ํ๊ณ ์ถ์ง ์์ต๋๋ค.
๋ค์ ๋งํ์ง๋ง, ์ธ ๊ฐ์ง ์๋ฃจ์
๋ชจ๋ ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ์ธ์งํ๊ณ ์์ผ๋ฉฐ ์ด๋ฌํ ์ต์ ํ๋ฅผ ๊ด๋ฆฌํ ์ ์๋๋ก ํฉ๋๋ค. ์ธ ์๋ฃจ์
๊ฐ์ ์ฐจ์ด๋ DX(๊ฐ๋ฐ์ ๊ฒฝํ)์ ์์ต๋๋ค.
`next-intl`๊ณผ `next-i18next`๋ ์ค์ ์ง์ค์ ์ ๊ทผ ๋ฐฉ์์ ์ฌ์ฉํ์ฌ ๋ฒ์ญ์ ๊ด๋ฆฌํ๋ฉฐ, ๋ก์ผ์ผ๋ณ ๋ฐ ํ์ ํ์ผ๋ณ๋ก JSON์ ๋ถํ ํ ์ ์์ต๋๋ค. `next-i18next`์์๋ JSON ํ์ผ์ '๋ค์์คํ์ด์ค(namespaces)'๋ผ๊ณ ๋ถ๋ฅด๊ณ , `next-intl`์ ๋ฉ์์ง๋ฅผ ์ ์ธํ ์ ์๊ฒ ํฉ๋๋ค. `intlayer`์์๋ JSON ํ์ผ์ '์ฌ์ (dictionaries)'์ด๋ผ๊ณ ๋ถ๋ฆ
๋๋ค.
- `next-intl`์ ๊ฒฝ์ฐ, `next-i18next`์ ๋ง์ฐฌ๊ฐ์ง๋ก, ์ฝํ
์ธ ๊ฐ ํ์ด์ง/๋ ์ด์์ ์์ค์์ ๋ก๋๋ ํ ์ด ์ฝํ
์ธ ๊ฐ ์ปจํ
์คํธ ํ๋ก๋ฐ์ด๋์ ๋ก๋๋ฉ๋๋ค. ์ด๋ ๊ฐ๋ฐ์๊ฐ ๊ฐ ํ์ด์ง์ ๋ก๋๋ JSON ํ์ผ์ ์๋์ผ๋ก ๊ด๋ฆฌํด์ผ ํจ์ ์๋ฏธํฉ๋๋ค.
> ์ค์ ๋ก๋ ๊ฐ๋ฐ์๋ค์ด ์ด ์ต์ ํ๋ฅผ ์ข
์ข
๊ฑด๋๋ฐ๊ณ , ๋จ์์ฑ์ ์ํด ํ์ด์ง์ ์ปจํ
์คํธ ํ๋ก๋ฐ์ด๋์ ๋ชจ๋ ์ฝํ
์ธ ๋ฅผ ๋ก๋ํ๋ ๋ฐฉ์์ ์ ํธํฉ๋๋ค.
- `intlayer`์ ๊ฒฝ์ฐ, ๋ชจ๋ ์ฝํ
์ธ ๊ฐ ์ ํ๋ฆฌ์ผ์ด์
๋ด์์ ๋ก๋๋ฉ๋๋ค. ๊ทธ๋ฐ ๋ค์ ํ๋ฌ๊ทธ์ธ(`@intlayer/babel` / `@intlayer/swc`)์ด ํ์ด์ง์์ ์ฌ์ฉ๋๋ ์ฝํ
์ธ ๋ง ๋ก๋ํ๋๋ก ๋ฒ๋ค์ ์ต์ ํํฉ๋๋ค. ๋ฐ๋ผ์ ๊ฐ๋ฐ์๋ ๋ก๋๋ ์ฌ์ ์ ์๋์ผ๋ก ๊ด๋ฆฌํ ํ์๊ฐ ์์ต๋๋ค. ์ด๋ ๋ ๋์ ์ต์ ํ, ๋ ๋์ ์ ์ง๋ณด์์ฑ, ๊ทธ๋ฆฌ๊ณ ๊ฐ๋ฐ ์๊ฐ ๋จ์ถ์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค.
์ ํ๋ฆฌ์ผ์ด์
์ด ์ปค์ง์๋ก(ํนํ ์ฌ๋ฌ ๊ฐ๋ฐ์๊ฐ ํจ๊ป ์์
ํ ๋) ๋ ์ด์ ์ฌ์ฉ๋์ง ์๋ ์ฝํ
์ธ ๋ฅผ JSON ํ์ผ์์ ์ ๊ฑฐํ๋ ๊ฒ์ ์๋ ๊ฒฝ์ฐ๊ฐ ํํฉ๋๋ค.
> ๋ชจ๋ ๊ฒฝ์ฐ์ JSON์ด ๋ชจ๋ ๋ก๋๋๋ค๋ ์ ์ ์ ์ํ์ธ์ (next-intl, next-i18next, intlayer).
์ด๊ฒ์ด Intlayer์ ์ ๊ทผ ๋ฐฉ์์ด ๋ ์ฑ๋ฅ์ด ์ข์ ์ด์ ์
๋๋ค: ์ปดํฌ๋ํธ๊ฐ ๋ ์ด์ ์ฌ์ฉ๋์ง ์์ผ๋ฉด ํด๋น ์ฌ์ ์ ๋ฒ๋ค์ ๋ก๋๋์ง ์์ต๋๋ค.
๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํด๋ฐฑ(fallback)์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์๋ ์ค์ํฉ๋๋ค. ์ ํ๋ฆฌ์ผ์ด์
์ด ๊ธฐ๋ณธ์ ์ผ๋ก ์์ด๋ก ์ค์ ๋์ด ์๊ณ ์ฌ์ฉ์๊ฐ `/fr/about` ํ์ด์ง๋ฅผ ๋ฐฉ๋ฌธํ๋ค๊ณ ๊ฐ์ ํด ๋ด
์๋ค. ํ๋์ค์ด ๋ฒ์ญ์ด ์๋ ๊ฒฝ์ฐ ์์ด ํด๋ฐฑ์ ๊ณ ๋ คํฉ๋๋ค.
`next-intl` ๋ฐ `next-i18next`์ ๊ฒฝ์ฐ, ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํ์ฌ ๋ก์ผ์ผ๊ณผ ํด๋ฐฑ ๋ก์ผ์ผ์ ๊ด๋ จ๋ JSON์ ๋ชจ๋ ๋ก๋ํด์ผ ํฉ๋๋ค. ๋ฐ๋ผ์ ๋ชจ๋ ์ฝํ
์ธ ๊ฐ ๋ฒ์ญ๋์๋ค๊ณ ๊ฐ์ ํ๋ฉด, ๊ฐ ํ์ด์ง๋ 100% ๋ถํ์ํ ์ฝํ
์ธ ๋ฅผ ๋ก๋ํ๊ฒ ๋ฉ๋๋ค. **๋ฐ๋ฉด์, `intlayer`๋ ์ฌ์ ๋น๋ ์์ ์ ํด๋ฐฑ์ ์ฒ๋ฆฌํฉ๋๋ค. ๋ฐ๋ผ์ ๊ฐ ํ์ด์ง๋ ์ฌ์ฉ๋ ์ฝํ
์ธ ๋ง ๋ก๋ํฉ๋๋ค.**
๋ค์์ vite + react ์ ํ๋ฆฌ์ผ์ด์
์์ `intlayer`๋ฅผ ์ฌ์ฉํ ๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ์ ์ํฅ ์์์
๋๋ค:
| ์ต์ ํ๋ ๋ฒ๋ค | ์ต์ ํ๋์ง ์์ ๋ฒ๋ค |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|  |  |
---
## TypeScript ๋ฐ ์์ ์ฑ
<Columns>
<Column>
**next-intl**
- ๊ฒฌ๊ณ ํ TypeScript ์ง์์ ์ ๊ณตํ์ง๋ง, **ํค๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๊ฒฉํ๊ฒ ํ์
์ง์ ๋์ง ์์**; ์์ ์ฑ ํจํด์ ์๋์ผ๋ก ์ ์งํด์ผ ํฉ๋๋ค.
</Column>
<Column>
**next-i18next**
- ํ
์ ๋ํ ๊ธฐ๋ณธ ํ์
์ ์ ์ ๊ณต; **์๊ฒฉํ ํค ํ์
์ง์ ์ ์ถ๊ฐ ๋๊ตฌ/๊ตฌ์ฑ์ด ํ์**ํฉ๋๋ค.
</Column>
<Column>
**intlayer**
- **์ฝํ
์ธ ์์ ์๊ฒฉํ ํ์
์ ์์ฑํฉ๋๋ค.** **IDE ์๋์์ฑ**๊ณผ **์ปดํ์ผ ํ์ ์ค๋ฅ**๊ฐ ๋ฐฐํฌ ์ ์ ์คํ์ ๋๋ฝ๋ ํค๋ฅผ ์ก์๋
๋๋ค.
</Column>
</Columns>
**์ค์ํ ์ด์ :** ๊ฐ๋ ฅํ ํ์
์ง์ ์ ์คํจ๋ฅผ **์ค๋ฅธ์ชฝ(๋ฐํ์)**์ด ์๋ **์ผ์ชฝ(CI/๋น๋)**์ผ๋ก ์ด๋์ํต๋๋ค.
---
## ๋๋ฝ๋ ๋ฒ์ญ ์ฒ๋ฆฌ
**next-intl**
- **๋ฐํ์ ํด๋ฐฑ**์ ์์กดํฉ๋๋ค(์: ํค ๋๋ ๊ธฐ๋ณธ ๋ก์ผ์ผ ํ์). ๋น๋๋ ์คํจํ์ง ์์ต๋๋ค.
**next-i18next**
- **๋ฐํ์ ํด๋ฐฑ**์ ์์กดํฉ๋๋ค(์: ํค ๋๋ ๊ธฐ๋ณธ ๋ก์ผ์ผ ํ์). ๋น๋๋ ์คํจํ์ง ์์ต๋๋ค.
**intlayer**
- ๋๋ฝ๋ ๋ก์ผ์ผ์ด๋ ํค์ ๋ํด **๋น๋ ํ์ ๊ฐ์ง**์ **๊ฒฝ๊ณ /์ค๋ฅ**๋ฅผ ์ ๊ณตํฉ๋๋ค.
**์ค์ํ ์ด์ :** ๋น๋ ์ค์ ๋๋ฝ์ ์ก์ผ๋ฉด ํ๋ก๋์
์์ โ๋ฏธ์คํฐ๋ฆฌ ๋ฌธ์์ดโ์ ๋ฐฉ์งํ๊ณ ์๊ฒฉํ ๋ฆด๋ฆฌ์ค ๊ฒ์ดํธ์ ์ผ์นํฉ๋๋ค.
---
## ๋ผ์ฐํ
, ๋ฏธ๋ค์จ์ด ๋ฐ URL ์ ๋ต
<Columns>
<Column>
**next-intl**
- App Router์์ **Next.js ์ง์ญํ ๋ผ์ฐํ
**๊ณผ ํจ๊ป ์๋ํฉ๋๋ค.
</Column>
<Column>
**next-i18next**
- App Router์์ **Next.js ์ง์ญํ ๋ผ์ฐํ
**๊ณผ ํจ๊ป ์๋ํฉ๋๋ค.
</Column>
<Column>
**intlayer**
- ์์ ๋ชจ๋ ๊ธฐ๋ฅ์ ๋ํด, **i18n ๋ฏธ๋ค์จ์ด**(ํค๋/์ฟ ํค๋ฅผ ํตํ ๋ก์ผ์ผ ๊ฐ์ง)์ ์ง์ญํ๋ URL ๋ฐ `<link rel="alternate" hreflang="โฆ">` ํ๊ทธ ์์ฑ์ ์ํ **๋์ฐ๋ฏธ**๋ฅผ ์ ๊ณตํฉ๋๋ค.
</Column>
</Columns>
**์ค์ํ ์ด์ :** ์ปค์คํ
์ฐ๊ฒฐ ๊ณ์ธต์ด ์ค์ด๋ค๊ณ , **์ผ๊ด๋ ์ฌ์ฉ์ ๊ฒฝํ(UX)**๊ณผ **๊น๋ํ SEO**๋ฅผ ์ง์ญ๋ณ๋ก ์ ์งํ ์ ์์ต๋๋ค.
---
## ์๋ฒ ์ปดํฌ๋ํธ(RSC) ์ ๋ ฌ
<Columns>
<Column>
**next-intl**
- Next.js 13+๋ฅผ ์ง์ํฉ๋๋ค. ํ์ด๋ธ๋ฆฌ๋ ์ค์ ์์ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ํตํด t-ํจ์/ํฌ๋งคํฐ๋ฅผ ์ ๋ฌํด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
</Column>
<Column>
**next-i18next**
- Next.js 13+๋ฅผ ์ง์ํฉ๋๋ค. ๊ฒฝ๊ณ ๊ฐ์ ๋ฒ์ญ ์ ํธ๋ฆฌํฐ๋ฅผ ์ ๋ฌํ๋ ๋ฐ ์ ์ฌํ ์ ์ฝ์ด ์์ต๋๋ค.
</Column>
<Column>
**intlayer**
- Next.js 13+๋ฅผ ์ง์ํ๋ฉฐ, ์ผ๊ด๋ API์ RSC ์งํฅ ํ๋ก๋ฐ์ด๋๋ฅผ ํตํด **์๋ฒ/ํด๋ผ์ด์ธํธ ๊ฒฝ๊ณ**๋ฅผ ์ํํ๊ฒ ์ฒ๋ฆฌํ์ฌ ํฌ๋งคํฐ๋ t-ํจ์๋ฅผ ์ ๋ฌํ๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค.
</Column>
</Columns>
**์ค์ํ ์ด์ :** ๋ ๊น๋ํ ์ ์ ๋ชจ๋ธ๊ณผ ํ์ด๋ธ๋ฆฌ๋ ํธ๋ฆฌ์์์ ์์ธ ์ํฉ ๊ฐ์.
---
## ๊ฐ๋ฐ์ ๊ฒฝํ(DX), ๋๊ตฌ ๋ฐ ์ ์ง๋ณด์
<Columns>
<Column>
**next-intl**
- ์ธ๋ถ ์ง์ญํ ํ๋ซํผ ๋ฐ ํธ์ง ์ํฌํ๋ก์ฐ์ ์์ฃผ ํจ๊ป ์ฌ์ฉ๋ฉ๋๋ค.
</Column>
<Column>
**next-i18next**
- ์ธ๋ถ ์ง์ญํ ํ๋ซํผ ๋ฐ ํธ์ง ์ํฌํ๋ก์ฐ์ ์์ฃผ ํจ๊ป ์ฌ์ฉ๋ฉ๋๋ค.
</Column>
<Column>
**intlayer**
- **๋ฌด๋ฃ ๋น์ฃผ์ผ ์๋ํฐ**์ **์ ํ์ CMS**(Git ์นํ์ ์ด๊ฑฐ๋ ์ธ๋ถํ ๊ฐ๋ฅ)๋ฅผ ์ ๊ณตํ๋ฉฐ, **VSCode ํ์ฅ**๊ณผ ์์ฒด ์ ๊ณต์ ํค๋ฅผ ์ฌ์ฉํ๋ **AI ์ง์ ๋ฒ์ญ**๋ ํฌํจ๋ฉ๋๋ค.
</Column>
</Columns>
**์ค์ํ ์ด์ :** ์ด์ ๋น์ฉ์ ๋ฎ์ถ๊ณ ๊ฐ๋ฐ์์ ์ฝํ
์ธ ์์ฑ์ ๊ฐ์ ํผ๋๋ฐฑ ์ฃผ๊ธฐ๋ฅผ ๋จ์ถํฉ๋๋ค.
## ํ์งํ ํ๋ซํผ(TMS)๊ณผ์ ํตํฉ
๋๊ท๋ชจ ์กฐ์ง์ ์ข
์ข
**Crowdin**, **Phrase**, **Lokalise**, **Localizely**, ๋๋ **Localazy**์ ๊ฐ์ ๋ฒ์ญ ๊ด๋ฆฌ ์์คํ
(TMS)์ ์์กดํฉ๋๋ค.
- **๊ธฐ์
์ด ๊ด์ฌ์ ๊ฐ์ง๋ ์ด์ **
- **ํ์
๋ฐ ์ญํ ๋ถ๋ด**: ๊ฐ๋ฐ์, ์ ํ ๊ด๋ฆฌ์, ๋ฒ์ญ๊ฐ, ๊ฒํ ์, ๋ง์ผํ
ํ ๋ฑ ์ฌ๋ฌ ์ดํด๊ด๊ณ์๊ฐ ์ฐธ์ฌํฉ๋๋ค.
- **๊ท๋ชจ ๋ฐ ํจ์จ์ฑ**: ์ง์์ ์ธ ํ์งํ์ ๋งฅ๋ฝ ๋ด ๋ฆฌ๋ทฐ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
- **next-intl / next-i18next**
- ์ผ๋ฐ์ ์ผ๋ก **์ค์ ์ง์ค์ JSON ์นดํ๋ก๊ทธ**๋ฅผ ์ฌ์ฉํ๋ฏ๋ก TMS์์ ๋ด๋ณด๋ด๊ธฐ/๊ฐ์ ธ์ค๊ธฐ๊ฐ ๊ฐ๋จํฉ๋๋ค.
- ์ ํ๋ซํผ๋ค์ ๋ํ ์ฑ์ํ ์ํ๊ณ์ ์์ /ํตํฉ ๊ธฐ๋ฅ์ด ์กด์ฌํฉ๋๋ค.
- **Intlayer**
- **๋ถ์ฐํ, ์ปดํฌ๋ํธ๋ณ ์ฌ์ **์ ๊ถ์ฅํ๋ฉฐ **TypeScript/TSX/JS/JSON/MD** ์ฝํ
์ธ ๋ฅผ ์ง์ํฉ๋๋ค.
- ์ด๋ ์ฝ๋์ ๋ชจ๋์ฑ์ ํฅ์์ํค์ง๋ง, ๋๊ตฌ๊ฐ ์ค์ ์ง์ค์ ํ๋ฉด JSON ํ์ผ์ ๊ธฐ๋ํ ๊ฒฝ์ฐ ํ๋ฌ๊ทธ ์ค ํ๋ ์ด ๋ฐฉ์์ TMS ํตํฉ์ ์ด๋ ต๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
- Intlayer๋ ๋์์ผ๋ก **AI ์ง์ ๋ฒ์ญ**(์์ฒด ์ ๊ณต์ ํค ์ฌ์ฉ), **๋น์ฃผ์ผ ์๋ํฐ/CMS**, ๊ทธ๋ฆฌ๊ณ **CLI/CI** ์ํฌํ๋ก์ฐ๋ฅผ ์ ๊ณตํ์ฌ ๋๋ฝ๋ ๋ถ๋ถ์ ์ฐพ์ ์ฑ์ธ ์ ์๋๋ก ํฉ๋๋ค.
> ์ฐธ๊ณ : `next-intl`๊ณผ `i18next`๋ TypeScript ์นดํ๋ก๊ทธ๋ฅผ ์ง์ํฉ๋๋ค. ๋ง์ฝ ํ์์ ๋ฉ์์ง๋ฅผ `.ts` ํ์ผ์ ์ ์ฅํ๊ฑฐ๋ ๊ธฐ๋ฅ๋ณ๋ก ๋ถ์ฐ ๊ด๋ฆฌํ๋ค๋ฉด, ์ ์ฌํ TMS ๋ง์ฐฐ์ ๊ฒช์ ์ ์์ต๋๋ค. ํ์ง๋ง ๋ง์ `next-intl` ์ค์ ์ ์ฌ์ ํ `locales/` ํด๋์ ์ค์ ์ง์คํ๋์ด ์์ด TMS์ฉ JSON์ผ๋ก ๋ฆฌํฉํ ๋งํ๊ธฐ๊ฐ ์กฐ๊ธ ๋ ์ฝ์ต๋๋ค.
## ๊ฐ๋ฐ์ ๊ฒฝํ
์ด ๋ถ๋ถ์์๋ ์ธ ๊ฐ์ง ์๋ฃจ์
์ ๊น์ด ๋น๊ตํฉ๋๋ค. ๊ฐ ์๋ฃจ์
์ '์์ํ๊ธฐ' ๋ฌธ์์ ์ค๋ช
๋ ๊ฐ๋จํ ์ฌ๋ก๋ฅผ ๊ณ ๋ คํ๊ธฐ๋ณด๋ค๋, ์ค์ ํ๋ก์ ํธ์ ๋ ์ ์ฌํ ์ค์ ์ฌ์ฉ ์ฌ๋ก๋ฅผ ๊ณ ๋ คํ ๊ฒ์
๋๋ค.
### ์ฑ ๊ตฌ์กฐ
์ฑ ๊ตฌ์กฐ๋ ์ฝ๋๋ฒ ์ด์ค์ ์ ์ง๋ณด์์ฑ์ ๋ณด์ฅํ๋ ๋ฐ ์ค์ํฉ๋๋ค.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
```bash
.
โโโ i18n.config.ts
โโโ src
โโโ locales
โ โโโ en
โ โ โโโ common.json
โ โ โโโ about.json
โ โโโ fr
โ โโโ common.json
โ โโโ about.json
โโโ app
โ โโโ i18n
โ โ โโโ server.ts
โ โโโ [locale]
โ โโโ layout.tsx
โ โโโ about.tsx
โโโ components
โโโ I18nProvider.tsx
โโโ ClientComponent.tsx
โโโ ServerComponent.tsx
```
</TabItem>
<TabItem label="next-intl" value="next-intl">
```bash
.
โโโ i18n.ts
โโโ locales
โ โโโ en
โ โ โโโ home.json
โ โ โโโ navbar.json
โ โโโ fr
โ โ โโโ home.json
โ โ โโโ navbar.json
โ โโโ es
โ โโโ home.json
โ โโโ navbar.json
โโโ src
โโโ middleware.ts
โโโ app
โ โโโ i18n
โ โ โโโ server.ts
โ โโโ [locale]
โ โโโ home.tsx
โโโ components
โโโ Navbar
โโโ index.tsx
```
</TabItem>
<TabItem label="intlayer" value="intlayer">
```bash
.
โโโ intlayer.config.ts
โโโ src
โโโ middleware.ts
โโโ app
โ โโโ [locale]
โ โโโ layout.tsx
โ โโโ home
โ โโโ index.tsx
โ โโโ index.content.ts
โโโ components
โโโ Navbar
โโโ index.tsx
โโโ index.content.ts
```
</TabItem>
</Tab>
#### ๋น๊ต
- **next-intl / next-i18next**: ์ค์ ์ง์ค์ ์นดํ๋ก๊ทธ(JSON; ๋ค์์คํ์ด์ค/๋ฉ์์ง). ๋ช
ํํ ๊ตฌ์กฐ, ๋ฒ์ญ ํ๋ซํผ๊ณผ ์ ํตํฉ๋์ง๋ง, ์ฑ์ด ์ปค์ง์๋ก ์ฌ๋ฌ ํ์ผ์ ๋์์ ์์ ํด์ผ ํ ๊ฐ๋ฅ์ฑ์ด ๋์์ง๋๋ค.
- **Intlayer**: ์ปดํฌ๋ํธ๋ณ `.content.{ts|js|json}` ์ฌ์ ์ ์ปดํฌ๋ํธ์ ํจ๊ป ๋ฐฐ์น. ์ปดํฌ๋ํธ ์ฌ์ฌ์ฉ๊ณผ ๋ก์ปฌ ๋จ์ ์ดํด๊ฐ ์ฉ์ดํ๋ฉฐ, ํ์ผ์ด ์ถ๊ฐ๋๊ณ ๋น๋ ํ์ ๋๊ตฌ์ ์์กดํฉ๋๋ค.
#### ์ค์ ๋ฐ ์ฝํ
์ธ ๋ก๋ฉ
์์ ์ธ๊ธํ๋ฏ์ด, ๊ฐ JSON ํ์ผ์ด ์ฝ๋์ ์ด๋ป๊ฒ ์ํฌํธ๋๋์ง ์ต์ ํํด์ผ ํฉ๋๋ค.
๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ฝํ
์ธ ๋ก๋ฉ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ด ์ค์ํฉ๋๋ค.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
```tsx fileName="next-i18next.config.js"
module.exports = {
i18n: {
locales: ["en", "fr", "es"],
defaultLocale: "en",
},
};
```
```tsx fileName="src/app/_app.tsx"
import { appWithTranslation } from "next-i18next";
const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;
export default appWithTranslation(MyApp);
```
```tsx fileName="src/app/[locale]/about/page.tsx"
import type { GetStaticProps } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useTranslation } from "next-i18next";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { createInstance } from "i18next";
import { ClientComponent, ServerComponent } from "@components";
export default function HomePage({ locale }: { locale: string }) {
// ์ด ์ปดํฌ๋ํธ์์ ์ฌ์ฉํ๋ ๋ค์์คํ์ด์ค๋ฅผ ๋ช
์์ ์ผ๋ก ์ ์ธํฉ๋๋ค
const resources = await loadMessagesFor(locale); // ๋ก๋ (JSON ๋ฑ)
const i18n = createInstance();
i18n.use(initReactI18next).init({
lng: locale,
fallbackLng: "en",
resources,
ns: ["common", "about"],
defaultNS: "common",
interpolation: { escapeValue: false },
});
const { t } = useTranslation("about");
return (
<I18nextProvider i18n={i18n}>
<main>
<h1>{t("title")}</h1>
<ClientComponent />
<ServerComponent />
</main>
</I18nextProvider>
);
}
export const getStaticProps: GetStaticProps = async ({ locale }) => {
// ์ด ํ์ด์ง์ ํ์ํ ๋ค์์คํ์ด์ค๋ง ๋ฏธ๋ฆฌ ๋ก๋ํฉ๋๋ค
return {
props: {
...(await serverSideTranslations(locale ?? "en", ["common", "about"])),
},
};
};
```
</TabItem>
<TabItem label="next-intl" value="next-intl">
```tsx fileName="i18n.ts"
import { getRequestConfig } from "next-intl/server";
import { notFound } from "next/navigation";
// ๊ณตํต ์ค์ ์์ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค
const locales = ["en", "fr", "es"];
export default getRequestConfig(async ({ locale }) => {
// ๋ค์ด์ค๋ `locale` ๋งค๊ฐ๋ณ์๊ฐ ์ ํจํ์ง ๊ฒ์ฆํฉ๋๋ค
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`../messages/${locale}.json`)).default,
};
});
```
```tsx fileName="src/app/[locale]/about/layout.tsx"
import { NextIntlClientProvider } from "next-intl";
import { getMessages, unstable_setRequestLocale } from "next-intl/server";
import pick from "lodash/pick";
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const { locale } = params;
// ์ด ์๋ฒ ๋ ๋๋ง(RSC)์ ์ํ ํ์ฑ ์์ฒญ ๋ก์ผ์ผ ์ค์
unstable_setRequestLocale(locale);
// ๋ฉ์์ง๋ src/i18n/request.ts๋ฅผ ํตํด ์๋ฒ ์ธก์์ ๋ก๋๋ฉ๋๋ค
// (next-intl ๋ฌธ์ ์ฐธ์กฐ). ์ฌ๊ธฐ์๋ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์ ํ์ํ
// ์ผ๋ถ ๋ฉ์์ง๋ง ํด๋ผ์ด์ธํธ๋ก ์ ๋ฌํฉ๋๋ค (ํ์ด๋ก๋ ์ต์ ํ).
const messages = await getMessages();
const clientMessages = pick(messages, ["common", "about"]);
const rtlLocales = ["ar", "he", "fa", "ur"];
return (
<html lang={locale} dir={rtlLocales.includes(locale) ? "rtl" : "ltr"}>
<body>
<NextIntlClientProvider locale={locale} messages={clientMessages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
```
```tsx fileName="src/app/[locale]/about/page.tsx"
import { getTranslations } from "next-intl/server";
import { ClientComponent, ServerComponent } from "@components";
export default async function LandingPage({
params,
}: {
params: { locale: string };
}) {
// ์๊ฒฉํ๊ฒ ์๋ฒ ์ธก์์๋ง ๋ก๋๋จ (ํด๋ผ์ด์ธํธ์ ํ์ด๋๋ ์ด์
๋์ง ์์)
const t = await getTranslations("about");
return (
<main>
<h1>{t("title")}</h1>
<ClientComponent />
<ServerComponent />
</main>
);
}
```
</TabItem>
<TabItem label="intlayer" value="intlayer">
```tsx fileName="intlayer.config.ts"
export default {
internationalization: {
locales: ["en", "fr", "es"],
defaultLocale: "en",
},
};
```
```tsx fileName="src/app/[locale]/layout.tsx"
import { getHTMLTextDir } from "intlayer";
import {
IntlayerClientProvider,
generateStaticParams,
type NextLayoutIntlayer,
} from "next-intlayer";
export const dynamic = "force-static";
const LandingLayout: NextLayoutIntlayer = async ({ children, params }) => {
const { locale } = await params;
return (
<html lang={locale} dir={getHTMLTextDir(locale)}>
<IntlayerClientProvider locale={locale}>
{children}
</IntlayerClientProvider>
</html>
);
};
export default LandingLayout;
```
```tsx fileName="src/app/[locale]/about/page.tsx"
import { PageContent } from "@components/PageContent";
import type { NextPageIntlayer } from "next-intlayer";
import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";
import { ClientComponent, ServerComponent } from "@components";
const LandingPage: NextPageIntlayer = async ({ params }) => {
const { locale } = await params;
const { title } = useIntlayer("about", locale);
return (
<IntlayerServerProvider locale={locale}>
<main>
<h1>{title}</h1>
<ClientComponent />
<ServerComponent />
</main>
</IntlayerServerProvider>
);
};
export default LandingPage;
```
</TabItem>
</Tab>
#### ๋น๊ต
์ธ ๊ฐ์ง ๋ชจ๋ ๋ก์ผ์ผ๋ณ ์ฝํ
์ธ ๋ก๋ฉ๊ณผ ํ๋ก๋ฐ์ด๋๋ฅผ ์ง์ํฉ๋๋ค.
- **next-intl/next-i18next**๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ, ์ผ๋ฐ์ ์ผ๋ก ๊ฒฝ๋ก๋ณ๋ก ์ ํ๋ ๋ฉ์์ง/๋ค์์คํ์ด์ค๋ฅผ ๋ก๋ํ๊ณ ํ์ํ ์์น์ ํ๋ก๋ฐ์ด๋๋ฅผ ๋ฐฐ์นํฉ๋๋ค.
- **Intlayer**๋ ๋น๋ ์์ ๋ถ์์ ์ถ๊ฐํ์ฌ ์ฌ์ฉ์ ์ถ๋ก ํ๋ฏ๋ก ์๋ ์ฐ๊ฒฐ์ ์ค์ด๊ณ ๋จ์ผ ๋ฃจํธ ํ๋ก๋ฐ์ด๋๋ฅผ ํ์ฉํ ์ ์์ต๋๋ค.
ํ์ ์ ํธ๋์ ๋ฐ๋ผ ๋ช
์์ ์ ์ด์ ์๋ํ ์ค์์ ์ ํํ์ธ์.
### ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์์ ์ฌ์ฉ๋ฒ
์นด์ดํฐ๋ฅผ ๋ ๋๋งํ๋ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ ์์ ๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
**๋ฒ์ญ (์ค์ JSON์ด์ด์ผ ํ๋ฉฐ `public/locales/...`์ ์์นํด์ผ ํฉ๋๋ค)**
```json fileName="public/locales/en/about.json"
{
"counter": {
"label": "Counter",
"increment": "Increment"
}
}
```
```json fileName="public/locales/fr/about.json"
{
"counter": {
"label": "์นด์ดํฐ",
"increment": "์ฆ๊ฐ"
}
}
```
**ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ**
```tsx fileName="src/components/ClientComponentExample.tsx"
"use client";
import React, { useMemo, useState } from "react";
import { useTranslation } from "next-i18next";
const ClientComponentExample = () => {
const { t, i18n } = useTranslation("about");
const [count, setCount] = useState(0);
// next-i18next๋ useNumber๋ฅผ ์ ๊ณตํ์ง ์์ผ๋ฏ๋ก Intl.NumberFormat ์ฌ์ฉ
const numberFormat = new Intl.NumberFormat(i18n.language);
return (
<div>
<p>{numberFormat.format(count)}</p>
<button
aria-label={t("counter.label")}
onClick={() => setCount((count) => count + 1)}
>
{t("counter.increment")}
</button>
</div>
);
};
```
> ํ์ด์ง serverSideTranslations์ "about" ๋ค์์คํ์ด์ค๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ ์์ง ๋ง์ธ์
> ์ฌ๊ธฐ์๋ react 19.x.x ๋ฒ์ ์ ์ฌ์ฉํ์ง๋ง, ํ์ ๋ฒ์ ์์๋ useMemo๋ฅผ ์ฌ์ฉํ์ฌ ํฌ๋งคํฐ ์ธ์คํด์ค๋ฅผ ์ ์ฅํด์ผ ํฉ๋๋ค. ์ด๋ ๋ฌด๊ฑฐ์ด ํจ์์ด๊ธฐ ๋๋ฌธ์
๋๋ค.
</TabItem>
<TabItem label="next-intl" value="next-intl">
**๋ฒ์ญ (ํ์ ์ฌ์ฌ์ฉ; ์ํ๋ ๋๋ก next-intl ๋ฉ์์ง์ ๋ก๋ํ์ธ์)**
```json fileName="locales/en/about.json"
{
"counter": {
"label": "Counter",
"increment": "Increment"
}
}
```
```json fileName="locales/fr/about.json"
{
"counter": {
"label": "Compteur",
"increment": "Incrรฉmenter"
}
}
```
**ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ**
```tsx fileName="src/components/ClientComponentExample.tsx"
"use client";
import React, { useState } from "react";
import { useTranslations, useFormatter } from "next-intl";
const ClientComponentExample = () => {
// ์ค์ฒฉ๋ ๊ฐ์ฒด์ ์ง์ ๋ฒ์ ์ง์
const t = useTranslations("about.counter");
const format = useFormatter();
const [count, setCount] = useState(0);
return (
<div>
<p>{format.number(count)}</p>
<button
aria-label={t("label")}
onClick={() => setCount((count) => count + 1)}
>
{t("increment")}
</button>
</div>
);
};
```
> ํ์ด์ง ํด๋ผ์ด์ธํธ ๋ฉ์์ง์ "about" ๋ฉ์์ง๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ ์์ง ๋ง์ธ์
</TabItem>
<TabItem label="intlayer" value="intlayer">
**๋ด์ฉ**
```ts fileName="src/components/ClientComponentExample/index.content.ts"
import { t, type Dictionary } from "intlayer";
const counterContent = {
key: "counter",
content: {
label: t({ ko: "์นด์ดํฐ", en: "Counter", fr: "Compteur" }),
increment: t({ ko: "์ฆ๊ฐ", en: "Increment", fr: "Incrรฉmenter" }),
},
} satisfies Dictionary;
export default counterContent;
```
**ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ**
```tsx fileName="src/components/ClientComponentExample/index.tsx"
"use client";
import React, { useState } from "react";
import { useNumber, useIntlayer } from "next-intlayer";
const ClientComponentExample = () => {
const [count, setCount] = useState(0);
const { label, increment } = useIntlayer("counter"); // ๋ฌธ์์ด์ ๋ฐํํฉ๋๋ค
const { number } = useNumber();
return (
<div>
<p>{number(count)}</p>
<button aria-label={label} onClick={() => setCount((count) => count + 1)}>
{increment}
</button>
</div>
);
};
```
</TabItem>
</Tab>
#### ๋น๊ต
- **์ซ์ ํฌ๋งทํ
**
- **next-i18next**: `useNumber` ์์; `Intl.NumberFormat` (๋๋ i18next-icu) ์ฌ์ฉ.
- **next-intl**: `useFormatter().number(value)`.
- **Intlayer**: ๋ด์ฅ๋ `useNumber()` ์ฌ์ฉ.
- **ํค**
- ์ค์ฒฉ ๊ตฌ์กฐ ์ ์ง (`about.counter.label`) ๋ฐ ํ
๋ฒ์ ์ง์ (`useTranslation("about")` + `t("counter.label")` ๋๋ `useTranslations("about.counter")` + `t("label")`).
- **ํ์ผ ์์น**
- **next-i18next**๋ `public/locales/{lng}/{ns}.json`์ JSON์ ๊ธฐ๋.
- **next-intl**๋ ์ ์ฐํ๋ฉฐ, ์ค์ ์ ๋ฐ๋ผ ๋ฉ์์ง ๋ก๋ ๊ฐ๋ฅ.
- **Intlayer**๋ TS/JS ๋์
๋๋ฆฌ์ ์ฝํ
์ธ ๋ฅผ ์ ์ฅํ๊ณ ํค๋ก ํด์.
---
### ์๋ฒ ์ปดํฌ๋ํธ์์์ ์ฌ์ฉ
UI ์ปดํฌ๋ํธ์ ๊ฒฝ์ฐ๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ์ด ์ปดํฌ๋ํธ๋ ์๋ฒ ์ปดํฌ๋ํธ์ด๋ฉฐ, ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์ ์์์ผ๋ก ์ฝ์
๋ ์ ์์ด์ผ ํฉ๋๋ค. (ํ์ด์ง (์๋ฒ ์ปดํฌ๋ํธ) -> ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ -> ์๋ฒ ์ปดํฌ๋ํธ). ์ด ์ปดํฌ๋ํธ๊ฐ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์ ์์์ผ๋ก ์ฝ์
๋ ์ ์์ผ๋ฏ๋ก ๋น๋๊ธฐ(async)์ผ ์ ์์ต๋๋ค.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
```tsx fileName="src/pages/about.tsx"
import type { GetStaticProps } from "next";
import { useTranslation } from "next-i18next";
type ServerComponentProps = {
count: number;
};
const ServerComponent = ({ count }: ServerComponentProps) => {
const { t, i18n } = useTranslation("about");
const formatted = new Intl.NumberFormat(i18n.language).format(count);
return (
<div>
<p>{formatted}</p>
<button aria-label={t("counter.label")}>{t("counter.increment")}</button>
</div>
);
};
```
</TabItem>
<TabItem label="next-intl" value="next-intl">
```tsx fileName="src/components/ServerComponent.tsx"
type ServerComponentProps = {
count: number;
t: (key: string) => string;
formatter: Intl.NumberFormat;
};
const ServerComponent = ({ t, count, formatter }: ServerComponentProps) => {
const formatted = formatter.format(count);
return (
<div>
<p>{formatted}</p>
<button aria-label={t("label")}>{t("increment")}</button>
</div>
);
};
```
> ์๋ฒ ์ปดํฌ๋ํธ๋ ๋น๋๊ธฐ(async)์ผ ์ ์์ผ๋ฏ๋ก, ๋ฒ์ญ ํจ์์ ํฌ๋งทํฐ ํจ์๋ฅผ props๋ก ์ ๋ฌํด์ผ ํฉ๋๋ค.
>
> - `const t = await getTranslations("about.counter");`
> - `const formatter = await getFormatter().then((formatter) => formatter.number());`
</TabItem>
<TabItem label="intlayer" value="intlayer">
```tsx fileName="src/components/ServerComponent.tsx"
import { useIntlayer, useNumber } from "next-intlayer/server";
const ServerComponent = ({ count }: { count: number }) => {
const { label, increment } = useIntlayer("counter");
const { number } = useNumber();
return (
<div>
<p>{number(count)}</p>
<button aria-label={label}>{increment}</button>
</div>
);
};
```
</TabItem>
</Tab>
> Intlayer๋ `next-intlayer/server`๋ฅผ ํตํด **์๋ฒ ์์ ** ํ
์ ์ ๊ณตํฉ๋๋ค. ์๋์ ์ํด `useIntlayer`์ `useNumber`๋ ํด๋ผ์ด์ธํธ ํ
๊ณผ ์ ์ฌํ ํ
ํํ์ ๋ฌธ๋ฒ์ ์ฌ์ฉํ์ง๋ง, ๋ด๋ถ์ ์ผ๋ก๋ ์๋ฒ ์ปจํ
์คํธ(`IntlayerServerProvider`)์ ์์กดํฉ๋๋ค.
### ๋ฉํ๋ฐ์ดํฐ / ์ฌ์ดํธ๋งต / ๋ก๋ด
์ฝํ
์ธ ๋ฒ์ญ์ ํ๋ฅญํฉ๋๋ค. ํ์ง๋ง ์ฌ๋๋ค์ ๋ณดํต ๊ตญ์ ํ์ ์ฃผ์ ๋ชฉํ๊ฐ ์น์ฌ์ดํธ๋ฅผ ์ ์ธ๊ณ์ ๋ ์ ๋ณด์ด๊ฒ ํ๋ ๊ฒ์์ ์์ด๋ฒ๋ฆฝ๋๋ค. I18n์ ์น์ฌ์ดํธ ๊ฐ์์ฑ์ ํฅ์์ํค๋ ๋๋ผ์ด ์๋จ์
๋๋ค.
๋ค๊ตญ์ด SEO์ ๊ด๋ จ๋ ์ข์ ์ค์ฒ ๋ชฉ๋ก์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- `<head>` ํ๊ทธ์ hreflang ๋ฉํ ํ๊ทธ ์ค์
> ๊ฒ์ ์์ง์ด ํ์ด์ง์์ ์ด๋ค ์ธ์ด๊ฐ ์ฌ์ฉ ๊ฐ๋ฅํ์ง ์ดํดํ๋ ๋ฐ ๋์์ ์ค๋๋ค
- `http://www.w3.org/1999/xhtml` XML ์คํค๋ง๋ฅผ ์ฌ์ฉํ์ฌ sitemap.xml์ ๋ชจ๋ ํ์ด์ง ๋ฒ์ญ์ ๋์ดํ์ธ์.
>
- robots.txt์์ ์ ๋์ฌ๊ฐ ๋ถ์ ํ์ด์ง๋ฅผ ์ ์ธํ๋ ๊ฒ์ ์์ง ๋ง์ธ์ (์: `/dashboard`, `/fr/dashboard`, `/es/dashboard`)
>
- ๊ฐ์ฅ ํ์งํ๋ ํ์ด์ง๋ก ๋ฆฌ๋๋ ์
ํ๊ธฐ ์ํด ์ปค์คํ
Link ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ธ์ (์: ํ๋์ค์ด์ ๊ฒฝ์ฐ `<a href="/fr/about">A propos</a>`)
>
๊ฐ๋ฐ์๋ค์ ์ข
์ข
์ฌ๋ฌ ๋ก์ผ์ผ์ ๊ฑธ์ณ ํ์ด์ง๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ฐธ์กฐํ๋ ๊ฒ์ ์์ด๋ฒ๋ฆฝ๋๋ค.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
```ts fileName="i18n.config.ts"
export const locales = ["en", "fr"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "en";
export function localizedPath(locale: string, path: string) {
return locale === defaultLocale ? path : "/" + locale + path;
}
const ORIGIN = "https://example.com";
export function abs(locale: string, path: string) {
return ORIGIN + localizedPath(locale, path);
}
```
```tsx fileName="src/app/[locale]/about/layout.tsx"
import type { Metadata } from "next";
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = params;
// ์ฌ๋ฐ๋ฅธ JSON ํ์ผ์ ๋์ ์ผ๋ก ๊ฐ์ ธ์ต๋๋ค
const messages = (
await import("@/../public/locales/" + locale + "/about.json")
).default;
const languages = Object.fromEntries(
locales.map((locale) => [locale, localizedPath(locale, "/about")])
);
return {
title: messages.title,
description: messages.description,
alternates: {
canonical: localizedPath(locale, "/about"),
languages: { ...languages, "x-default": "/about" },
},
};
}
export default async function AboutPage() {
return <h1>์๊ฐ</h1>; // ํ์ด์ง ์ ๋ชฉ์ ํ๊ตญ์ด๋ก ๋ฒ์ญ
}
```
```ts fileName="src/app/sitemap.ts"
import type { MetadataRoute } from "next";
import { locales, defaultLocale, abs } from "@/i18n.config";
export default function sitemap(): MetadataRoute.Sitemap {
const languages = Object.fromEntries(
locales.map((locale) => [locale, abs(locale, "/about")])
);
return [
{
url: abs(defaultLocale, "/about"),
lastModified: new Date(), // ๋ง์ง๋ง ์์ ๋ ์ง
changeFrequency: "monthly", // ๋ณ๊ฒฝ ๋น๋: ๋งค์
priority: 0.7, // ์ฐ์ ์์
alternates: { languages }, // ๋์ฒด ์ธ์ด ๊ฒฝ๋ก
},
];
}
```
```ts fileName="src/app/robots.ts"
import type { MetadataRoute } from "next";
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
const ORIGIN = "https://example.com";
const expandAllLocales = (path: string) => [
localizedPath(defaultLocale, path),
...locales
.filter((locale) => locale !== defaultLocale)
.map((locale) => localizedPath(locale, path)),
];
export default function robots(): MetadataRoute.Robots {
const disallow = [
...expandAllLocales("/dashboard"),
...expandAllLocales("/admin"),
];
return {
rules: { userAgent: "*", allow: ["/"], disallow },
host: ORIGIN,
sitemap: ORIGIN + "/sitemap.xml",
};
}
```
</TabItem>
<TabItem label="next-intl" value="next-intl">
```tsx fileName="src/app/[locale]/about/layout.tsx"
import type { Metadata } from "next";
import { locales, defaultLocale } from "@/i18n";
import { getTranslations } from "next-intl/server";
function localizedPath(locale: string, path: string) {
return locale === defaultLocale ? path : "/" + locale + path;
}
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = params;
const t = await getTranslations({ locale, namespace: "about" });
const url = "/about";
const languages = Object.fromEntries(
locales.map((locale) => [locale, localizedPath(locale, url)])
);
return {
title: t("title"),
description: t("description"),
alternates: {
canonical: localizedPath(locale, url),
languages: { ...languages, "x-default": url },
},
};
}
// ... ํ์ด์ง ์ฝ๋์ ๋๋จธ์ง ๋ถ๋ถ
```
```tsx fileName="src/app/sitemap.ts"
import type { MetadataRoute } from "next";
import { locales, defaultLocale } from "@/i18n";
const origin = "https://example.com";
const formatterLocalizedPath = (locale: string, path: string) =>
locale === defaultLocale ? origin + path : origin + "/" + locale + path;
export default function sitemap(): MetadataRoute.Sitemap {
const aboutLanguages = Object.fromEntries(
locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
);
return [
{
url: formatterLocalizedPath(defaultLocale, "/about"),
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
alternates: { languages: aboutLanguages },
},
];
}
```
```tsx fileName="src/app/robots.ts"
import type { MetadataRoute } from "next";
import { locales, defaultLocale } from "@/i18n";
const origin = "https://example.com";
const withAllLocales = (path: string) => [
path,
...locales
.filter((locale) => locale !== defaultLocale)
.map((locale) => "/" + locale + path),
];
export default function robots(): MetadataRoute.Robots {
const disallow = [
...withAllLocales("/dashboard"),
...withAllLocales("/admin"),
];
return {
rules: { userAgent: "*", allow: ["/"], disallow },
host: origin,
sitemap: origin + "/sitemap.xml",
};
}
```
</TabItem>
<TabItem label="intlayer" value="intlayer">
```typescript fileName="src/app/[locale]/about/layout.tsx"
import { getIntlayer, getMultilingualUrls } from "intlayer";
import type { Metadata } from "next";
import type { LocalPromiseParams } from "next-intlayer";
export const generateMetadata = async ({
params,
}: LocalPromiseParams): Promise<Metadata> => {
const { locale } = await params;
const metadata = getIntlayer("page-metadata", locale);
const multilingualUrls = getMultilingualUrls("/about");
return {
...metadata,
alternates: {
canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
languages: { ...multilingualUrls, "x-default": "/about" },
},
};
};
// ... ํ์ด์ง ๋๋จธ์ง ์ฝ๋
```
```tsx fileName="src/app/sitemap.ts"
import { getMultilingualUrls } from "intlayer";
import type { MetadataRoute } from "next";
const sitemap = (): MetadataRoute.Sitemap => [
{
url: "https://example.com/about",
alternates: {
languages: { ...getMultilingualUrls("https://example.com/about") },
},
},
];
```
```tsx fileName="src/app/robots.ts"
import { getMultilingualUrls } from "intlayer";
import type { MetadataRoute } from "next";
const getAllMultilingualUrls = (urls: string[]) =>
urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
const robots = (): MetadataRoute.Robots => ({
rules: {
userAgent: "*",
allow: ["/"],
disallow: getAllMultilingualUrls(["/dashboard"]),
},
host: "https://example.com",
sitemap: "https://example.com/sitemap.xml",
});
export default robots;
```
</TabItem>
</Tab>
> Intlayer๋ ์ฌ์ดํธ๋งต์ ์ํด ๋ค๊ตญ์ด URL์ ์์ฑํ๋ `getMultilingualUrls` ํจ์๋ฅผ ์ ๊ณตํฉ๋๋ค.
---
---
## ๊ทธ๋ฆฌ๊ณ ์น์๋โฆ
๊ฐ๋จํ์ง ์์ต๋๋ค. ๊ฐ ์ต์
๋ง๋ค ์ฅ๋จ์ ์ด ์์ต๋๋ค. ์ ๊ฐ ๋ณด๋ ๊ด์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
<Columns>
<Column>
**next-intl**
- ๊ฐ์ฅ ๊ฐ๋จํ๊ณ ๊ฐ๋ฒผ์ฐ๋ฉฐ, ๊ฐ์๋๋ ๊ฒฐ์ ์ด ์ ์ต๋๋ค. **์ต์ํ์** ์๋ฃจ์
์ ์ํ๊ณ ์ค์ ์ง์ค์ ์นดํ๋ก๊ทธ์ ์ต์ํ๋ฉฐ ์ฑ์ด **์๊ท๋ชจ์์ ์ค๊ฐ ๊ท๋ชจ**์ธ ๊ฒฝ์ฐ ์ ํฉํฉ๋๋ค.
</Column>
<Column>
**next-i18next**
- ์ฑ์ํ๊ณ ๊ธฐ๋ฅ์ด ํ๋ถํ๋ฉฐ ์ปค๋ฎค๋ํฐ ํ๋ฌ๊ทธ์ธ์ด ๋ง์ง๋ง ์ค์ ๋น์ฉ์ด ๋ ๋์ต๋๋ค. **i18next์ ํ๋ฌ๊ทธ์ธ ์ํ๊ณ**(์: ํ๋ฌ๊ทธ์ธ์ ํตํ ๊ณ ๊ธ ICU ๊ท์น)๊ฐ ํ์ํ๊ณ ํ์ด ์ด๋ฏธ i18next๋ฅผ ์๊ณ ์์ผ๋ฉฐ ์ ์ฐ์ฑ์ ์ํด **๋ ๋ง์ ์ค์ **์ ๊ฐ์ํ ์ ์๋ค๋ฉด ์ ํฉํฉ๋๋ค.
</Column>
<Column>
**Intlayer**
- ๋ชจ๋์ ์ฝํ
์ธ , ํ์
์์ ์ฑ, ๋๊ตฌ ์ง์, ๊ทธ๋ฆฌ๊ณ ๋ณด์ผ๋ฌํ๋ ์ดํธ๊ฐ ์ ์ ํ๋์ ์ธ Next.js๋ฅผ ์ํด ์ค๊ณ๋์์ต๋๋ค. ํนํ **Next.js App Router**, ๋์์ธ ์์คํ
, ๊ทธ๋ฆฌ๊ณ **๋๊ท๋ชจ ๋ชจ๋์ ์ฝ๋๋ฒ ์ด์ค**์ ๋ํด **์ปดํฌ๋ํธ ๋ฒ์ ์ฝํ
์ธ **, **์๊ฒฉํ TypeScript**, **๋น๋ ํ์ ๋ณด์ฅ**, **ํธ๋ฆฌ ์์ดํน**, ๊ทธ๋ฆฌ๊ณ **๊ธฐ๋ณธ ์ ๊ณต** ๋ผ์ฐํ
/SEO/์๋ํฐ ๋๊ตฌ๋ฅผ ์ค์ํ๊ฒ ์๊ฐํ๋ค๋ฉด ์ ํฉํฉ๋๋ค.
</Column>
</Columns>
์ต์ํ์ ์ค์ ์ ์ ํธํ๊ณ ์ผ๋ถ ์๋ ์ฐ๊ฒฐ์ ๊ฐ์ํ ์ ์๋ค๋ฉด next-intl์ด ์ข์ ์ ํ์
๋๋ค. ๋ชจ๋ ๊ธฐ๋ฅ์ด ํ์ํ๊ณ ๋ณต์ก์ฑ์ ๊ฐ์ํ ์ ์๋ค๋ฉด next-i18next๊ฐ ์ ํฉํฉ๋๋ค. ํ์ง๋ง ํ๋์ ์ด๊ณ ํ์ฅ ๊ฐ๋ฅํ๋ฉฐ ๋ชจ๋์ ์๋ฃจ์
๊ณผ ๋ด์ฅ ๋๊ตฌ๋ฅผ ์ํ๋ค๋ฉด Intlayer๊ฐ ๋ฐ๋ก ๊ทธ ์๊ตฌ๋ฅผ ์ถฉ์กฑ์ํค๊ณ ์ ํฉ๋๋ค.
> **๊ธฐ์
ํ์ ์ํ ๋์**: **Crowdin**, **Phrase**์ ๊ฐ์ ๊ฒ์ฆ๋ ํ์งํ ํ๋ซํผ์ด๋ ๊ธฐํ ์ ๋ฌธ ๋ฒ์ญ ๊ด๋ฆฌ ์์คํ
๊ณผ ์๋ฒฝํ๊ฒ ์๋ํ๋ ๊ฒ์ฆ๋ ์๋ฃจ์
์ด ํ์ํ๋ค๋ฉด, ์ฑ์ํ ์ํ๊ณ์ ๊ฒ์ฆ๋ ํตํฉ ๊ธฐ๋ฅ์ ๊ฐ์ถ **next-intl** ๋๋ **next-i18next**๋ฅผ ๊ณ ๋ คํด ๋ณด์ธ์.
> **ํฅํ ๋ก๋๋งต**: Intlayer๋ ๋ํ **i18next** ๋ฐ **next-intl** ์๋ฃจ์
์์์ ์๋ํ๋ ํ๋ฌ๊ทธ์ธ ๊ฐ๋ฐ์ ๊ณํํ๊ณ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ์๋ํ, ๊ตฌ๋ฌธ, ์ฝํ
์ธ ๊ด๋ฆฌ ์ธก๋ฉด์์ Intlayer์ ์ฅ์ ์ ์ ๊ณตํ๋ฉด์๋, ์ ํ๋ฆฌ์ผ์ด์
์ฝ๋์์ ์ด๋ฌํ ๊ฒ์ฆ๋ ์๋ฃจ์
๋ค์ด ์ ๊ณตํ๋ ๋ณด์์ฑ๊ณผ ์์ ์ฑ์ ์ ์งํ ์ ์์ต๋๋ค.
## GitHub STARs
GitHub ์คํ๋ ํ๋ก์ ํธ์ ์ธ๊ธฐ, ์ปค๋ฎค๋ํฐ ์ ๋ขฐ๋, ๊ทธ๋ฆฌ๊ณ ์ฅ๊ธฐ์ ์ธ ๊ด๋ จ์ฑ์ ๊ฐ๋ ฅํ๊ฒ ๋ํ๋ด๋ ์งํ์
๋๋ค. ๊ธฐ์ ์ ํ์ง์ ์ง์ ์ ์ธ ์ฒ๋๋ ์๋์ง๋ง, ์ผ๋ง๋ ๋ง์ ๊ฐ๋ฐ์๊ฐ ํด๋น ํ๋ก์ ํธ๋ฅผ ์ ์ฉํ๋ค๊ณ ์๊ฐํ๊ณ , ์งํ ์ํฉ์ ํ๋ก์ฐํ๋ฉฐ, ์ฑํํ ๊ฐ๋ฅ์ฑ์ด ์๋์ง๋ฅผ ๋ฐ์ํฉ๋๋ค. ํ๋ก์ ํธ์ ๊ฐ์น๋ฅผ ํ๊ฐํ ๋, ์คํ๋ ๋์๋ค ๊ฐ์ ๊ด์ฌ๋๋ฅผ ๋น๊ตํ๊ณ ์ํ๊ณ ์ฑ์ฅ์ ๋ํ ํต์ฐฐ์ ์ ๊ณตํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
[](https://www.star-history.com/#i18next/next-i18next&amannn/next-intl&aymericzip/intlayer)
---
## ๊ฒฐ๋ก
์ธ ๊ฐ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ชจ๋ ํต์ฌ ๋ก์ปฌ๋ผ์ด์ ์ด์
์์๋ ์ฑ๊ณต์ ์
๋๋ค. ์ฐจ์ด์ ์ **ํ๋์ ์ธ Next.js์์ ๊ฒฌ๊ณ ํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ์ค์ ์ ๋ฌ์ฑํ๊ธฐ ์ํด ์ผ๋ง๋ ๋ง์ ์์
์ ํด์ผ ํ๋๊ฐ**์ ์์ต๋๋ค:
- **Intlayer**๋ฅผ ์ฌ์ฉํ๋ฉด, **๋ชจ๋ํ๋ ์ฝํ
์ธ **, **์๊ฒฉํ TS(ํ์
์คํฌ๋ฆฝํธ)**, **๋น๋ ํ์ ์์ ์ฑ**, **ํธ๋ฆฌ ์์ดํน๋ ๋ฒ๋ค**, ๊ทธ๋ฆฌ๊ณ **์ผ๋ฅ App Router + SEO ๋๊ตฌ**๊ฐ **๊ธฐ๋ณธ๊ฐ**์ผ๋ก ์ ๊ณต๋๋ฉฐ, ๋ฒ๊ฑฐ๋ก์ด ์์
์ด ์๋๋๋ค.
- ๋ค๊ตญ์ด, ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์ฑ์์ **์ ์ง๋ณด์์ฑ๊ณผ ์๋**๋ฅผ ์ค์์ํ๋ ํ์ด๋ผ๋ฉด, Intlayer๊ฐ ์ค๋๋ ๊ฐ์ฅ **์๋ฒฝํ** ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค.
์์ธํ ๋ด์ฉ์ ['Why Intlayer?' ๋ฌธ์](https://intlayer.org/doc/why)๋ฅผ ์ฐธ์กฐํ์ธ์.