🌐 Next.jsμ—μ„œ λ‹€κ΅­μ–΄ μ§€μ›ν•˜κΈ° (next-intl)

HaileyΒ·2024λ…„ 4μ›” 30일

next-intl

λͺ©λ‘ 보기
1/3

Next.js App Routerμ—μ„œ next-intl을 μ‚¬μš©ν•΄ λ‹€κ΅­μ–΄λ₯Ό μ§€μ›ν•˜λŠ” 방법에 λŒ€ν•΄ μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.


λ“€μ–΄κ°€λ©°

λ¨Όμ €, Next.js κ³΅μ‹λ¬Έμ„œ Routing > Internationalization νŽ˜μ΄μ§€λ₯Ό μ‚΄νŽ΄λ³΄λ©΄, Accept-Language 헀더와 미듀웨어, json 파일 등을 μ‚¬μš©ν•˜μ—¬ λ‹€κ΅­μ–΄λ₯Ό μ§€μ›ν•˜λŠ” 방법을 μ†Œκ°œν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ μ €λŠ” 더 νŽΈλ¦¬ν•œ 개발과 μ•ˆμ •μ μΈ μ„œλΉ„μŠ€λ₯Ό μœ„ν•΄ μ™ΈλΆ€ 라이브러리λ₯Ό λ„μž…ν•˜κΈ°λ‘œ κ²°μ •ν–ˆκ³ , μ•„λž˜μ™€ 같은 이유둜 next-intl을 μ‚¬μš©ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

next-intl을 μ„ νƒν•œ 이유

  • npm trends둜 비ꡐ해 λ³΄μ•˜μ„ λ•Œ λ‹€μš΄λ‘œλ“œ μˆ˜κ°€ μ›”λ“±νžˆ λ†’μŒ.
  • Next.js App Router와 Server Componentλ₯Ό 지원함.
  • κ³΅μ‹λ¬Έμ„œκ°€ 잘 μž‘μ„±λ˜μ–΄ 있음.
  • μ„€μΉ˜μ™€ μ‚¬μš©μ΄ κ°„νŽΈν•˜λ©΄μ„œ ν•„μš”ν•œ κΈ°λŠ₯을 λ‹€ μ œκ³΅ν•¨.

그러면 본격적으둜 next-intl을 μ‚¬μš©ν•˜λŠ” 방법을 μ†Œκ°œν•˜κ² μŠ΅λ‹ˆλ‹€.


μ„€μΉ˜ν•˜κΈ° πŸ’ͺ

TipπŸ’‘ κ³΅μ‹λ¬Έμ„œμ— 방법이 잘 λ‚˜μ™€ μžˆμ–΄μ„œ λ¬Έμ„œλ₯Ό λ³΄λ©΄μ„œ λ§Œλ“€μ–΄ λ³΄μ‹œκ³ , 이해가 μ•ˆ λ˜κ±°λ‚˜ μ—λŸ¬κ°€ λ°œμƒν•˜λŠ” κ²½μš°μ—λ§Œ 제 글을 μ°Έκ³ ν•˜μ‹œλ©΄ 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€.

파일 ꡬ쑰

λ¨Όμ €, 파일 κ΅¬μ‘°λŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.
npm i -D next-intl μ»€λ§¨λ“œλ‘œ νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ν•˜μ‹œκ³ , μ•„λž˜μ˜ 파일과 폴더λ₯Ό λ§Œλ“€μ–΄ μ£Όμ„Έμš”.

β”œβ”€β”€ messages (1)
β”‚   β”œβ”€β”€ en.json
β”‚   └── ...
β”œβ”€β”€ next.config.mjs (2)
└── src
    β”œβ”€β”€ i18n.ts (3)
    β”œβ”€β”€ middleware.ts (4)
    └── app
        └── [locale]
            β”œβ”€β”€ layout.tsx (5)
            └── page.tsx (6)

1. messages

