i18next를 활용한 다국어 지원: 번역 파일을 효율적으로 로드하고 관리하는 방법 (2)

치와와견주·2024년 9월 11일
0

프로젝트에서 i18next를 사용하여 다국어 지원을 구현하기로 결정한 후, 번역 파일을 어떻게 효율적으로 관리하고 로드할지에 대한 고민이 시작되었습니다. 다국어 지원을 위해 단순히 번역 파일을 작성하는 것만으로는 충분하지 않았고, 각 언어 파일을 어떻게 애플리케이션에 불러오고, 관리할 것인지에 대한 전략이 필요했습니다. 여러 가지 방법을 검토한 끝에, 최종적으로 i18next-http-backend 라이브러리를 사용하기로 결정했으며, 그 과정에서의 고민과 결정을 공유하고자 합니다.

1. 고민했던 번역 파일 로드 방법들

다양한 방법을 비교하면서, 각각의 접근 방식이 어떤 장점과 단점을 가지고 있는지 분석했습니다. 다음은 고려했던 주요 방법들입니다.

1-1. 번역 리소스를 코드에 직접 포함하기 (Inline Resources)

가장 먼저 떠오른 방법은 번역 리소스를 코드에 직접 포함하는 것이었습니다. 이 방식은 초기 설정이 매우 간단하고, 소규모 프로젝트에서는 빠르게 구현할 수 있다는 장점이 있었습니다. 그러나 번역 데이터가 많아지면 유지보수가 어렵고, 코드가 복잡해진다는 문제가 있었습니. 특히, 번역 파일 구조를 페이지별로 나누어 사용하기 어려워, 필요한 번역 리소스만 로드하는 것이 불가능해진다는 큰 단점이 있었습니다.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n
  .use(initReactI18next)
  .init({
    resources: {
      en: { translation: { hello: "Hello World" } },
      fr: { translation: { hello: "Bonjour le monde" } },
    },
    lng: 'en',
    fallbackLng: 'en',
  });

export default i18n;

1-2. 번역 파일을 직접 import하여 로드하기

두 번째로 검토한 방법은 번역 파일을 별도로 JSON 형식으로 관리하고, 이를 코드에서 직접 import하는 방식입니다. 이 방식은 번역 파일을 독립적으로 관리할 수 있어 코드의 가독성이 좋아지고, 쉽게 수정할 수 있다는 장점이 있었습니다. 하지만 프로젝트가 커지면서 많은 파일을 import할 때 초기 로딩 시간이 늘어날 수 있는 단점이 있었습니다.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import enTranslation from './locales/en/translation.json';
import frTranslation from './locales/fr/translation.json';

i18n
  .use(initReactI18next)
  .init({
    resources: {
      en: { translation: enTranslation },
      fr: { translation: frTranslation },
    },
    lng: 'en',
    fallbackLng: 'en',
  });

export default i18n;

1-3. custom backend 구현하기

번역 파일을 서버에서 직접 로드하기 위해 커스텀 백엔드를 구현하는 방법도 고려했습니다. i18next의 백엔드 플러그인 구조를 활용해 HTTP 요청을 직접 관리할 수 있었지만, 이를 위해 추가적인 구현이 필요했습니다. 직접 서버에서 번역 파일을 가져오도록 설정하면 유연하게 관리할 수 있는 장점이 있었지만, 커스텀 로직을 작성해야 해서 복잡성이 증가했습니다.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

const customBackend = {
  type: 'backend',
  read: function (language, namespace, callback) {
    fetch(`/locales/${language}/${namespace}.json`)
      .then((response) => response.json())
      .then((data) => callback(null, data))
      .catch((error) => callback(error, false));
  },
};

i18n
  .use(customBackend)
  .use(initReactI18next)
  .init({
    lng: 'en',
    fallbackLng: 'en',
  });

export default i18n;

1-4. 최종 결정: i18next-http-backend, i18next-resources-to-backend라이브러리 사용하기

위의 방법들을 비교한 결과, 최종적으로 i18next-http-backend 라이브러리를 사용하기로 결정했습니다. 이 라이브러리는 i18next의 공식 백엔드 플러그인으로, 서버에서 번역 파일을 동적으로 가져올 수 있게 해주며, 설정이 간단하면서도 안정적인 동작을 보장합니다. 사용하게 될 라이브러리에 대해서 간략히 설명하겠습니다.

i18next-chained-backend:
여러 백엔드 플러그인을 체인으로 연결하여 순차적으로 번역 리소스를 가져올 수 있도록 해주는 플러그인입니다. 이를 통해 번역 데이터를 효율적으로 관리하고 우선순위에 따라 번역 리소스를 로드할 수 있습니다.
i18next-http-backend:

서버에서 번역 파일을 동적으로 가져오는 역할을 합니다. XMLHttpRequest 또는 fetch API를 사용하여 서버와 통신하여 최신 번역 데이터를 유지할 수 있습니다. 이 방식은 새로운 번역 파일이 추가되거나 기존 번역이 수정될 때 유용합니다.
i18next-resources-to-backend:

클라이언트 내부에 포함된 번역 리소스를 먼저 로드하는 역할을 합니다. 이를 통해 초기 로딩 시간을 단축하고, 네트워크 요청을 최소화할 수 있습니다. 브라우저에서 번역 파일을 직접 로드하기 때문에 오프라인 상태에서도 번역 기능을 사용할 수 있는 장점이 있습니다.

