next-intl 언어 감지 설정 방법
next-intl을 활용하여 사용자의 브라우저 언어 설정에 따라 자동으로 언어를 감지하고 전환하는 방법을 알아봅니다.
next-intl 언어 감지 설정 방법
Introduction
다국어 웹사이트에서는 사용자의 언어 설정에 따라 콘텐츠를 자동으로 전환해야 합니다. next-intl은 Next.js에서 국제화(i18n)를 쉽게 구현할 수 있는 라이브러리입니다. 이 글에서는 언어 감지와 동적 전환 설정을 다룹니다.
Environment
# 프로젝트 구조
my-app/
├── app/
│ ├── [locale]/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── about/
│ │ └── page.tsx
│ └── layout.tsx
├── messages/
│ ├── ko.json
│ ├── en.json
│ └── ja.json
├── i18n/
│ ├── config.ts
│ ├── request.ts
│ └── routing.ts
├── middleware.ts
└── package.json
# 기술 스택
Next.js: 14.2.5
next-intl: 3.15.0
TypeScript: 5.3# next-intl 설치
npm install next-intlProblem
언어 감지 없이는 모든 사용자가 기본 언어로 콘텐츠를 봅니다:
// ❌ 언어 감지 미적용
export default function HomePage() {
return (
안녕하세요
{/* 모든 사용자에게 한국어 표시 */}
);
}Analysis
언어 감지 과정:
- 쿠키 확인: 이전에 선택한 언어가 있는지 확인
- Accept-Language 헤더: 브라우저 설정 언어 확인
- 기본 언어 폴백: 감지 실패 시 기본 언어 사용
- URL 경로:
/ko/about과 같은 경로로 언어 결정
Solution
1. i18n 설정 파일
// i18n/config.ts
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';
export const locales = ['ko', 'en', 'ja'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'ko';
export const routing = defineRouting({
locales,
defaultLocale,
});
// 네비게이션 헬퍼
export const { Link, redirect, usePathname, useRouter } =
createNavigation(routing);2. 미들웨어 설정
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/config';
export default createMiddleware(routing);
export const config = {
// Next.js 미들웨어가 처리할 경로
matcher: [
// 모든 경로 중에서
'/((?!api|_next|_vercel|.*\\..*).*)',
],
};3. 언어 감지 쿠키 설정
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const COOKIE_NAME = 'NEXT_LOCALE';
export function middleware(request: NextRequest) {
// 1. 쿠키에서 언어 확인
const cookieLocale = request.cookies.get(COOKIE_NAME)?.value;
if (cookieLocale && routing.locales.includes(cookieLocale as any)) {
// 쿠키에 유효한 언어가 있으면 해당 언어로 리다이렉션
const pathname = request.nextUrl.pathname;
const pathnameHasLocale = routing.locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (!pathnameHasLocale) {
return NextResponse.redirect(
new URL(`/${cookieLocale}${pathname}`, request.url)
);
}
}
// 2. Accept-Language 헤더에서 언어 감지
const acceptLanguage = request.headers.get('Accept-Language');
if (acceptLanguage) {
const detectedLocale = detectLocaleFromHeader(acceptLanguage);
// 감지된 언어로 쿠키 설정 및 리다이렉션
const response = NextResponse.next();
response.cookies.set(COOKIE_NAME, detectedLocale, {
maxAge: 365 * 24 * 60 * 60, // 1년
path: '/',
});
return response;
}
return NextResponse.next();
}
function detectLocaleFromHeader(acceptLanguage: string): string {
// Accept-Language 헤더 파싱
const languages = acceptLanguage
.split(',')
.map(lang => {
const [locale, q] = lang.trim().split(';q=');
return {
locale: locale.split('-')[0], // 'ko-KR' -> 'ko'
quality: q ? parseFloat(q) : 1,
};
})
.sort((a, b) => b.quality - a.quality);
// 지원하는 언어 중 첫 번째 매칭되는 언어 반환
for (const { locale } of languages) {
if (routing.locales.includes(locale as any)) {
return locale;
}
}
return routing.defaultLocale;
}4. 요청 설정
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
// 런타임에 감지된 로케일 가져오기
let locale = await requestLocale;
// 지원하지 않는 로케일인 경우 기본값 사용
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});5. 레이아웃에서 언어 적용
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/config';
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: string };
}) {
// 유효한 로케일인지 확인
if (!routing.locales.includes(locale as any)) {
notFound();
}
const messages = await getMessages();
return (
{children}
);
}6. 언어 전환 컴포넌트
// components/LanguageSwitcher.tsx
'use client';
import { useLocale, useTranslations } from 'next-intl';
import { useRouter, usePathname } from '@/i18n/config';
import { routing } from '@/i18n/config';
export function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const t = useTranslations('LocaleSwitcher');
const handleLanguageChange = (newLocale: string) => {
router.replace(
{ pathname },
{ locale: newLocale }
);
};
return (
{t('label')}:
{routing.locales.map((loc) => (
))}
);
}7. 언어별 메시지 파일
// messages/ko.json
{
"Common": {
"hello": "안녕하세요",
"welcome": "환영합니다",
"language": "언어"
},
"LocaleSwitcher": {
"label": "언어 선택"
},
"HomePage": {
"title": "홈페이지",
"description": "다국어 웹사이트에 오신 것을 환영합니다"
}
}// messages/en.json
{
"Common": {
"hello": "Hello",
"welcome": "Welcome",
"language": "Language"
},
"LocaleSwitcher": {
"label": "Language"
},
"HomePage": {
"title": "Home",
"description": "Welcome to our multilingual website"
}
}Lessons Learned
- 쿠키 우선: 사용자가 선택한 언어는 쿠키에 저장하여 유지
- Accept-Language 활용: 브라우저 설정에 따른 자동 감지
- SEO 고려: 각 언어별 별도 URL로 검색 엔진 최적화
- 사용자 경험: 언어 전환 시 현재 페이지 유지
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.