[๐Ÿค–] Next.js App Router ๊ตญ์ œํ™” (i18n) ์™„๋ฒฝ ๊ฐ€์ด๋“œ: ๋‹ค๊ตญ์–ด ์ง€์› ๊ตฌํ˜„ ์ „๋žต

Next.js 14+ App Router ํ™˜๊ฒฝ์—์„œ ๊ตญ์ œํ™”(i18n)๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ž์„ธํžˆ ์•Œ๋ ค๋“œ๋ ค์š”. ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ™œ์šฉํ•œ ์–ธ์–ด ๊ฐ์ง€๋ถ€ํ„ฐ ํด๋ผ์ด์–ธํŠธ/์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ์˜ ๋‹ค๊ตญ์–ด ํ…์ŠคํŠธ ์ฒ˜๋ฆฌ๊นŒ์ง€, ์‹ค๋ฌด์— ๋ฐ”๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค๊ตญ์–ด ์ง€์› ์ „๋žต์„ ๋ธ”๋ฃจ๊ฐ€ ์•ˆ๋‚ดํ•ด ๋“œ๋ฆด๊ฒŒ์š”.

21๋ถ„
๋‹จ์–ด: 1,843๊ฐœ
๊ฒŒ์‹œ๊ธ€ ์ธ๋„ค์ผ
์ •๋ณด

๐Ÿค– ์ด ํฌ์ŠคํŒ…์€ Gemini 2.5 Flash AI๊ฐ€ ์ž‘์„ฑํ–ˆ์–ด์š”.
๋‚ด์šฉ์˜ ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณค์ง€๋งŒ, ์‹ค๋ฌด ์ ์šฉ ์ „ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•จ๊ป˜ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

์œ ์šฉํ•œ ํŒ

Next.js App Router ํ™˜๊ฒฝ์—์„œ next-intl ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋‹ค๊ตญ์–ด ์ง€์›(i18n)์„ ๊ตฌํ˜„ํ•˜๊ณ , ๋ฏธ๋“ค์›จ์–ด, ์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ, SEO๊นŒ์ง€ ๊ณ ๋ คํ•œ ์‹ค์šฉ์ ์ธ ๊ตญ์ œํ™” ์ „๋žต์„ ๋ฐฐ์›Œ๋ด์š”.

์•ˆ๋…•ํ•˜์„ธ์š”, 10๋…„ ์ด์ƒ ๊ฒฝ๋ ฅ์˜ ์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž ๋ธ”๋ฃจ์˜ˆ์š”. ์ €๋Š” ์‹ค์ œ ์กด์žฌํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋Š” ์•„๋‹ˆ๊ณ , ์—ฌ๋Ÿฌ๋ถ„์˜ ์„ฑ์žฅ์„ ๋•๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์˜ˆ์š”.
์˜ค๋Š˜์€ Next.js App Router ํ™˜๊ฒฝ์—์„œ ๋ณต์žกํ•˜๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ๋Š” ๊ตญ์ œํ™”(i18n) ๊ตฌํ˜„์— ๋Œ€ํ•ด ์‹ค์งˆ์ ์ธ ๊ฐ€์ด๋“œ๋ฅผ ์ œ๊ณตํ•ด ๋“œ๋ฆฌ๋ ค๊ณ  ํ•ด์š”.
๊ธ€๋กœ๋ฒŒ ์„œ๋น„์Šค๋ฅผ ๊ธฐํšํ•˜๊ฑฐ๋‚˜ ๋‹ค๊ตญ์–ด ์ง€์›์ด ํ•„์š”ํ•œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์‹ ๋‹ค๋ฉด ์ด ๊ธ€์ด ํฐ ๋„์›€์ด ๋  ๊ฑฐ์˜ˆ์š”.

๐Ÿค” ์™œ ๊ตญ์ œํ™”(i18n)๊ฐ€ ์ค‘์š”ํ•œ๊ฐ€์š”?

0๏ธโƒฃ ๊ธ€๋กœ๋ฒŒ ์„œ๋น„์Šค์˜ ํ•„์ˆ˜ ์š”์†Œ์˜ˆ์š”