root/messages 폴더 μ•ˆμ— μ§€μ›ν•˜μ‹€ μ–Έμ–΄λ§ˆλ‹€ json νŒŒμΌμ„ λ§Œλ“€μ–΄ μ£Όμ„Έμš”.
λ‚˜μ€‘μ— 잘 μž‘λ™ν•˜λŠ”μ§€ 확인할 수 μžˆλ„λ‘ κ°„λ‹¨ν•œ ν…ŒμŠ€νŠΈ λ‚΄μš©μ„ μž‘μ„±ν•˜μ‹œλŠ” 것도 μ’‹μŠ΅λ‹ˆλ‹€.

μ €λŠ” μ΄λ ‡κ²Œ μž‘μ„±ν•΄ λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

// messages/en.json

{	
	"Home": {
		"title": "Hi! I'm Hailey Kim.",
		"subtitle": "I'm a front-end developer."
	}
}

// messages/ko.json

{	
	"Home": {
		"title": "μ•ˆλ…•ν•˜μ„Έμš”!",
		"subtitle": "ν”„λ‘ νŠΈμ—”λ“œ 개발자 κΉ€ν˜œμ›μž…λ‹ˆλ‹€."
	}
}

2. next.config.mjs

μ›λž˜ 있던 next.config.mjs νŒŒμΌμ— μ•„λž˜μ˜ μ½”λ“œλ₯Ό μž‘μ„±ν•΄ μ£Όμ„Έμš”. (next.config.js νŒŒμΌμ„ μ‚¬μš©ν•˜μ‹ λ‹€λ©΄ κ³΅μ‹λ¬Έμ„œλ₯Ό μ°Έκ³ ν•΄ μ£Όμ„Έμš”.)

// next.config.mjs

import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin();

/** @type {import('next').NextConfig} */
const nextConfig = {};

export default withNextIntl(nextConfig);

3. src/i18n.ts

src 폴더 μ•ˆμ— λ§Œλ“  i18n.ts νŒŒμΌμ— μ•„λž˜μ˜ μ½”λ“œλ₯Ό μž‘μ„±ν•΄ μ£Όμ„Έμš”.

// src/i18n.ts

import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
 
// 이 뢀뢄은 λ‹€λ₯Έ νŒŒμΌμ—μ„œ μž„ν¬νŠΈν•΄ 올 수 μžˆμŠ΅λ‹ˆλ‹€.
const locales = ['en', 'de'];
 
export default getRequestConfig(async ({locale}) => {
  if (!locales.includes(locale as any)) notFound();
 
  return {
    messages: (await import(`../messages/${locale}.json`)).default
  };
});

+ localesλ₯Ό config둜 κ΄€λ¦¬ν•˜κΈ° (선택 사항)

μ €λŠ” μœ„μ— localesλ₯Ό config νŒŒμΌμ— λ”°λ‘œ λ‹΄μ•„μ„œ κ΄€λ¦¬ν•˜κΈ°λ‘œ κ²°μ •ν–ˆμŠ΅λ‹ˆλ‹€.

κ·Έλž˜μ„œ μ•„λž˜μ™€ 같이 intl.config.ts(파일λͺ…은 μƒκ΄€μ—†μŠ΅λ‹ˆλ‹€)λ₯Ό root에 μƒμ„±ν•΄μ„œ localesλ₯Ό export ν•˜κ³ , i18n.tsμ—μ„œ import ν•˜λ„λ‘ μ½”λ“œλ₯Ό μž‘μ„±ν–ˆμŠ΅λ‹ˆλ‹€.

// intl.config.ts

export const locales: string[] = ['en', 'ko'];

// src/i18n.ts

import { notFound } from 'next/navigation';
import { getRequestConfig } from 'next-intl/server';
import { locales } from '../intl.config';

export default getRequestConfig(async ({ locale }) => {
    if (!locales.includes(locale as any)) notFound();

    return {
        messages: (await import(`../messages/${locale}.json`)).default,
    };
});