pnpm add i18next-chained-backend i18next-http-backend i18next-resources-to-backend

useTranslation 커스텀 훅 생성

import {
  createInstance,
  FlatNamespace,
  i18n as I18nInstance,
  InitOptions,
  Resource,
  KeyPrefix as I18nKeyPrefix,
} from "i18next";
import i18nConfig, { Locale } from "./i18nConfig";
import { initReactI18next } from "react-i18next/initReactI18next";
import ChainedBackend from "i18next-chained-backend";
import HttpBackend from "i18next-http-backend";
import resourcesToBackend from "i18next-resources-to-backend";

function addBackendResourceLoader(i18nInstance: I18nInstance) {
  i18nInstance.use(ChainedBackend);
}

function createInitOptions({
  locale,
  namespaces,
  resources,
  options,
}: {
  locale: Locale;
  namespaces: FlatNamespace[];
  resources?: Resource;
  options?: InitOptions;
}): InitOptions {
  return {
    lng: locale,
    resources,
    fallbackLng: i18nConfig.defaultLocale,
    supportedLngs: i18nConfig.locales,
    defaultNS: namespaces[0],
    fallbackNS: namespaces[0],
    ns: namespaces.map((ns) => ns.toString()),
    preload: resources ? [] : i18nConfig.locales,
    backend: {
      backends: [
        HttpBackend,
        resourcesToBackend(
          (language: Locale, namespace: string) =>
            import(`@/app/i18n/locales/${language}/${namespace}.json`)
        ),
      ],
      backendOptions: [
        {
          load: (language: Locale, namespace: string) =>
            import(`@/app/i18n/locales/${language}/${namespace}.json`),
        },
      ],
    },
    ...options,
  };
}

interface UseTranslationsProps<
  KPrefix extends I18nKeyPrefix<string> = undefined
> {
  locale: Locale;
  namespaces: FlatNamespace[];
  i18nInstance?: I18nInstance;
  resources?: Resource;
  options?: InitOptions;
  keyPrefix?: KPrefix;
}
interface UseTranslationsResult {
  i18n: I18nInstance;
  resources: Resource;
  t: I18nInstance["t"];
}

export default async function useTranslation<
  KPrefix extends I18nKeyPrefix<string> = undefined
>({
  locale,
  namespaces,
  i18nInstance = createInstance(),
  resources,
  keyPrefix,
  options,
}: UseTranslationsProps<KPrefix>): Promise<UseTranslationsResult> {
  i18nInstance.use(initReactI18next);

  // resource가 없을경우, 백엔드에서 가져오도록 설정
  if (!resources) {
    addBackendResourceLoader(i18nInstance);
  }
  //i18n 초기화 옵션 설정
  const initOptions = createInitOptions({
    locale,
    namespaces,
    resources,
    options,
  });

  await i18nInstance.init(initOptions);

  return {
    i18n: i18nInstance,
    resources: i18nInstance.services.resourceStore.data,
    t: i18nInstance.getFixedT(locale, namespaces[0], keyPrefix),
  };
}
  • locale과 namespaces를 기반으로 i18next 인스턴스를 생성합니다.
  • 백엔드 설정을 통해 번역 파일을 서버 또는 클라이언트에서 적절히 로드합니다.
  • 초기화 옵션을 설정하여 i18next를 초기화하고, 번역 인스턴스와 번역 함수를 반환합니다.

서버 컴포넌트에서는 다음과 같이 작성하면 됩니다.

import useTranslation from "@/app/i18n";
import { Locale } from "@/app/i18n/i18nConfig";
interface FooterProps {
  locale: Locale;
}

const Footer = async ({ locale }: FooterProps) => {
  const { t } = await useTranslation({
    namespaces: ["common"],
    locale,
  });
  return (
    <footer className="bg-gray-800 text-white py-4">
      <div className="container mx-auto px-4">
        <p className="text-center">{t("footer")}</p>
      </div>
    </footer>
  );
};

export default Footer;

결론

이번 포스팅에서는 i18next를 활용하여 다국어 지원을 구현하는 과정에서 번역 파일을 효율적으로 관리하고 로드하는 다양한 방법을 고민하고, 최종적으로 i18next-http-backend와 i18next-resources-to-backend 라이브러리를 사용하기로 결정한 배경을 공유했습니다.

이 방법은 서버와 클라이언트 모두에서 번역 파일을 동적으로 관리할 수 있게 해주며, 초기 로딩 시간을 최적화하고 네트워크 요청을 최소화하는 장점이 있습니다. 또한, 번역 파일이 변경되거나 업데이트될 때 유연하게 대응할 수 있어 확장성과 유지보수 측면에서도 좋은 선택이었습니다.

이를 통해 프로젝트에서 다국어 지원을 효율적으로 구축할 수 있었으며, 커스텀 훅을 통해 번역 파일 로드와 초기화를 간편하게 처리할 수 있는 방안을 제시했습니다.

다음 포스팅에서는 Context API를 활용하여 클라이언트 컴포넌트에서 번역 데이터를 관리하는 방법에 대해 다룰 예정입니다.

profile
건들면 물어요

0개의 댓글

관련 채용 정보