ํ˜„๋Œ€ ์›น ์„œ๋น„์Šค๋Š” ๋” ์ด์ƒ ํŠน์ • ์ง€์—ญ ์‚ฌ์šฉ์ž๋งŒ์„ ์œ„ํ•œ ๊ฒƒ์ด ์•„๋‹ˆ์—์š”.
์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ์ž๋ฅผ ๋Œ€์ƒ์œผ๋กœ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋ ค๋ฉด ๊ฐ ์‚ฌ์šฉ์ž์˜ ๋ชจ๊ตญ์–ด๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ์ค‘์š”ํ•ด์š”.
๊ตญ์ œํ™”(Internationalization, i18n)๋Š” ๋ฐ”๋กœ ์ด๋Ÿฐ ๋‹ค๊ตญ์–ด ์ง€์›์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ๊ฐœ๋ฐœ ๊ณผ์ •์„ ์˜๋ฏธํ•ด์š”.
์‚ฌ์šฉ์ž๊ฐ€ ์นœ์ˆ™ํ•œ ์–ธ์–ด๋กœ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX)์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋˜๊ณ , ์„œ๋น„์Šค์˜ ์ ‘๊ทผ์„ฑ๊ณผ ์‹œ์žฅ ํ™•์žฅ์„ฑ๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋†’์•„์ ธ์š”.

1๏ธโƒฃ App Router ํ™˜๊ฒฝ์—์„œ i18n์˜ ๋ณ€ํ™”๋ฅผ ์ดํ•ดํ•ด์•ผ ํ•ด์š”

Next.js 13+ ๋ฒ„์ „์—์„œ ๋„์ž…๋œ App Router๋Š” ๊ธฐ์กด Pages Router์™€๋Š” ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•ด์š”.
ํŠนํžˆ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ(Server Components)์˜ ๋„์ž…์€ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๋ฐ ๋ Œ๋”๋ง ๋ฐฉ์‹์— ํฐ ๋ณ€ํ™”๋ฅผ ๊ฐ€์ ธ์™”๊ณ , ์ด๋Š” ๊ตญ์ œํ™” ๊ตฌํ˜„ ๋ฐฉ์‹์—๋„ ์˜ํ–ฅ์„ ๋ฏธ์ณ์š”.
Pages Router์—์„œ๋Š” ์ฃผ๋กœ next.config.js์˜ i18n ์„ค์ •์„ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ, App Router์—์„œ๋Š” ๋ฏธ๋“ค์›จ์–ด(Middleware)์™€ ๋™์  ๋ผ์šฐํŒ…(Dynamic Routing)์„ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ด์—์š”.
์ด ๊ธ€์—์„œ๋Š” App Router์˜ ํŠน์„ฑ์„ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜์—ฌ ํšจ์œจ์ ์ด๊ณ  ๊ฒฌ๊ณ ํ•œ i18n ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค๋“œ๋ฆด ๊ฑฐ์˜ˆ์š”.

โš™๏ธ Next.js App Router i18n ํ•ต์‹ฌ ์ „๋žต

Next.js App Router์—์„œ ๊ตญ์ œํ™”๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ์—๋Š” ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์ด ์žˆ์ง€๋งŒ, next-intl ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ด๊ณ  ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜์˜ˆ์š”.
next-intl์€ App Router์˜ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์™€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋ชจ๋‘์—์„œ ์›ํ™œํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋ฉฐ, ๋‹ค์–‘ํ•œ ๊ตญ์ œํ™” ๊ธฐ๋Šฅ์„ ํŽธ๋ฆฌํ•˜๊ฒŒ ์ œ๊ณตํ•ด์š”.

0๏ธโƒฃ ๊ธฐ๋ณธ ๊ฐœ๋…: ์–ธ์–ด ๊ฐ์ง€, ๋ผ์šฐํŒ…, ํ…์ŠคํŠธ ๊ด€๋ฆฌ์˜ˆ์š”

๊ตญ์ œํ™” ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ๋•Œ ๊ณ ๋ คํ•ด์•ผ ํ•  ํ•ต์‹ฌ ์š”์†Œ๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•„์š”.

  • ์–ธ์–ด ๊ฐ์ง€ (Locale Detection): ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ € ์„ค์ •, IP ์ฃผ์†Œ, ๋˜๋Š” ์ด์ „์— ์ €์žฅ๋œ ์ฟ ํ‚ค ๋“ฑ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ์„ ํ˜ธ ์–ธ์–ด๋ฅผ ๊ฐ์ง€ํ•˜๋Š” ๊ธฐ๋Šฅ์ด์—์š”.
  • ๋ผ์šฐํŒ… (Routing): URL์— ์–ธ์–ด ์ฝ”๋“œ๋ฅผ ํฌํ•จ์‹œ์ผœ ํŠน์ • ์–ธ์–ด ๋ฒ„์ „์˜ ํŽ˜์ด์ง€๋กœ ๋ผ์šฐํŒ…ํ•˜๋Š” ๋ฐฉ์‹์ด์—์š”. (์˜ˆ: /en/about, /ko/about)
  • ํ…์ŠคํŠธ ๊ด€๋ฆฌ (Message Management): ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์˜ ๋ชจ๋“  ํ…์ŠคํŠธ๋ฅผ ์–ธ์–ด๋ณ„๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๊ณ , ํ•„์š”ํ•œ ๊ณณ์—์„œ ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋Šฅ์ด์—์š”.

