deep-dive2025-06-08·11 min·40/348

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-intl

Problem

언어 감지 없이는 모든 사용자가 기본 언어로 콘텐츠를 봅니다:

// ❌ 언어 감지 미적용
export default function HomePage() {
  return (
    

안녕하세요

{/* 모든 사용자에게 한국어 표시 */}
); }

Analysis

언어 감지 과정:

  1. 쿠키 확인: 이전에 선택한 언어가 있는지 확인
  2. Accept-Language 헤더: 브라우저 설정 언어 확인
  3. 기본 언어 폴백: 감지 실패 시 기본 언어 사용
  4. 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

  1. 쿠키 우선: 사용자가 선택한 언어는 쿠키에 저장하여 유지
  2. Accept-Language 활용: 브라우저 설정에 따른 자동 감지
  3. SEO 고려: 각 언어별 별도 URL로 검색 엔진 최적화
  4. 사용자 경험: 언어 전환 시 현재 페이지 유지

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.