4. src/middleware.ts

src 폴더 μ•ˆμ— λ§Œλ“  middleware.ts νŒŒμΌμ— μ•„λž˜μ˜ μ½”λ“œλ₯Ό μž‘μ„±ν•΄ μ£Όμ„Έμš”.

// src/middleware.ts

import createMiddleware from 'next-intl/middleware';
 
export default createMiddleware({
  locales: ['en', 'ko'],
  defaultLocale: 'en'
});
 
export const config = {
  matcher: ['/', '/(en|ko)/:path*']
};

+ localesλ₯Ό λ‹€λ₯Έ νŒŒμΌμ—μ„œ λΆˆλŸ¬μ˜€λŠ” 경우

// src/middleware.ts

import createMiddleware from 'next-intl/middleware';
import { locales } from '../intl.config';

export default createMiddleware({
    locales,
    defaultLocale: locales[0],
});

export const config = {
    matcher: ['/', '/(en|ko)/:path*'],
};

config.matcher 뢀뢄도 '/(en|ko)/:path*'λ₯Ό 직접 μ“°λŠ” 게 μ•„λ‹ˆλΌ localesλ₯Ό μ΄μš©ν•΄ 보렀고 ν–ˆμ§€λ§Œ, array.map을 μ‚¬μš©ν•˜κ±°λ‚˜ μ™ΈλΆ€μ—μ„œ matcherλ₯Ό λ§Œλ“€μ–΄ μ‚¬μš©ν•˜λŠ” λ“±μ˜ 방법은 λͺ¨λ‘ 404 μ—λŸ¬κ°€ λ°œμƒν•΄μ„œ en|koλ₯Ό 직접 μž‘μ„±ν•  μˆ˜λ°–μ— μ—†μ—ˆμŠ΅λ‹ˆλ‹€.

λ§Œμ•½ 이게 정말 μ‹«λ‹€λ©΄, Next.js κ³΅μ‹λ¬Έμ„œμ—μ„œ μ‚¬μš©ν•œ 것과 같이 matcher: ['/((?!_next).*)']λ₯Ό μ“°λŠ” 방법도 μžˆμŠ΅λ‹ˆλ‹€. (μ‹€μ œλ‘œ ν•΄λ΄€κ³ , μ—λŸ¬ 없이 잘 μž‘λ™ν•©λ‹ˆλ‹€.)

5. app/[locale]

src/app μ•ˆμ— [locale] 폴더λ₯Ό μƒμ„±ν•˜κ³ , 폴더 μ•ˆμ— layout.tsx와 page.tsxλ₯Ό 생성해 μ£Όμ„Έμš”. μ•„λž˜μ˜ μ½”λ“œλ₯Ό μž‘μ„±ν•΄ μ£Όμ„Έμš”.

5-1. layout.tsx

// app/[locale]/layout.tsx

export default function LocaleLayout({
  children,
  params: {locale}
}: {
  children: React.ReactNode;
  params: {locale: string};
}) {
  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  );
}

5-2. page.tsx

// app/[locale]/page.tsx

import {useTranslations} from 'next-intl';
 
export default function Index() {
  const t = useTranslations('Index');
  return <h1>{t('title')}</h1>;
}

+ layout.tsxμ—μ„œ generateStaticParams() μ‚¬μš©ν•˜κΈ° (선택 사항)

μ €λŠ” μ•„λž˜μ˜ μ½”λ“œλ₯Ό μž‘μ„±ν•΄ static rendering이 되고(λΉŒλ“œ νƒ€μž„μ— 미리 생성 κ°€λŠ₯), μ •ν•΄μ§„ locale κ°’μœΌλ‘œλ§Œ /[locale]에 μ ‘κ·Όν•  수 μžˆλ„λ‘ μ„€μ •ν–ˆμŠ΅λ‹ˆλ‹€.

