Next.js 15 + i18next SSR(i18n) 적용 (feat. Hydration failed 예방)

조민혜·2025년 10월 21일
0

React.js

목록 보기
14/14

💬 문제 상황

Next.js 15(App Router)에서
react-i18next를 단순히 클라이언트에서만 초기화하면
아래와 같은 Hydration mismatch 오류가 발생합니다.

Hydration failed because the server rendered text didn't match the client.

서버는 한국어로 HTML을 렌더하고,
클라이언트는 영어로 다시 렌더하기 때문이에요.

즉,
서버와 클라이언트의 언어 상태가 불일치하는 게 원인입니다.

🎯 해결방법

SSR에서도 i18n을 적용해서,
서버와 클라이언트 모두 같은 언어로 HTML을 렌더합니다.

✅ 서버에서도 번역된 HTML 생성
✅ 클라이언트에서도 같은 인스턴스로 hydration
✅ Hydration mismatch 완전 제거

🗂️ 디렉토리 구조

src/
 └ lib/
    └ locales/
       ├ i18n.server.ts
       ├ i18n.client.ts
       ├ resources.ts
       ├ en-US/common.json
       ├ ko-KR/common.json
       └ ...
 └ app/
    └ i18n/
       └ I18nProvider.tsx

1️⃣ resources.ts

import koKR from "./ko-KR/common.json";
import enUS from "./en-US/common.json";
import jaJP from "./ja-JP/common.json";

const resources = {
  "ko-KR": { common: koKR },
  "en-US": { common: enUS },
  "ja-JP": { common: jaJP },
};

export default resources;

2️⃣ i18n.server.ts

서버용 i18n 초기화 코드입니다.
SSR 시점에서 locale별 인스턴스를 생성합니다.

import i18next, { Resource } from "i18next";
import resources from "./resources";

export async function createI18nServerInstance(locale: string) {
  const instance = i18next.createInstance();

  await instance.init({
    resources: resources as Resource,
    lng: locale,
    fallbackLng: "ko-KR",
    defaultNS: "common",
    interpolation: { escapeValue: false },
  });

  return instance;
}

3️⃣ i18n.client.ts

클라이언트 전용 초기화 코드입니다.
react-i18next가 실제로 동작하는 부분이에요.

"use client";

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import resources from "./resources";

i18n
  .use(initReactI18next)
  .init({
    resources,
    lng: "ko-KR",
    fallbackLng: "ko-KR",
    defaultNS: "common",
    interpolation: { escapeValue: false },
    react: { useSuspense: false },
  });

export default i18n;

4️⃣ I18nProvider.tsx

서버에서 전달된 locale을 클라이언트 i18n과 동기화하는 컴포넌트입니다.

즉,
서버 → 클라이언트 언어 브리지 역할을 합니다.

"use client";

import { I18nextProvider } from "react-i18next";
import React from "react";
import i18n from "@/lib/locales/i18n.client";

export default function I18nProvider({
  children,
  locale,
}: {
  children: React.ReactNode;
  locale: string;
}) {
  if (i18n.language !== locale) {
    i18n.changeLanguage(locale);
  }

  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}

5️⃣ layout.tsx (/app 하위 ssr 파일)

서버에서 locale 쿠키를 읽고,
서버 i18n 인스턴스를 만든 뒤 Provider로 전달합니다.

import "./globals.scss";
import { cookies } from "next/headers";
import { COOKIE_KEY_USER_LANG } from "@/lib/utils/config";
import { createI18nServerInstance } from "@/lib/locales/i18n.server";
import I18nProvider from "@/app/i18n/I18nProvider";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const ck = await cookies();
  const locale = ck.get(COOKIE_KEY_USER_LANG)?.value || "ko-KR";
  const i18nServer = await createI18nServerInstance(locale);

  return (
    <html data-country={country}>
      <body>
        <I18nProvider locale={i18nServer.language}>
          {children}
        </I18nProvider>
      </body>
    </html>
  );
}

6️⃣ 클라이언트 페이지 예시

"use client";
import { useTranslation } from "react-i18next";

export default function PageClient() {
  const { t } = useTranslation("common");

  return (
    <div>
      <h2>{t("doneText")}</h2>
    </div>
  );
}

🧩 핵심 요약

i18n.server.ts → 서버용 i18n 초기화
i18n.client.ts → 클라이언트용 react-i18next
I18nProvider → 서버 locale을 클라이언트에 연결
RootLayout → 쿠키 기반 locale 전달

🧭 정리

Next.js 15(App Router)에서 i18next를 완전 SSR 방식으로 적용하면,
서버가 locale에 맞게 번역된 HTML을 미리 렌더하고,
클라이언트는 동일한 언어로 hydration을 이어받게 됩니다.

이렇게 구조를 나누고 I18nProvider로 연결해주면,
서버와 클라이언트의 언어가 정확히 일치해서
Hydration mismatch 오류가 완전히 해결됩니다.

profile
Currently diving deeper into React.js development.

0개의 댓글