1๏ธโƒฃ next-intl ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๋Š” ์ด์œ ์˜ˆ์š”

next-intl์€ Next.js App Router์— ์ตœ์ ํ™”๋œ ๊ตญ์ œํ™” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ˆ์š”.

  • App Router ์ง€์›: ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์™€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋ชจ๋‘์—์„œ ๋ฒˆ์—ญ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ์–ด์š”.
  • ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ง€์›: ๋ฉ”์‹œ์ง€(๋ฒˆ์—ญ ํ…์ŠคํŠธ)์— ๋Œ€ํ•œ ๊ฐ•๋ ฅํ•œ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ง€์›์„ ํ†ตํ•ด ๊ฐœ๋ฐœ ์‹œ ์˜ค๋ฅ˜๋ฅผ ์ค„์ด๊ณ  ์ƒ์‚ฐ์„ฑ์„ ๋†’์—ฌ์ค˜์š”.
  • ๋ฏธ๋“ค์›จ์–ด ํ†ตํ•ฉ: Next.js ๋ฏธ๋“ค์›จ์–ด์™€ ์—ฐ๋™ํ•˜์—ฌ ์–ธ์–ด ๊ฐ์ง€ ๋ฐ ๋ผ์šฐํŒ…์„ ์†์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • ์œ ์—ฐํ•œ ๋ฒˆ์—ญ ํ˜•์‹: JSON ํŒŒ์ผ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฒˆ์—ญ ๋ฉ”์‹œ์ง€๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉฐ, ICU Message Format์„ ์ง€์›ํ•˜์—ฌ ๋ณต์ˆ˜ํ˜•, ์„ฑ๋ณ„ ๋“ฑ ๋ณต์žกํ•œ ๋ฒˆ์—ญ ๊ทœ์น™์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

2๏ธโƒฃ next-intl ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ •์ด์—์š”

๋จผ์ € next-intl ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ด ์ฃผ์„ธ์š”.

npm install next-intl # ๋˜๋Š” yarn add next-intl # ๋˜๋Š” pnpm add next-intl

์„ค์น˜ ํ›„, ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— i18n.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๊ธฐ๋ณธ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด ์ค„๊ฒŒ์š”.
์ด ํŒŒ์ผ์€ ๊ฐ ๋กœ์ผ€์ผ(locale)์— ํ•ด๋‹นํ•˜๋Š” ๋ฒˆ์—ญ ๋ฉ”์‹œ์ง€๋ฅผ ๋™์ ์œผ๋กœ ๋กœ๋“œํ•˜๋Š” ์—ญํ• ์„ ํ•ด์š”.

import {getRequestConfig} from 'next-intl/server'; export default getRequestConfig(async ({locale}) => ({ messages: (await import(`../messages/${locale}.json`)).default }));

๊ทธ๋ฆฌ๊ณ  ๋ฒˆ์—ญ ๋ฉ”์‹œ์ง€ ํŒŒ์ผ์„ ์ค€๋น„ํ•ด์•ผ ํ•ด์š”.
ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— messages ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ณ , ๊ฐ ์–ธ์–ด๋ณ„ JSON ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด ์ฃผ์„ธ์š”.
์˜ˆ์‹œ๋กœ en.json๊ณผ ko.json ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ๋ณผ๊ฒŒ์š”.

messages/en.json:

{ "Index": { "title": "Hello Next.js i18n", "description": "This is a sample application with internationalization.", "changeLanguage": "Change Language" }, "About": { "title": "About Us", "content": "We are a team of passionate developers." }, "NotFound": { "title": "Page Not Found", "description": "The page you are looking for does not exist." } }

messages/ko.json:

