TanStack Start v1에서 파일에서 번역을 로드하는 방법
코드에서 번역 가능한 콘텐츠 분리하기
문제
사용자에게 표시되는 문자열을 컴포넌트에 직접 하드코딩하면 콘텐츠와 코드 간에 강한 결합이 생깁니다. 문자열이 변경되거나 새 언어가 추가될 때마다 개발자는 소스 파일을 찾아 수정한 다음 애플리케이션을 다시 배포해야 합니다. 이 접근 방식은 번역 워크플로우가 엔지니어링 주기에 의존하게 만들고 비기술적 팀원이 텍스트를 업데이트하는 것을 방해합니다. 지원되는 언어 수가 증가함에 따라 올바른 문자열을 선택하기 위한 조건부 로직이 복잡해지고 오류가 발생하기 쉬워집니다. 그 결과 반복 속도가 느려지고, 유지 관리 비용이 증가하며, 다른 곳에 있어야 할 번역 가능한 콘텐츠로 코드베이스가 복잡해집니다.
해결책
모든 번역 가능한 문자열을 로케일별로 하나씩 외부 JSON 파일에 저장하여 애플리케이션 코드에서 분리합니다. 각 메시지에 대해 안정적인 키를 정의하고 리터럴 텍스트 대신 컴포넌트에서 해당 키를 참조합니다. 런타임에 사용자의 로케일에 따라 적절한 번역 파일을 로드하고 이러한 메시지를 react-intl의 IntlProvider에 제공합니다. 이렇게 하면 콘텐츠와 코드가 분리됩니다: 번역가는 JSON 파일로 직접 작업할 수 있고, 텍스트 변경에는 코드 수정이 필요하지 않으며, 새 언어를 추가하는 것은 컴포넌트를 건드리지 않고 새 파일을 추가하는 것을 의미합니다.
단계
1. 각 로케일에 대한 번역 파일 생성
전용 디렉토리에 JSON 파일로 번역을 구성하고, 로케일별로 하나의 파일에 모든 메시지에 대한 키-값 쌍을 포함시킵니다.
{
"welcome": "Welcome back",
"greeting": "Hello, {name}",
"itemCount": "{count, plural, =0 {No items} one {One item} other {# items}}"
}
이것을 app/translations/en.json으로 저장합니다. app/translations/es.json 및 app/translations/fr.json과 같은 다른 로케일에 대한 병렬 파일을 생성하되, 동일한 키와 번역된 값을 사용합니다.
2. 서버 함수를 사용하여 번역 로드하기
서버 함수를 사용하여 요청된 로케일에 기반하여 디스크에서 번역 파일을 읽어옵니다. 이를 통해 SSR 중에는 서버 측에서 번역이 로드되고 탐색 중에는 클라이언트에서 가져올 수 있습니다.
import { createServerFn } from "@tanstack/react-start";
import * as fs from "node:fs";
export const getMessages = createServerFn({ method: "GET" }).handler(
async ({ request }) => {
const url = new URL(request.url);
const locale = url.searchParams.get("locale") || "en";
const filePath = `app/translations/${locale}.json`;
const content = await fs.promises.readFile(filePath, "utf-8");
return JSON.parse(content);
},
);
이 함수는 주어진 로케일에 대한 JSON 파일을 읽고 파싱된 메시지 객체를 반환합니다. 서버에서만 실행되어 파일 시스템 접근을 안전하게 유지합니다.
3. 사용자의 로케일을 결정하는 헬퍼 만들기
요청에서 로케일을 추출하거나 기본값으로 대체하는 작은 유틸리티를 정의하여 라우트 전반에서 재사용할 수 있게 합니다.
export function getLocaleFromRequest(request: Request): string {
const url = new URL(request.url);
const localeParam = url.searchParams.get("locale");
if (localeParam) return localeParam;
const acceptLanguage = request.headers.get("accept-language");
if (acceptLanguage) {
const match = acceptLanguage.split(",")[0].split("-")[0];
return match || "en";
}
return "en";
}
이 함수는 먼저 쿼리 매개변수를 확인한 다음 Accept-Language 헤더를 확인하고 기본값으로 영어를 사용합니다. 로케일 감지를 위한 단일 진실 소스를 제공합니다.
4. 라우트 로더에서 메시지 로드하기
라우트 로더를 사용하여 렌더링 전에 현재 로케일에 대한 메시지를 가져와 컴포넌트 트리에서 사용할 수 있게 합니다.
import { createFileRoute } from "@tanstack/react-router";
import { getMessages, getLocaleFromRequest } from "../lib/i18n";
export const Route = createFileRoute("/")({
loader: async ({ context }) => {
const locale = getLocaleFromRequest(context.request);
const messages = await getMessages({ data: { locale } });
return { locale, messages };
},
component: HomePage,
});
function HomePage() {
const { locale, messages } = Route.useLoaderData();
return (
<div>
<p>{messages.welcome}</p>
</div>
);
}
로더는 서버 함수를 호출하여 메시지를 검색하고, 컴포넌트는 useLoaderData를 통해 이에 접근합니다. 이 패턴은 SSR과 클라이언트 측 탐색 모두에서 작동합니다.
5. react-intl에 메시지 제공하기
로드된 로케일과 메시지를 전달하여 IntlProvider로 컴포넌트 트리를 감싸면 모든 하위 컴포넌트가 번역에 접근할 수 있습니다.
import { IntlProvider } from "react-intl";
function HomePage() {
const { locale, messages } = Route.useLoaderData();
return (
<IntlProvider locale={locale} messages={messages}>
<AppContent />
</IntlProvider>
);
}
function AppContent() {
return (
<div>
<FormattedMessage id="welcome" />
</div>
);
}
IntlProvider는 로케일과 메시지를 모든 react-intl 컴포넌트와 훅에서 사용할 수 있게 합니다. 이제 컴포넌트는 FormattedMessage 또는 useIntl을 사용하여 키로 메시지를 참조할 수 있으며, 로드된 로케일에 기반하여 올바른 번역이 렌더링됩니다.
6. 컴포넌트에서 키로 메시지 참조하기
react-intl의 FormattedMessage 컴포넌트나 useIntl 훅을 사용하여 JSON 파일에 정의된 키를 참조하여 번역된 문자열을 표시합니다.
import { FormattedMessage, useIntl } from "react-intl";
function UserGreeting({ name }: { name: string }) {
const intl = useIntl();
const title = intl.formatMessage({ id: "greeting" }, { name });
return (
<div>
<h1 title={title}>
<FormattedMessage id="greeting" values={{ name }} />
</h1>
<p>
<FormattedMessage id="itemCount" values={{ count: 5 }} />
</p>
</div>
);
}
FormattedMessage는 번역된 문자열을 인라인으로 렌더링하고, useIntl().formatMessage는 속성이나 JavaScript 로직에서 사용할 수 있는 문자열을 반환합니다. 둘 다 보간을 위한 values를 받으며 복수형과 포맷팅을 위한 ICU 메시지 구문을 지원합니다.