// app/[locale]/layout.tsx

export const dynamicParams = false;
export function generateStaticParams() {
    return locales.map((locale) => ({ locale }));
}

결과물 ✨

잘 λ˜λŠ”μ§€ μ‹œν—˜ν•΄ 보기 μœ„ν•΄ page.tsxλ₯Ό λ‹€μŒκ³Ό 같이 μž‘μ„±ν•΄ λ΄€μŠ΅λ‹ˆλ‹€.

// app/[locale]/page.tsx

import { useTranslations } from 'next-intl';

export default function Home() {
    const t = useTranslations('Home');
    return (
        <>
            <h1 className="mt-20 w-72 mx-auto">{t('title')}</h1>
            <h2 className="w-72 mx-auto">{t('subtitle')}</h2>
        </>
    );
}

μ•„λž˜μ˜ μ‹œμ—° μ˜μƒμ„ λ³΄μ‹œλ©΄, / μ£Όμ†Œλ‘œ μ ‘κ·Όν–ˆμ„ λ•Œ λ°”λ‘œ μ‚¬μš©μžμ˜ λΈŒλΌμš°μ € μ„€μ • μ–Έμ–΄λ‘œ (/en) redirect 되고, 그에 맞게 ν…μŠ€νŠΈλ„ 잘 λ Œλ”λ§ λ˜λŠ” 것을 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€. /ko둜 μ ‘κ·Όν•˜λ©΄ ν•œκ΅­μ–΄λ‘œ 잘 λ³€ν™˜λ˜κ³ , 이 μƒνƒœμ—μ„œ λ‹€μ‹œ /둜 λ“€μ–΄κ°€λ©΄ λ§ˆμ§€λ§‰μœΌλ‘œ μ„€μ •ν–ˆλ˜ μ–Έμ–΄λ₯Ό κΈ°μ–΅ν•΄μ„œ /ko둜 redirect λ˜λŠ” 것을 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.


더 λ‚˜μ•„κ°€κΈ° πŸš€

μœ„μ˜ λ‚΄μš©λ§Œ λ”°λΌν•˜μ…”λ„ useTranslationsλ₯Ό μ‚¬μš©ν•΄ μ–΄λ””μ„œλ“  json νŒŒμΌμ— μž‘μ„± 된 ν…μŠ€νŠΈλ₯Ό 언어에 맞게 뢈러올 수 μžˆμŠ΅λ‹ˆλ‹€.
ν•˜μ§€λ§Œ 더 λ‹€μ–‘ν•˜κ²Œ μ‚¬μš©ν•˜κ³  μ‹ΆμœΌμ‹œλ‹€λ©΄ μ•„λž˜ λ‚΄μš©μ„ μ°Έκ³ ν•΄ μ£Όμ‹œκΈ° λ°”λžλ‹ˆλ‹€.

Metadata에 λ‹€κ΅­μ–΄ μ§€μ›ν•˜κΈ°

메타데이터도 언어에 따라 λ‹€λ₯΄κ²Œ 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.
🌐 Next.js Metadata에 λ‹€κ΅­μ–΄ μ§€μ›ν•˜κΈ° (next-intl)

ICU messages λ‹€μ–‘ν•˜κ²Œ μ‚¬μš©ν•˜κΈ°

λ‹¨μˆœνžˆ λ©”μ„Έμ§€λ₯Ό κ·ΈλŒ€λ‘œ λΆˆλŸ¬μ˜€λŠ” t('title') 외에도 λ‹€μ–‘ν•œ μ‚¬μš© 방법이 μžˆμŠ΅λ‹ˆλ‹€.
🌐 next-intl의 ICU messages μ‚¬μš©ν•˜κΈ°

profile
λΉ λ₯΄κ²Œ λ°œμ „ν•˜λŠ” ν”„λ‘ νŠΈμ—”λ“œ 개발자

0개의 λŒ“κΈ€