{ "Index": { "title": "์•ˆ๋…•ํ•˜์„ธ์š” Next.js ๊ตญ์ œํ™”", "description": "์ด๊ฒƒ์€ ๊ตญ์ œํ™”๊ฐ€ ์ ์šฉ๋œ ์ƒ˜ํ”Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค.", "changeLanguage": "์–ธ์–ด ๋ณ€๊ฒฝ" }, "About": { "title": "ํšŒ์‚ฌ ์†Œ๊ฐœ", "content": "์ €ํฌ๋Š” ์—ด์ •์ ์ธ ๊ฐœ๋ฐœ์ž ํŒ€์ž…๋‹ˆ๋‹ค." }, "NotFound": { "title": "ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", "description": "์ฐพ์œผ์‹œ๋Š” ํŽ˜์ด์ง€๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." } }

๐Ÿ› ๏ธ ๋‹จ๊ณ„๋ณ„ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ App Router์— next-intl์„ ์ ์šฉํ•ด ๋ณผ๊นŒ์š”?

0๏ธโƒฃ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ™œ์šฉํ•œ ์–ธ์–ด ๊ฐ์ง€ ๋ฐ ๋ฆฌ๋‹ค์ด๋ ‰์…˜์ด์—์š”

Next.js ๋ฏธ๋“ค์›จ์–ด๋Š” ์š”์ฒญ์ด ์ฒ˜๋ฆฌ๋˜๊ธฐ ์ „์— ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ์˜ˆ์š”.
์ด๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ์„ ํ˜ธ ์–ธ์–ด๋ฅผ ๊ฐ์ง€ํ•˜๊ณ , ํ•ด๋‹น ์–ธ์–ด์˜ URL๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ํ•  ์ˆ˜ ์žˆ์–ด์š”.
ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— middleware.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ์„ธ์š”.

