관리 메뉴

Unwound Developer

Next.js 14(App Router) + React i18n(next-intl)을 통한 다국어 처리 본문

Web/Next.js

Next.js 14(App Router) + React i18n(next-intl)을 통한 다국어 처리

unwind 2024. 12. 10. 21:45
반응형

Next.js를 활용한 SSR 다국어 처리(i18n)

1. 유저의 locale 감지 방법

애플같은 사이트를 접속하다보면, 내 국가에 따라 언어가 자동으로 설정되는 것을 흔히 볼 수 있습니다.

보통 url에서 apple.ko 혹은 apple/ko 이런 식으로 국가 코드에 따라 언어가 설정 됩니다.

그 다국어 처리를 Next.js SSR 환경에서 구현하는 것을 알아보겠습니다.

클라이언트 측 (navigator.language)

  • 브라우저에서 사용자가 어떤 언어를 쓰고 있는지 알 수 있음.
  • navigator.language는 브라우저의 UI 언어 정보를 저장합니다.
    브라우저 설정에 따라 달라짐.
const locale = window.navigator.language;

서버 측 (Accept-Language)

  • Next.js는 서버 사이드 렌더링을 지원하므로, HTTP 헤더의 Accept-Language 값을 이용해 유저의 언어를 감지합니다.
  • Accept-Language도 브라우저 설정을 기반으로 함.

주의
Accept-Language는 핑거프린팅(추적) 가능성이 있기 때문에 직접 변경하지 않는 게 좋습니다.


2. Next.js의 Internationalized Routing

서버에서 하는 일

  1. Accept-Language 값을 이용해 유저 언어 감지합니다.
  2. 유저의 언어에 맞는 URL로 리다이렉트합니다.
  3. <html> 태그에 lang 속성 추가합니다.

라우팅 방식

(1) Sub-path Routing

  • 언어를 URL의 서브패스로 구분.
  • 예시:
    • 한국어 사이트 → interneteye.co.kr/payment
    • 영어 사이트 → interneteye.co.kr/en-us/payment

(2) Domain Routing

  • 도메인 자체를 언어에 따라 다르게 설정.
  • 예시:
    • 글로벌 → example.com
    • 프랑스어 → example.fr


Sub-path Routing이 더 간단하고 유지보수하기 쉽습니다.
Domain Routing은 설정해야 할 게 많아 규모가 큰 사이트가 아니면 추천하지 않습니다..


Next.js가 i18n 라우팅을 사용하는 이유

  • 인앱 상태값이나 HTTP 헤더를 직접 수정하는 방법은 권장되지 않음.
  • Next.js는 정적 페이지 기반 i18n을 지원하며, URL을 기반으로 언어를 구분.

예시:
example.com/ko
example.com/en-US

설정 방법 (Next.js 10 이상)

  1. next.config.js 파일에 i18n 설정 추가.
  2. npm install next-intl로 관련 패키지 설치.

디렉토리 구조

app router 기준으로 app 디렉토리 하위에 [locale] 이라는 디렉토리를 생성해야합니다.

반드시 해당 locale 디렉토리를 생성해야 국가 코드로 라우팅 됩니다. 예) test.com/ko test.com/en-US

// i18n.ts
import { notFound } from 'next/navigation';
import { getRequestConfig } from 'next-intl/server';

const locales = ['en', 'ko'];

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

    // 위치 확인
    return {
        messages: (await import(`@/locales/${locale}/translate.json`)).default 
    };
});

3. NEXT_LOCALE 쿠키 저장 시점

  • URL, defaultLocale, 또는 브라우저 언어에 따라 언어 결정 후.
  • 미들웨어에서 리다이렉션이 발생한 후.
  • 사용자가 언어를 변경했을 때.
  • 초기 요청에서 Next.js가 언어를 판단했을 때.

4. 문제점 및 해결 방법

문제: 미들웨어에서 Accept-Language 값 읽기

  • Negotiator.language(): 클라이언트가 요청한 accept-language 헤더를 기준으로 서버에서 지원하는 언어를 매칭.
  • 기본 동작: 첫 번째 매칭된 언어를 반환.
    하지만 예상과 다른 언어가 반환되는 경우 발생.

예시

