React Router v7에서 파일로부터 번역을 로드하는 방법

코드에서 번역 가능한 콘텐츠 분리하기

문제

사용자 대면 문자열을 컴포넌트에 직접 하드코딩하면 콘텐츠와 코드 간에 강한 결합이 생성됩니다. 새로운 언어를 추가할 때마다 개발자가 구현 파일을 수정해야 하며, 조건부 로직이 확장되고 복잡성이 증가합니다. 문구가 변경될 때 사소한 표현 조정조차도 코드 배포가 필요합니다. 이러한 접근 방식은 번역 워크플로를 엔지니어링 주기에 종속시키고 비기술 팀원이 콘텐츠를 독립적으로 관리하는 것을 방해합니다.

애플리케이션이 성장함에 따라 흩어진 문자열 리터럴은 추적하고 유지 관리하기 어려워집니다. 코드베이스 전체에서 특정 문구의 모든 발생을 찾는 것은 오류가 발생하기 쉬우며, 중앙 집중식 정보 소스 없이 유사한 메시지 간의 일관성을 보장하는 것은 거의 불가능합니다.

해결책

모든 번역 가능한 문자열을 언어별로 구성된 외부 JSON 파일로 추출하되, 로케일당 하나의 파일을 사용합니다. 컴포넌트의 하드코딩된 문자열을 이러한 파일의 항목을 참조하는 메시지 식별자로 교체합니다. 런타임에 애플리케이션은 사용자의 로케일에 따라 적절한 번역 파일을 로드하고 해당 메시지를 국제화 라이브러리에 제공하며, 라이브러리는 식별자를 번역된 값으로 해석합니다.

이러한 분리를 통해 번역가는 코드를 건드리지 않고 JSON 파일로 직접 작업할 수 있으며, 간단한 파일 변경을 통해 콘텐츠를 업데이트할 수 있고, 각 언어의 문자열에 대한 단일 정보 소스를 제공합니다.

단계

1. 각 로케일에 대한 번역 파일 생성

전용 디렉토리에 번역 파일을 구성하되, 언어당 하나의 JSON 파일을 사용합니다. 각 파일을 메시지 식별자를 번역된 문자열에 매핑하는 플랫 객체로 구조화합니다.

{
"welcome.title": "Welcome back",
"welcome.subtitle": "Continue where you left off",
"nav.home": "Home",
"nav.about": "About",
"nav.contact": "Contact"
}

이것을 영어용 app/translations/en.json로 저장한 다음, app/translations/es.jsonapp/translations/fr.json와 같은 병렬 파일을 다른 언어의 번역과 함께 생성하세요. 모든 파일에서 일관된 키를 사용하여 동일한 식별자가 각 로케일에서 적절한 번역으로 확인되도록 하세요.

2. 라우트 로더에서 번역 로드

라우트 로더를 사용하여 렌더링 전에 현재 로케일의 번역 파일을 가져옵니다. 이를 통해 컴포넌트가 마운트될 때 메시지를 사용할 수 있습니다.

import type { Route } from "./+types/root";
import enMessages from "./translations/en.json";
import esMessages from "./translations/es.json";
import frMessages from "./translations/fr.json";

const messages: Record<string, Record<string, string>> = {
  en: enMessages,
  es: esMessages,
  fr: frMessages,
};

export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const locale = url.searchParams.get("locale") || "en";

  return {
    locale,
    messages: messages[locale] || messages.en,
  };
}

로더는 URL 쿼리 매개변수에서 로케일을 읽고 로케일과 해당 메시지를 모두 반환합니다. 컴포넌트는 loaderData를 통해 이 데이터에 액세스하여 국제화 프로바이더를 구성할 수 있습니다.

3. 로드된 메시지로 IntlProvider 구성

react-intl의 IntlProvider로 애플리케이션을 래핑하고, 로더 데이터의 로케일과 메시지를 전달하세요.

import { IntlProvider } from "react-intl";
import { Outlet } from "react-router";
import type { Route } from "./+types/root";

export default function Root({ loaderData }: Route.ComponentProps) {
  return (
    <IntlProvider locale={loaderData.locale} messages={loaderData.messages}>
      <html lang={loaderData.locale}>
        <head>
          <meta charSet="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
        </head>
        <body>
          <Outlet />
        </body>
      </html>
    </IntlProvider>
  );
}

IntlProvider는 React 컨텍스트를 통해 모든 하위 컴포넌트에서 로케일과 메시지를 사용할 수 있도록 합니다. 하위 라우트는 Outlet를 통해 렌더링되며 번역 데이터에 대한 액세스를 상속받습니다.

4. 컴포넌트에서 식별자로 메시지 참조

하드코딩된 문자열을 번역 파일의 메시지 식별자를 참조하는 FormattedMessage 컴포넌트로 교체하세요.

import { FormattedMessage } from "react-intl";

export default function Welcome() {
  return (
    <div>
      <h1>
        <FormattedMessage id="welcome.title" />
      </h1>
      <p>
        <FormattedMessage id="welcome.subtitle" />
      </p>
      <nav>
        <a href="/">
          <FormattedMessage id="nav.home" />
        </a>
        <a href="/about">
          <FormattedMessage id="nav.about" />
        </a>
        <a href="/contact">
          <FormattedMessage id="nav.contact" />
        </a>
      </nav>
    </div>
  );
}

FormattedMessage 컴포넌트는 IntlProvider가 제공하는 메시지 객체에서 해당 id를 조회하고 해당하는 번역된 문자열을 렌더링합니다. 로케일이 변경되고 로더가 다른 메시지로 다시 실행되면, 모든 컴포넌트는 코드 변경 없이 자동으로 새로운 번역을 표시합니다.