import createMiddleware from 'next-intl/middleware'; export default createMiddleware({ // A list of all locales that are supported locales: ['en', 'ko'], // Used when no locale matches defaultLocale: 'en', localePrefix: 'as-needed' }); export const config = { // Match only internationalized pathnames matcher: ['/', '/(ko|en)/:path*'] };
  • locales: ์ง€์›ํ•˜๋Š” ๋ชจ๋“  ๋กœ์ผ€์ผ ๋ชฉ๋ก์„ ์ง€์ •ํ•ด์š”.
  • defaultLocale: URL์— ๋กœ์ผ€์ผ์ด ๋ช…์‹œ๋˜์ง€ ์•Š์•˜์„ ๋•Œ ์‚ฌ์šฉ๋  ๊ธฐ๋ณธ ๋กœ์ผ€์ผ์„ ์ง€์ •ํ•ด์š”.
  • localePrefix: 'as-needed': ๊ธฐ๋ณธ ๋กœ์ผ€์ผ(์—ฌ๊ธฐ์„œ๋Š” en)์˜ ๊ฒฝ์šฐ URL์— ์ ‘๋‘์‚ฌ๋ฅผ ๋ถ™์ด์ง€ ์•Š๊ณ , ๋‹ค๋ฅธ ๋กœ์ผ€์ผ(ko)์—๋งŒ ์ ‘๋‘์‚ฌ๋ฅผ ๋ถ™์ด๋„๋ก ์„ค์ •ํ•ด์š”. (์˜ˆ: /about ๋Œ€์‹  /ko/about).
  • matcher: ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์ ์šฉ๋  ๊ฒฝ๋กœ๋ฅผ ์ •๊ทœ์‹์œผ๋กœ ์ง€์ •ํ•ด์š”. /(ko|en)/:path*๋Š” /ko/ ๋˜๋Š” /en/์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ๊ฒฝ๋กœ์™€ ๋ฃจํŠธ ๊ฒฝ๋กœ(/)์— ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•˜๋ผ๋Š” ์˜๋ฏธ์˜ˆ์š”.

1๏ธโƒฃ App Router์—์„œ ๋กœ์ผ€์ผ ๋ผ์šฐํŒ… ์„ค์ •ํ•˜๊ธฐ์˜ˆ์š”

App Router์—์„œ๋Š” [locale]์ด๋ผ๋Š” ๋™์  ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ์ผ€์ผ ๋ผ์šฐํŒ…์„ ๊ตฌํ˜„ํ•ด์š”.
์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐ ๋กœ์ผ€์ผ์— ํ•ด๋‹นํ•˜๋Š” ํŽ˜์ด์ง€๋ฅผ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

app/[locale]/layout.tsx ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ๋ชจ๋“  ํŽ˜์ด์ง€์— next-intl์˜ NextIntlClientProvider๋ฅผ ์ ์šฉํ•ด ์ฃผ์„ธ์š”.

import {NextIntlClientProvider} from 'next-intl'; import {getRequestConfig} from 'next-intl/server'; type Props = { children: React.ReactNode; params: {locale: string}; }; export default async function LocaleLayout({children, params: {locale}}: Props) { const messages = (await import(`../../messages/${locale}.json`)).default; return ( <html lang={locale}> <body> <NextIntlClientProvider messages={messages}> {children} </NextIntlClientProvider> </body> </html> ); }
  • getRequestConfig๋Š” ์„œ๋ฒ„์—์„œ ๋ฒˆ์—ญ ๋ฉ”์‹œ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์—ญํ• ์„ ํ•ด์š”.
  • NextIntlClientProvider๋Š” ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฒˆ์—ญ ๋ฉ”์‹œ์ง€์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์ปจํ…์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•ด์š”.

๋˜ํ•œ, app/layout.tsx ํŒŒ์ผ์€ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜, ์ „์—ญ์ ์ธ ์„ค์ •์„ ๋‹ด๋‹นํ•˜๋„๋ก ๋น„์›Œ๋‘๊ณ  app/[locale]/layout.tsx๊ฐ€ ์‹ค์ œ ๋ ˆ์ด์•„์›ƒ ์—ญํ• ์„ ํ•˜๋„๋ก ํ•ด์ฃผ์„ธ์š”.

2๏ธโƒฃ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋‹ค๊ตญ์–ด ํ…์ŠคํŠธ ์‚ฌ์šฉํ•˜๊ธฐ์˜ˆ์š”

next-intl์€ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ๋ฒˆ์—ญ ๊ธฐ๋Šฅ์„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก useTranslations ํ›…์˜ ์„œ๋ฒ„ ๋ฒ„์ „๊ณผ getTranslator ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•ด์š”.
ํ•˜์ง€๋งŒ next-intl/server์—์„œ ์ œ๊ณตํ•˜๋Š” getTranslator๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ๋ช…ํ™•ํ•˜๊ณ  ๊ถŒ์žฅ๋˜๋Š” ๋ฐฉ๋ฒ•์ด์—์š”.

app/[locale]/page.tsx:

import {getTranslator} from 'next-intl/server'; import Link from 'next/link'; export default async function IndexPage({params: {locale}}: {params: {locale: string}}) { const t = await getTranslator(locale, 'Index'); return ( <div> <h1>{t('title')}</h1> <p>{t('description')}</p> <Link href="/about"> {t('changeLanguage')} </Link> </div> ); }
  • getTranslator(locale, 'Index')๋ฅผ ํ†ตํ•ด Index ๋„ค์ž„์ŠคํŽ˜์ด์Šค์— ํ•ด๋‹นํ•˜๋Š” ๋ฒˆ์—ญ ํ•จ์ˆ˜ t๋ฅผ ๊ฐ€์ ธ์™€์š”.
  • t('title')๊ณผ ๊ฐ™์ด ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋œ ํ…์ŠคํŠธ๋ฅผ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ์–ด์š”.

3๏ธโƒฃ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋‹ค๊ตญ์–ด ํ…์ŠคํŠธ ์‚ฌ์šฉํ•˜๊ธฐ์˜ˆ์š”

ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” next-intl์˜ useTranslations ํ›…์„ ์‚ฌ์šฉํ•ด์š”.

๋จผ์ € app/[locale]/components/LocaleSwitcher.tsx ๊ฐ™์€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด ๋ณผ๊ฒŒ์š”.

'use client'; import {useTranslations} from 'next-intl'; import {useRouter, usePathname} from 'next/navigation'; import {useLocale} from 'next-intl'; export function LocaleSwitcher() { const t = useTranslations('Index'); const router = useRouter(); const pathname = usePathname(); const currentLocale = useLocale(); const handleLocaleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const newLocale = e.target.value; // ํ˜„์žฌ ๊ฒฝ๋กœ์—์„œ ๋กœ์ผ€์ผ ๋ถ€๋ถ„๋งŒ ๋ณ€๊ฒฝํ•˜์—ฌ ์ƒˆ ๊ฒฝ๋กœ๋ฅผ ์ƒ์„ฑ const newPath = `/${newLocale}${pathname.substring(3)}`; // ์˜ˆ: /ko/about -> /en/about router.push(newPath); }; return ( <select onChange={handleLocaleChange} value={currentLocale}> <option value="en">English</option> <option value="ko">ํ•œ๊ตญ์–ด</option> </select> ); }

๊ทธ๋ฆฌ๊ณ  ์ด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์ธ app/[locale]/page.tsx์—์„œ ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ์š”.

app/[locale]/page.tsx (์ˆ˜์ •):

import {getTranslator} from 'next-intl/server'; import {LocaleSwitcher} from './components/LocaleSwitcher'; // ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ์ž„ํฌํŠธ import Link from 'next/link'; export default async function IndexPage({params: {locale}}: {params: {locale: string}}) { const t = await getTranslator(locale, 'Index'); return ( <div> <h1>{t('title')}</h1> <p>{t('description')}</p> <Link href="/about">{t('changeLanguage')}</Link> <LocaleSwitcher /> {/* ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ */} </div> ); }

4๏ธโƒฃ ๋™์  ๋ผ์šฐํŒ… ๋ฐ URL ๋งค๊ฐœ๋ณ€์ˆ˜ ์ฒ˜๋ฆฌ์˜ˆ์š”

๋™์  ๋ผ์šฐํŒ… (app/[locale]/[slug]/page.tsx์™€ ๊ฐ™์€ ํ˜•ํƒœ)์—์„œ๋„ params.locale์„ ํ†ตํ•ด ํ˜„์žฌ ๋กœ์ผ€์ผ์„ ์–ป์„ ์ˆ˜ ์žˆ์–ด์š”.

app/[locale]/about/page.tsx:

import {getTranslator} from 'next-intl/server'; export default async function AboutPage({params: {locale}}: {params: {locale: string}}) { const t = await getTranslator(locale, 'About'); return ( <div> <h1>{t('title')}</h1> <p>{t('content')}</p> </div> ); }

5๏ธโƒฃ SEO๋ฅผ ์œ„ํ•œ alternate ํƒœ๊ทธ ์ถ”๊ฐ€์˜ˆ์š”

๋‹ค๊ตญ์–ด ์‚ฌ์ดํŠธ์˜ SEO๋Š” ๋งค์šฐ ์ค‘์š”ํ•ด์š”.
๊ฒ€์ƒ‰ ์—”์ง„์ด ๊ฐ ์–ธ์–ด ๋ฒ„์ „์˜ ํŽ˜์ด์ง€๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ธ์‹ํ•˜๋„๋ก rel="alternate" ๋งํฌ ํƒœ๊ทธ๋ฅผ <head>์— ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด์š”.
next-intl์€ ์ด๋ฅผ ์œ„ํ•œ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, Next.js์˜ Metadata API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ง์ ‘ ์ถ”๊ฐ€ํ•ด ์ค„ ์ˆ˜ ์žˆ์–ด์š”.

app/[locale]/layout.tsx์— generateMetadata ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ด ๋ณผ๊ฒŒ์š”.

import {NextIntlClientProvider} from 'next-intl'; import {getRequestConfig} from 'next-intl/server'; import type {Metadata} from 'next'; type Props = { children: React.ReactNode; params: {locale: string}; }; export async function generateMetadata({params: {locale}}: Props): Promise<Metadata> { const messages = (await import(`../../messages/${locale}.json`)).default; const t = (key: string) => messages.Index[key as keyof typeof messages.Index]; return { title: t('title'), description: t('description'), alternates: { canonical: `/${locale}`, languages: { 'en': '/en', 'ko': '/ko', 'x-default': '/en' // ๊ธฐ๋ณธ ์–ธ์–ด (ํด๋ฐฑ) } } }; } export default async function LocaleLayout({children, params: {locale}}: Props) { const messages = (await import(`../../messages/${locale}.json`)).default; return ( <html lang={locale}> <body> <NextIntlClientProvider messages={messages}> {children} </NextIntlClientProvider> </body> </html> ); }
  • generateMetadata ํ•จ์ˆ˜๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜๋ฉฐ, ํŽ˜์ด์ง€์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋™์ ์œผ๋กœ ์ƒ์„ฑํ•ด์š”.
  • alternates.languages ๊ฐ์ฒด์— ๊ฐ ๋กœ์ผ€์ผ๋ณ„ URL๊ณผ x-default (๊ธฐ๋ณธ ํด๋ฐฑ ์–ธ์–ด)๋ฅผ ์ง€์ •ํ•˜์—ฌ ๊ฒ€์ƒ‰ ์—”์ง„์— ํžŒํŠธ๋ฅผ ์ œ๊ณตํ•ด์š”.
  • ์ด๋ ‡๊ฒŒ ์„ค์ •ํ•˜๋ฉด ๊ฒ€์ƒ‰ ์—”์ง„์ด ํŠน์ • ์–ธ์–ด ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ์˜ฌ๋ฐ”๋ฅธ ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค€๋‹ต๋‹ˆ๋‹ค.

๐Ÿ’ก ์‹ค์ „ ํŒ ๋ฐ ๊ณ ๋ ค์‚ฌํ•ญ

์„ฑ๊ณต์ ์ธ ๊ตญ์ œํ™” ๊ตฌํ˜„์„ ์œ„ํ•ด ๋ช‡ ๊ฐ€์ง€ ์ถ”๊ฐ€ ํŒ์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”.

0๏ธโƒฃ ๋ฒˆ์—ญ ํŒŒ์ผ ๊ด€๋ฆฌ ์ „๋žต์ด์—์š”

ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ๊ฐ€ ์ปค์ง€๋ฉด ๋ฒˆ์—ญ ํŒŒ์ผ์˜ ์–‘๋„ ๋งŽ์•„์ง€๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์–ด๋ ค์›Œ์งˆ ์ˆ˜ ์žˆ์–ด์š”.

  • ๋„ค์ž„์ŠคํŽ˜์ด์Šค ๋ถ„๋ฆฌ: Index, About์ฒ˜๋Ÿผ ํŽ˜์ด์ง€๋‚˜ ์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„๋กœ ๋ฒˆ์—ญ ๋ฉ”์‹œ์ง€๋ฅผ ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋กœ ๋ถ„๋ฆฌํ•˜๋ฉด ๊ด€๋ฆฌํ•˜๊ธฐ ์šฉ์ดํ•ด์š”.
  • ๋ฒˆ์—ญ ๊ด€๋ฆฌ ์„œ๋น„์Šค ํ™œ์šฉ: Crowdin, Lokalise, Transifex์™€ ๊ฐ™์€ ์ „๋ฌธ ๋ฒˆ์—ญ ๊ด€๋ฆฌ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฒˆ์—ญ ํ”„๋กœ์„ธ์Šค๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ํŒ€์›๋“ค๊ณผ ํ˜‘์—…ํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • ICU Message Format: ๋ณต์ˆ˜ํ˜•, ์„ฑ๋ณ„, ๋‚ ์งœ/์‹œ๊ฐ„ ํ˜•์‹ ๋“ฑ ๋ณต์žกํ•œ ๋ฒˆ์—ญ ๊ทœ์น™์€ ICU Message Format์„ ํ™œ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”. next-intl์€ ์ด๋ฅผ ์ง€์›ํ•ด์š”.

1๏ธโƒฃ ํด๋ฐฑ ๋กœ์ผ€์ผ ๋ฐ ๋ˆ„๋ฝ๋œ ๋ฒˆ์—ญ ์ฒ˜๋ฆฌ์˜ˆ์š”

๋ฒˆ์—ญ ํŒŒ์ผ์— ํŠน์ • ํ‚ค๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์„ ๋•Œ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ์ „๋žต์„ ์„ธ์›Œ์•ผ ํ•ด์š”.
next-intl์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ˆ„๋ฝ๋œ ํ‚ค์— ๋Œ€ํ•ด ๊ฒฝ๊ณ ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ , ํ•ด๋‹น ํ‚ค๋ฅผ ๊ทธ๋Œ€๋กœ ์ถœ๋ ฅํ•ด์š”.

i18n.ts ํŒŒ์ผ์—์„œ defaultMessage ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ˆ„๋ฝ๋œ ๋ฒˆ์—ญ์— ๋Œ€ํ•œ ํด๋ฐฑ ๋ฉ”์‹œ์ง€๋ฅผ ์ง€์ •ํ•  ์ˆ˜๋„ ์žˆ์–ด์š”.

import {getRequestConfig} from 'next-intl/server'; export default getRequestConfig(async ({locale}) => ({ messages: (await import(`../messages/${locale}.json`)).default, // ๋ˆ„๋ฝ๋œ ๋ฒˆ์—ญ ๋ฉ”์‹œ์ง€์— ๋Œ€ํ•œ ํด๋ฐฑ ์ฒ˜๋ฆฌ // defaultMessage: ({id, defaultMessage}) => defaultMessage || id, // defaultMessage: '๋ฒˆ์—ญ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค' }));

ํ•˜์ง€๋งŒ ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ ๋ฒˆ์—ญ ํŒŒ์ผ์ด ์™„๋ฒฝํ•˜๊ฒŒ ์ฑ„์›Œ์ง€๋„๋ก ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด๊ฒ ์ฃ ?

2๏ธโƒฃ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•ด์š”

๋ฒˆ์—ญ ๋ฉ”์‹œ์ง€ ํŒŒ์ผ์ด ๋งค์šฐ ์ปค์ง€๋ฉด ์ดˆ๊ธฐ ๋กœ๋”ฉ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์–ด์š”.

  • ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…: getRequestConfig์—์„œ import()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•„์š”ํ•œ ๋กœ์ผ€์ผ ํŒŒ์ผ๋งŒ ๋™์ ์œผ๋กœ ๋กœ๋“œํ•จ์œผ๋กœ์จ ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•˜๊ณ  ์žˆ์–ด์š”. ์ด๊ฒƒ์ด next-intl์˜ ๊ธฐ๋ณธ ๋™์ž‘์ด์—์š”.
  • ๋ฉ”์‹œ์ง€ ์บ์‹ฑ: ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ getTranslator๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ, Next.js์˜ ๋ฆฌํ€˜์ŠคํŠธ ์บ์‹œ(Request Cache)๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋™์ผํ•œ ์š”์ฒญ ๋‚ด์—์„œ ์ค‘๋ณต ๋กœ๋“œ๋ฅผ ๋ฐฉ์ง€ํ•ด์š”. ๋”ฐ๋ผ์„œ ๋ถˆํ•„์š”ํ•œ ๋ฉ”์‹œ์ง€ ๋กœ๋”ฉ์„ ๊ฑฑ์ •ํ•  ํ•„์š”๋Š” ์—†์–ด์š”.

๐Ÿ“ ์ •๋ฆฌํ•˜๋ฉฐ

0๏ธโƒฃ ํ•ต์‹ฌ ์š”์•ฝ์ด์—์š”

์˜ค๋Š˜์€ Next.js App Router ํ™˜๊ฒฝ์—์„œ next-intl ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๊ตญ์ œํ™”(i18n)๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ž์„ธํžˆ ์‚ดํŽด๋ณด์•˜์–ด์š”.

  • next-intl ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ •: i18n.ts์™€ messages ํด๋”๋ฅผ ๊ตฌ์„ฑํ–ˆ์–ด์š”.
  • ๋ฏธ๋“ค์›จ์–ด ํ™œ์šฉ: middleware.ts๋ฅผ ํ†ตํ•ด ์–ธ์–ด๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ๋กœ์ผ€์ผ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…์„ ๊ตฌํ˜„ํ–ˆ์–ด์š”.
  • app/[locale]/layout.tsx: NextIntlClientProvider๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•˜๊ณ  generateMetadata๋กœ SEO๋ฅผ ์œ„ํ•œ alternate ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์–ด์š”.
  • ์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ: getTranslator์™€ useTranslations ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ ํ…์ŠคํŠธ๋ฅผ ์ถœ๋ ฅํ–ˆ์–ด์š”.

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์˜ Next.js ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋‹ค๊ตญ์–ด ์ง€์› ๊ธฐ๋Šฅ์„ ์ž์‹  ์žˆ๊ฒŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์˜ˆ์š”.

1๏ธโƒฃ ๋‹ค์Œ ์•ก์…˜์ด์—์š”

์ด ๊ฐ€์ด๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ง์ ‘ ํ”„๋กœ์ ํŠธ์— next-intl์„ ์ ์šฉํ•ด ๋ณด์„ธ์š”.
๊ทธ๋ฆฌ๊ณ  next-intl ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ๋” ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ(๋‚ ์งœ/์‹œ๊ฐ„ ํฌ๋งคํŒ…, ๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ ๋“ฑ)์„ ํƒ์ƒ‰ํ•ด ๋ณด์‹œ๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ด์š”.
๊ธ€๋กœ๋ฒŒ ์‚ฌ์šฉ์ž๋“ค์„ ์œ„ํ•œ ๋” ๋‚˜์€ ์›น ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋‚˜๊ฐ€์‹œ๊ธธ ๋ฐ”๋ผ์š”!

๐Ÿ“ฎ ์ฐธ๊ณ 

์—ฐ๊ด€๋œ ํฌ์ŠคํŠธ