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를 찾아 해당하는 번역된 문자열을 렌더링합니다. 로케일이 변경되고 로더가 다른 메시지로 다시 실행되면, 모든 컴포넌트는 코드 변경 없이 자동으로 새 번역을 표시합니다.