๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Front
NextJs
14-Internationalization

Internationalization(๊ตญ์ œํ™”)

Next.js๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ ์–ธ์–ด๋ฅผ ์ง€์›ํ•˜๋„๋ก ์ฝ˜ํ…์ธ  ๋ผ์šฐํŒ… ๋ฐ ๋ Œ๋”๋ง์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์ดํŠธ๋ฅผ ๋‹ค์–‘ํ•œ ๋กœ์บ˜์— ๋งž๊ฒŒ ์กฐ์ •ํ•˜๋ ค๋ฉด ๋ฒˆ์—ญ๋œ ์ฝ˜ํ…์ธ (๋กœ์ปฌ๋ผ์ด์ œ์ด์…˜)์™€ ๊ตญ์ œํ™”๋œ ๊ฒฝ๋กœ๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.


1. Terminology(์šฉ์–ด)

Locale(ํ˜„์ง€์˜): ์–ธ์–ด ๋ฐ ์„œ์‹ ๊ธฐ๋ณธ ์„ค์ • ์ง‘ํ•ฉ์— ๋Œ€ํ•œ ์‹๋ณ„์ž์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํ˜ธํ•˜๋Š” ์–ธ์–ด์™€ ํ•ด๋‹น ์ง€์—ญ์˜ ์–ธ์–ด๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

  • en-US: ๋ฏธ๊ตญ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์˜์–ด
  • nl-NL: ๋„ค๋œ๋ž€๋“œ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋„ค๋œ๋ž€๋“œ์–ด
  • nl: ๋„ค๋œ๋ž€๋“œ์–ด, ํŠน์ • ์ง€์—ญ ์—†์Œ

2. Routing Overview

๋ธŒ๋ผ์šฐ์ €์—์„œ ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด ๊ธฐ๋ณธ ์„ค์ •์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉํ•  Locale์„ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์–ธ์–ด๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๋“ค์–ด์˜ค๋Š” Accept-Language ํ—ค๋”๊ฐ€ ์ˆ˜์ •๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ๋‹ค์Œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ˆ˜์‹  ์š”์ฒญ์„ ์‚ดํŽด๋ณด๊ณ  ํ—ค๋”, ์ง€์›ํ•˜๋ ค๋Š” Locale ๋ฐ ๊ธฐ๋ณธ Locale์— ๋”ฐ๋ผ ์„ ํƒํ•  Locale์„ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

middleware.js
1
import { match } from '@formatjs/intl-localematcher'
2
import Negotiator from 'negotiator'
3
4
let headers = { 'accept-language': 'en-US,en;q=0.5' }
5
let languages = new Negotiator({ headers }).languages()
6
let locales = ['en-US', 'nl-NL', 'nl']
7
let defaultLocale = 'en-US'
8
9
match(languages, locales, defaultLocale) // -> 'en-US'

๋ผ์šฐํŒ…์€ ํ•˜์œ„ ๊ฒฝ๋กœ(/fr/products) ๋˜๋Š” ๋„๋ฉ”์ธ(my-site.fr/products)์„ ํ†ตํ•ด ๊ตญ์ œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ •๋ณด๋ฅผ ํ†ตํ•ด ์ด์ œ ๋ฏธ๋“ค์›จ์–ด ๋‚ด๋ถ€์˜ Locale์— ๋”ฐ๋ผ ์‚ฌ์šฉ์ž๋ฅผ ๋ฆฌ๋””๋ ‰์…˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

middleware.js
1
2
let locales = ['en-US', 'nl-NL', 'nl']
3
4
// Get the preferred locale, similar to the above or using a library
5
function getLocale(request) { ... }
6
7
export function middleware(request) {
8
// Check if there is any supported locale in the pathname
9
const { pathname } = request.nextUrl
10
const pathnameHasLocale = locales.some(
11
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
12
)
13
14
if (pathnameHasLocale) return
15
16
// Redirect if there is no locale
17
const locale = getLocale(request)
18
request.nextUrl.pathname = `/${locale}${pathname}`
19
// e.g. incoming request is /products
20
// The new URL is now /en-US/products
21
return Response.redirect(request.nextUrl)
22
}
23
24
export const config = {
25
matcher: [
26
// Skip all internal paths (_next)
27
'/((?!_next).*)',
28
// Optional: only run on root (/) URL
29
// '/'
30
],
31
}

๋งˆ์ง€๋ง‰์œผ๋กœ app/ ๋‚ด๋ถ€์˜ ๋ชจ๋“  ํŠน์ˆ˜ ํŒŒ์ผ์ด app/[lang] ์•„๋ž˜์— ์ค‘์ฒฉ๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Next.js ๋ผ์šฐํ„ฐ๊ฐ€ ๋ผ์šฐํŠธ์—์„œ ๋‹ค์–‘ํ•œ Locale์„ ๋™์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , ๋ชจ๋“  ๋ ˆ์ด์•„์›ƒ๊ณผ ํŽ˜์ด์ง€์— lang ๋งค๊ฐœ ๋ณ€์ˆ˜๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด:

