๐ Next.js ์ฌํ 13์ฅ: Advanced I18n โ ์ ์ธ๊ณ ์ ์ ๋ฅผ ์ฌ๋ก์ก๋ ๋ก์ปฌ๋ผ์ด์ ์ด์ ์ ๋ต
๐ ๊ฐ์
๊ณ ๊ธ ๊ตญ์ ํ ์ ๋ต โ ๋์ locale, ์๋ฒ/ํด๋ผ์ด์ธํธ i18n, RTL ์ง์๊น์ง ๋ค๋ฃน๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐บ๏ธ App Router์ I18n ๊ธฐ๋ณธ ์ฒ ํ ๐ข
- ๐๏ธ ๋์ ์ฌ์ (Dict) ๋ก๋ ์์คํ ์ค๊ณ ๐ก
- ๐ก๏ธ ํ์ ์์ ์ฑ ํ๋ณดํ๊ธฐ (i18next vs Next native) ๐ก
- ๐ ๋ฏธ๋ค์จ์ด์ ์ธ์ด ๊ฐ์ง(Detection) ์ต์ ํ ๐ด
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 18๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 8๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์(๋์์ด๋): "์์ ๋, ์ฐ๋ฆฌ ์ปค๋ฎค๋ํฐ์ ์ผ๋ณธ์ด๋ ๋ฏธ๊ตญ ์ ์ ๋ค์ด ์์ฒญ ์ ์ ๋๊ณ ์์ด์! ์์ด๋ ์ผ๋ณธ์ด ๋ฒ์ ๋ ๋ง๋ค์ด์ฃผ์ธ์."
- ์์(PM): "์ข์์! ์์ฒ ๋, ์ง๊ธ ๋ฐ๋ก ๋ค๊ตญ์ด ๊ธฐ๋ฅ ์ถ๊ฐํด์ค ์ ์์ฃ ?"
- ์์ฒ (์ฃผ๋์ด): "๋ค!
localStorage์language์ ์ฅํด์ ๋ฐ๊พธ๋ฉด ๋๊ฒ ์ฃ ?" - ์ํธ(๋ฆฌ๋): "์์ฒ ๋, ์ ๋ผ์! ๊ทธ๋ฌ๋ฉด SEO๊ฐ ํผ์๋ผ์. ๊ตฌ๊ธ ๊ฒ์ ๋ก๋ด์ ํ๊ตญ์ด ๋ฌธ์๋ฐ์ ๋ชป ๋ณด๊ฒ ๋ ๊ฑฐ์์. App Router์ ๊ฒฝ๋ก ๊ธฐ๋ฐ I18n์ผ๋ก ๊ฐ์ผ ํฉ๋๋ค."
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
/ko,/en์ฒ๋ผ URL ๊ฒฝ๋ก๋ฅผ ํตํด ๊ฒ์ ์์ง ์ต์ ํ(SEO)๊ฐ ๋ณด์ฅ๋๋ ๋ค๊ตญ์ด๋ฅผ ๊ตฌํํ ์ ์๋ค - ์๋ฒ ์ปดํฌ๋ํธ์์ ํ์ํ ์ฌ์ ์ ๋์ ์ผ๋ก ๋ก๋ํ์ฌ ์ด๊ธฐ ์ ์ก๋์ ์ต์ํํ ์ ์๋ค
- ๋ฏธ๋ค์จ์ด๋ฅผ ์ด์ฉํด ์ ์ ์ ๋ธ๋ผ์ฐ์ ์ค์ ์ ๋ง๋ ์ธ์ด๋ก ์๋ ์ฐ๊ฒฐ(Redirect) ์๋น์ค๋ฅผ ๊ตฌ์ถํ ์ ์๋ค
๐ค ์ ์์์ผ ํ๋๊ฐ
๋ค๊ตญ์ด๋ ๋จ์ํ ๊ธ์๋ฅผ ๋ฐ๊พธ๋ ๊ฒ ์๋์ผ. ๋น์ฆ๋์ค์ ํ์ฅ์ฑ ๊ทธ ์์ฒด์ง.
Next.js App Router์์๋ ๋ค๊ตญ์ด ์ฒ๋ฆฌ๊ฐ ๋ผ์ฐํ
์์คํ
์ ํต์ฌ์ผ๋ก ๋ค์ด์์ด.
ํต์ฌ ํฌ์ธํธ:
- SEO: ๊ฐ ์ธ์ด๋ณ๋ก ๊ณ ์ ํ URL์ด ์์ด์ผ ๊ฒ์ ์์ง์ด ๋ชจ๋ ๋ฒ์ ์ ํ์ด์ง๋ฅผ ์ธ๋ฑ์ฑํ ์ ์์ด.
- Server-side First: ํด๋ผ์ด์ธํธ๊ฐ ๋ฒ์ญ ํ์ผ์ ๋ฐ๊ธฐ ์ ์, ์๋ฒ์์ ์ด๋ฏธ ๋ฒ์ญ๋ HTML์ ๋ด๋ ค์ค์ผ ํด.
- PPR & Layouts: ๋ ์ด์์ ๊ตฌ์กฐ๋ฅผ ์ ์งํ๋ฉด์ ์ธ์ด๋ง ๊ฐ์ ๋ผ์ฐ๋ ์ฐ์ํ ์ค๊ณ๊ฐ ํ์ํด.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ ๋์๊ด ์๋ด์ฑ ์๋ก ์ค๋ช ํ๋ค๋ฉด?
๋์๊ด ์ ๊ตฌ์ ์๋ด์ฑ ์๊ฐ ์์ฌ์์ด.
- Client-side I18n: ์ฑ ์๋ ๋ชจ๋ ํ๊ตญ์ด์ธ๋ฐ, ์์์ธ์ด ์ค๋ฉด ๋ง์ ์ ๋ถ๋ ค์ ๋์์์ ๊ธ์๋ฅผ ์์ด๋ก ๋ฐ๊พธ๋ ๊ฑฐ์ผ. ๋ง์ ์ฌ๊ฐ ์์ผ๋ฉด(JS ๋ก๋ ์ ) ๋ฌด์กฐ๊ฑด ํ๊ตญ์ด๋ง ๋ณด์ฌ.
- App Router I18n: ์ ๊ตฌ์ 'ํ๊ตญ์ด ์ ๊ตฌ', '์์ด ์ ๊ตฌ'๊ฐ ๋ฐ๋ก ์๋ ๊ฑฐ์ผ. ์์ด ์ ๊ตฌ๋ก ๋ค์ด๊ฐ๋ฉด ์์ ์ฒ์๋ถํฐ ๋๊น์ง ์์ด๋ก ๋ ์ฑ ์๋ค๋ง ๋น์น๋์ด ์์ง. ์๋์ด ๋ง์ ์ ๋ถ๋ฆด ํ์๊ฐ ์์ด.
๐บ๏ธ App Router์ I18n ๊ธฐ๋ณธ ์ฒ ํ ๐ข
Next.js๋ ๋ด์ฅ๋ I18n ์์คํ ์ ๊ฐ์ ํ์ง ์์. ๋์ ํด๋ ๊ตฌ์กฐ๋ฅผ ํตํด ์ ์ฐํ๊ฒ ๊ตฌํํ๋ผ๊ณ ๊ถ์ฅํ์ง.
๊ตฌ์กฐ ์์:
app/
[lang]/
layout.tsx
page.tsx
about/page.tsx
์ด๋ ๊ฒ ํ๋ฉด ์ ์ ๊ฐ /en/about์ผ๋ก ๋ค์ด์์ ๋ lang ํ๋ผ๋ฏธํฐ๊ฐ "en"์ผ๋ก ์ ๋ฌ๋ผ.
๐๏ธ ๋์ ์ฌ์ (Dict) ๋ก๋ ์์คํ ์ค๊ณ ๐ก
๋ชจ๋ ์ค๋ฌด์ฉ ๊ฐ์ด๋๋ ์๋ฒ ์ฌ์ด๋ ๋ก๋๋ฅผ ์งํฅํด์ผ ํด.
// lib/get-dictionary.ts
import 'server-only'
// โ
1. ๊ฐ ์ธ์ด๋ณ ์ฌ์ ํ์ผ์ ์ ์ํด (์ค์ ๋ก๋ public/locales ๋ฐ์ ๋๋ ๊ฒ ์ข์)
const dictionaries = {
en: () => import('@/dictionaries/en.json').then((module) => module.default),
ko: () => import('@/dictionaries/ko.json').then((module) => module.default),
}
export const getDictionary = async (lang: 'en' | 'ko') =>
dictionaries[lang]?.() ?? dictionaries.en() // ํด๋น ์ธ์ด ์์ผ๋ฉด ์์ด๊ฐ ๊ธฐ๋ณธ์๋ฒ ์ปดํฌ๋ํธ์์ ์ฌ์ฉํ๊ธฐ
// app/[lang]/page.tsx
import { getDictionary } from '@/lib/get-dictionary'
export default async function Page({ params }: { params: { lang: 'en' | 'ko' } }) {
const { lang } = await params
const dict = await getDictionary(lang) // โ
์๋ฒ์์ ํ์ํ ์ธ์ด๋ง ์ ๊ฐ์ ธ์ด
return (
<main>
<h1>{dict.home.title}</h1>
<p>{dict.home.description}</p>
</main>
)
}๐ ๋ฏธ๋ค์จ์ด์ ์ธ์ด ๊ฐ์ง(Detection) ์ต์ ํ ๐ด
์ ์ ๊ฐ ๊ทธ๋ฅ youngsu.com์ผ๋ก ๋ค์ด์์ ๋, ๋ธ๋ผ์ฐ์ ์ธ์ด ์ค์ ์ ์ฝ์ด์ /ko๋ /en์ผ๋ก ๋ณด๋ด์ค์ผ ํด.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
const locales = ['ko', 'en']
const defaultLocale = 'ko'
function getLocale(request: NextRequest) {
const headers = { 'accept-language': request.headers.get('accept-language') ?? '' }
const languages = new Negotiator({ headers }).languages()
return match(languages, locales, defaultLocale)
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// โ
์ด๋ฏธ ์ธ์ด ๊ฒฝ๋ก๊ฐ ๋ถ์ด์๋์ง ํ์ธ
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) return
// โ
์ธ์ด๊ฐ ์๋ค๋ฉด ๊ฐ์งํด์ ๋ฆฌ๋ค์ด๋ ํธ
const locale = getLocale(request)
request.nextUrl.pathname = `/${locale}${pathname}`
return NextResponse.redirect(request.nextUrl)
}
export const config = {
matcher: [
// โ ๏ธ ์ ์ ์์ฐ(images, favicon ๋ฑ)์ ์ ์ธํ๊ณ ๋ผ์ฐํธ๋ง ๊ฐ์ง!
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ I18n ์ค๊ณ ํต์ฌ ์์น
| ํญ๋ชฉ | ์ ๋ต | ์ด์ |
|---|---|---|
| URL ๊ตฌ์กฐ | /lang/path (๋์ ์ธ๊ทธ๋จผํธ) | ์ธ์ด๋ณ ๊ณ ์ URL ํ๋ณด (SEO) |
| ๋ฐ์ดํฐ ๋ก๋ | import() (๋์ ์ํฌํธ) | ํ์ ์๋ ์ธ์ด ํฉ์ ์๋ฒ ๋ฉ๋ชจ๋ฆฌ์ ์ฌ๋ฆฌ์ง ์์ |
| ์ธ์ด ๊ฐ์ง | middleware.ts | ์ ์ ๊ฒฝํ(UX) ์ต์ ํ ๋ฐ ์๋ ์ ํ |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| ์ํฉ | โ ๋์ ์ | โ ์ข์ ์ |
|---|---|---|
| ์ธ์ด ์ ์ฅ | localStorage๋ง ์ฌ์ฉ | URL ๊ฒฝ๋ก([lang])๋ฅผ ์ง์ค์ ๊ทผ์์ผ๋ก ์ฌ์ฉ |
| ๋ฒ์ญ ๋ก๋ | ํด๋ผ์ด์ธํธ์์ ๊ฑฐ๋ํ JSON ํต์งธ๋ก fetch | ์๋ฒ ์ปดํฌ๋ํธ์์ ํ์ํ ๋งํผ๋ง ์ฌ์ ๋ก๋ |
| SEO | ?lang=en ์ฟผ๋ฆฌ ์คํธ๋ง ์ฌ์ฉ | /en ์๋ธ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ ์ฌ์ฉ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. App Router i18n์์ /ko, /en ๊ฐ์ ๊ฒฝ๋ก ๊ธฐ๋ฐ locale์ ์ฐ๋ ๊ฐ์ฅ ํฐ ์ด์ ๋ ๋ฌด์์ธ๊ฐ?
โ ์ ๋ต: ์ธ์ด๋ณ ํ์ด์ง๊ฐ ๊ณ ์ URL์ ๊ฐ์ ธ SEO, ๊ณต์ , ์บ์ฑ, ๋ผ์ฐํ ํ๋จ์ด ๋ช ํํด์ง๊ธฐ ๋๋ฌธ์ด๋ค.
๐ก ์์ธ ํด์ค: localStorage์ ์ธ์ด๋ง ์ ์ฅํ๋ฉด ๊ฒ์ ์์ง๊ณผ ๊ณต์ ๋งํฌ๋ ์ด๋ค ์ธ์ด ๋ฌธ์์ธ์ง ์๊ธฐ ์ด๋ ต๋ค. URL์ locale์ด ์์ผ๋ฉด ์๋ฒ ์ปดํฌ๋ํธ๊ฐ ์ฒ์๋ถํฐ ์ฌ๋ฐ๋ฅธ ์ฌ์ ์ ๊ณ ๋ฅด๊ณ , hreflang ๊ฐ์ ๋ฉํ๋ฐ์ดํฐ๋ ๋ช ํํ๊ฒ ๋ง๋ค ์ ์๋ค.
Q2. ๋ฒ์ญ ์ฌ์ ์ server-only๋ก ๊ด๋ฆฌํ๋ ์ด์ ๋ ๋ฌด์์ธ๊ฐ?
โ ์ ๋ต: ํฐ JSON ์ฌ์ ๊ณผ ์๋ฒ ์ ์ฉ ๋ก์ง์ด ํด๋ผ์ด์ธํธ ๋ฒ๋ค์ ์์ด๋ ์ค์๋ฅผ ๋ง๊ธฐ ์ํด์๋ค.
๐ก ์์ธ ํด์ค: ์๋ฒ ์ปดํฌ๋ํธ์์ ํ์ํ ๋ฒ์ญ ๊ฒฐ๊ณผ๋ง HTML๋ก ๋ด๋ ค๋ณด๋ด๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ ๋ชจ๋ ์ธ์ด ์ฌ์ ์ ๋ฐ์ ํ์๊ฐ ์๋ค. server-only๋ ์ค์๋ก ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ importํ ๋ ๋น๋ ๋จ๊ณ์์ ๋ง์ ์ฃผ๋ ์ธํ๋ฆฌ ์ญํ ์ ํ๋ค.
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์: ์์์ด "ํ๊ตญ์ด๋ ๋ ์ง๊ฐ 2026๋ 4์ 30์ผ, ์์ด๋ Apr 30, 2026์ฒ๋ผ ๋ณด์ฌ์ผ ํ๋ค"๊ณ ์์ฒญํ๋ค. ๋จ์ ๋ฒ์ญ ํค๋ง์ผ๋ก ์ถฉ๋ถํ ๊น?
โ ์ ๋ต: ๋ถ์กฑํ๋ค. locale๋ณ Intl ํฌ๋งท, ํ์์กด, ์ซ์/ํตํ ํ๊ธฐ๊น์ง ๋ก์ปฌ๋ผ์ด์ ์ด์ ๊ท์น์ผ๋ก ๋ถ๋ฆฌํด์ผ ํ๋ค.
๐ก ์์ธ ํด์ค: i18n์ ํ ์คํธ ์นํ์ด๊ณ , localization์ ์ฌ์ฉ์๊ฐ ์๊ธฐ ๋ฌธํ๊ถ์ ๊ท์น์ผ๋ก ์ดํดํ๊ฒ ๋ง๋๋ ์ผ์ด๋ค. ๋ ์ง, ํตํ, ๋ณต์ํ, ์ ๋ ฌ, RTL ์ฌ๋ถ๋ ๋ฌธ์์ด ์ฌ์ ๋ง์ผ๋ก ํด๊ฒฐ๋์ง ์๋๋ค. URL locale์ ๊ธฐ์ค์ผ๋ก ํฌ๋งทํฐ์ ๋ฉํ๋ฐ์ดํฐ๊น์ง ํจ๊ป ์ ํํด์ผ ํ๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ๋ค๊ตญ์ด๋ฅผ "๋ฌธ์์ด ํ์ผ์ ์ฌ๋ฌ ๊ฐ ๋๋ ์ผ"๋ก๋ง ์๊ฐํ๋ ๊ฒ ๋ถ๋๋ฌ์ ๋ค. URL, ์๋ฒ ์ฌ์ ๋ก๋ฉ, SEO, ๋ ์ง์ ํตํ ํฌ๋งท๊น์ง ์ด์ด์ ธ์ผ ์ฌ์ฉ์๋ ์ง์ง ์๊ธฐ ์ธ์ด์ ์๋น์ค๋ผ๊ณ ๋๋๋ค.
๐ก "๊ธ์๋ฅผ ๋ฐ๊พธ๋ ๊ฑด ๋ฒ์ญ์ด๊ณ , ์ฌ์ฉ์์ ๋งฅ๋ฝ์ ๋ง์ถ๋ ๊ฑด ๋ก์ปฌ๋ผ์ด์ ์ด์ ์ด๋ค."
๋ค์ i18n ์์ ์์๋ ๋จผ์ locale์ด ๋ผ์ฐํ ์ ์ผ๋ถ์ธ์ง ํ์ธํ๊ฒ ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฒ์ญ ํค๋ฅผ ์ถ๊ฐํ๋ PR์์๋ ๋ฉํ๋ฐ์ดํฐ, ๋ ์ง/์ซ์ ํฌ๋งท, ๋๋ฝ ํค fallback์ด ํจ๊ป ๊ฒ์ฆ๋๋์ง ์ฒดํฌํ๊ฒ ๋ค.