SUPPORTED_LANGUAGES = ['en', 'ko'];
accept-language = 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7';
  • 매칭 과정
    1. ko-KRSUPPORTED_LANGUAGES에 없음 → 무시.
    2. ko → 매칭 성공 → 반환.
    3. 하지만 간혹 en이 반환되는 경우 발생.

해결 방법: 매칭 로직 수정

const priorities = negotiator.languages();
const matchedLanguage = priorities.find((lang) =>
    SUPPORTED_LANGUAGES.includes(lang)
);
const locale = matchedLanguage || DEFAULT_LANGUAGE;

console.log('Matched language:', locale);
  • 클라이언트의 요청 언어를 순서대로 확인하고 직접 매칭.
  • 매칭된 언어가 없으면 DEFAULT_LANGUAGE 반환.

5. Next.js Client Component에서 useRouter 사용 불가

문제

  • Next.js 13 이후, 클라이언트 컴포넌트는 use client를 사용해야 함.
  • 그러나, use client를 사용하면 useRouter 사용 시 에러 발생:
    "NextRouter was not mounted."

대안

  1. useNavigation 사용
    • 하지만 queriesdynamicPaths 같은 데이터는 포함되지 않습니다.
  2. usePathname 추가 사용
    • query, dynamicPaths가 필요할 경우 보완적으로 활용합니다.

// middleware.ts
import createMiddleware from 'next-intl/middleware';
import Negotiator from 'negotiator';
import { NextResponse } from 'next/server';

// 서버에서 지원하는 언어와 기본 언어 설정
const SUPPORTED_LANGUAGES = ['ko', 'en'];
const DEFAULT_LANGUAGE = 'ko';

// 기본 next-intl 미들웨어 생성
const intlMiddleware = createMiddleware({
    locales: SUPPORTED_LANGUAGES,
    defaultLocale: DEFAULT_LANGUAGE,
});

// Custom Negotiator를 추가한 미들웨어
export default function middleware(req : any) {
    const { cookies } = req; // 요청에서 쿠키 읽기
    const { pathname } = req.nextUrl;
    const nextLocale = cookies.get('NEXT_LOCALE') // NEXT_LOCALE 쿠키 확인

    // 이미 언어 코드가 경로에 포함되어 있다면 추가 리디렉션 방지하기 위한 변수
    const isLanguagePath = SUPPORTED_LANGUAGES.some((lang) => pathname.startsWith(`/${lang}`));

    if (nextLocale && SUPPORTED_LANGUAGES.includes(nextLocale.value) && !isLanguagePath) { // 쿠키에 유효한 언어 값이 있으면 해당 값 사용        
        const url = req.nextUrl.clone()
        url.pathname = `/${nextLocale.value}`
        return NextResponse.redirect(new URL(`/${nextLocale.value}`,req.url))
    }

    // URL에 로케일이 없으면 리다이렉트    
    if (!isLanguagePath) {
        // 언어 코드가 포함되어 있다면, 쿠키에 값이 없으면 Accept-Language 헤더로 언어 결정
        const negotiator = new Negotiator(req); // 클라이언트의 Request 읽어옴
        const priorities = negotiator.languages(); // 클라이언트의 Request에서 Accept_Language 값 읽어옴
        const matchedLanguage = priorities.find((lang) =>
            SUPPORTED_LANGUAGES.includes(lang)
        ); // 가장 우선순위로 있는 언어가 SUPPORTED_LANGUAGE에 있는지 우선순위대로 find
        console.log("matchedLanguage:",matchedLanguage);
        const locale = matchedLanguage || DEFAULT_LANGUAGE; // matchedLanguage가 false나 undefined같은 값이라면 DEFAULT_LANGUAGE로 세팅

        const url = req.nextUrl.clone();
        url.pathname = `/${locale}${pathname}`;
        return NextResponse.redirect(url);
    }

    // Default: next-intl 미들웨어 실행
    return intlMiddleware(req);
}

// next-intl 미들웨어 설정
export const config = {
    matcher: ['/((?!api|_next|.*\\..*).*)'], // i18n 적용 경로 설정
};

참고 사항

  • Next.js에서 i18n을 제대로 활용하려면 서버와 클라이언트 모두 협력해야 함.
  • 설정이 복잡할 수 있으니 간단한 구현부터 시작하는 게 좋음!
반응형