app/[lang]/page.js
1
// You now have access to the current locale
2
// e.g. /en-US/products -> `lang` is "en-US"
3
export default async function Page({ params: { lang } }) {
4
return ...
5
}

๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ์€ ์ƒˆ ํด๋”์— ์ค‘์ฒฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. (e.g. app/[lang]/layout.js)


3. Localization(์ง€์—ญํ™”)

์‚ฌ์šฉ์ž๊ฐ€ ์„ ํ˜ธํ•˜๋Š” Locale ๋˜๋Š” ํ˜„์ง€ํ™”์— ๋”ฐ๋ผ ํ‘œ์‹œ๋˜๋Š” ์ฝ˜ํ…์ธ ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์€ Next.js์—๋งŒ ๊ตญํ•œ๋œ ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. ์•„๋ž˜์— ์„ค๋ช…๋œ ํŒจํ„ด์€ ๋ชจ๋“  ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ ์˜์–ด์™€ ๋„ค๋œ๋ž€๋“œ์–ด ์ฝ˜ํ…์ธ ๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ ํ‚ค์—์„œ ํ˜„์ง€ํ™”๋œ ๋ฌธ์ž์—ด๋กœ์˜ ๋งคํ•‘์„ ์ œ๊ณตํ•˜๋Š” ๊ฐ์ฒด์ธ ๋‘ ๊ฐœ์˜ ์„œ๋กœ ๋‹ค๋ฅธ โ€˜์‚ฌ์ „(dictionaries)โ€˜์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด:

dictionaries/en.json
1
{
2
"products": {
3
"cart": "Add to Cart"
4
}
5
}
dictionaries/nl.json
1
{
2
"products": {
3
"cart": "Toevoegen aan Winkelwagen"
4
}
5
}

๊ทธ๋Ÿฐ ๋‹ค์Œ getDictionary ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์š”์ฒญ๋œ locale์— ๋Œ€ํ•œ ๋ฒˆ์—ญ์„ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

app/[lang]/dictionaries.js
1
import 'server-only'
2
3
const dictionaries = {
4
en: () => import('./dictionaries/en.json').then(module => module.default),
5
nl: () => import('./dictionaries/nl.json').then(module => module.default),
6
}
7
8
export const getDictionary = async locale => dictionaries[locale]()

ํ˜„์žฌ ์„ ํƒ๋œ ์–ธ์–ด๊ฐ€ ์ฃผ์–ด์ง€๋ฉด, ๋ ˆ์ด์•„์›ƒ์ด๋‚˜ ํŽ˜์ด์ง€ ๋‚ด์—์„œ ์‚ฌ์ „์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

app/[lang]/page.js
1
import { getDictionary } from './dictionaries'
2
3
export default async function Page({ params: { lang } }) {
4
const dict = await getDictionary(lang) // en
5
return <button>{dict.products.cart}</button> // Add to Cart
6
}

app/ ๋””๋ ‰ํ† ๋ฆฌ์˜ ๋ชจ๋“  ๋ ˆ์ด์•„์›ƒ๊ณผ ํŽ˜์ด์ง€๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ๋ฒˆ์—ญ ํŒŒ์ผ์˜ ํฌ๊ธฐ๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์ธก JavaScript ๋ฒˆ๋“ค ํฌ๊ธฐ์— ์˜ํ–ฅ์„ ๋ฏธ์น˜๋Š” ๊ฒƒ์— ๋Œ€ํ•ด ๊ฑฑ์ •ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๋Š” ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๋ฉฐ ๊ฒฐ๊ณผ HTML๋งŒ ๋ธŒ๋ผ์šฐ์ €๋กœ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.


4. Static Generation(์ •์  ์ƒ์„ฑ)

์ง€์ •๋œ locales ์ง‘ํ•ฉ์— ๋Œ€ํ•œ ์ •์  ๊ฒฝ๋กœ๋ฅผ ์ƒ์„ฑํ•˜๋ ค๋ฉด ํŽ˜์ด์ง€ ๋˜๋Š” ๋ ˆ์ด์•„์›ƒ์— generateStaticParams๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ์—์„œ ์ „์—ญ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

app/[lang]/layout.js
1
export async function generateStaticParams() {
2
return [{ lang: 'en-US' }, { lang: 'de' }]
3
}
4
5
export default function Root({ children, params }) {
6
return (
7
<html lang={params.lang}>
8
<body>{children}</body>
9
</html>
10
)
11
}

5. Resources(์ž์›)