Logo

React Intl로 다국어 지원하기 (국제화)

국내 많은 서비스들이 당장 다국어 지원의 요구가 없더라도 향후 해외 진출을 염두해두고 개발 초기부터 애플리케이션을 국제화(internalization)하는 사례가 늘고 있습니다. 이번 포스팅에서는 React Intl 라이브러리를 이용해서 다국어를 지원하는 방법에 대해서 알아보겠습니다.

React Intl 설치

React Intl 라이브러리는 Node.js 패키지 매니저인 npm으로 설치할 수 있습니다.

$ npm i react-intl

Locale: Language & Region

국제화(internalization)된 애플리케이션은 다양한 언어로 서비스를 할 수 있습니다. 그러기 위해서는 사용자의 언어/지역을 자동으로 감지하거나 사용자가 직접 선택할 수 있도록 합니다.

사용자의 언어/지역 정보를 플랫폼이나 프로그래밍 언어에 구애받지 않고 사용할 수 있도록 표준화 시킨 코드를 locale이라고 부르는데요. locale 코드는 필수적으로 2자리로 언어를 표시하고, 선택적으로 여기에 2자리를 더해 지역까지 표시할 수 있습니다.

예를 들어, 한국어는 한국에서만 사용되기 때문에 ko를 사용하고, 여러 지역에서 사용되는 영어의 경우, 미국 영어는 en-US, 영국 영어는 en_GB를 사용합니다.

브라우저에서 돌아가는 웹 애플리케이션의 경우 언어/지역 설정을 navigator.language 또는 navigator.languages으로부터 읽어올 수 있습니다.

const locale = navigator.language; // 또는 `navigator.languages[0]`

애플리케이션 수준에서 사용자가 직접 선택한 언어/지역을 선택하게 하고 localStorage와 같은 곳에 저장해두고 읽어와서 사용할 수도 있습니다.

const locale = localStorage.getItem("locale") ?? "ko";

본 포스팅에서는 브라우저 설정 변경없이 간단하게 언어/지역을 바꿔볼 수 있도록 2번째 방법을 사용하여 예제 애플리케이션을 구현해보겠습니다.

메세지 관리

다국어를 지원하려면 지원하는 언어 별로 메세지를 관리해야 합니다. 일반적으로는 locale 별로 JSON 파일에 필요한 모든 메세지 데이터를 객체 형태로 저장합니다. 객체의 속성은 메세지의 식별자(ID)가 되고 값은 해당 locale에서 사용할 메세지의 템플릿이 됩니다.

lang/en-US.json
{
  "title": "React Intl",
  "info": "The current language is {locale}.",
  "label": "Language",
  "button": "Save"
}
lang/ko.json
{
  "title": "리액트 Intl",
  "info": "현재 언어는 {locale}입니다.",
  "label": "언어",
  "button": "저장"
}
lang/zh.json
{
  "label": "语",
  "button": "节省"
}

이렇게 지원하는 모든 locale 별로 메세지 데이터를 JSON 파일에 저장해두고 불러올 수 있습니다.

import enUsMsg from "./lang/en-US.json";
import koMsg from "./lang/ko.json";
import zhMsg from "./lang/zh.json";

IntlProvider 컴포넌트

다국어 지원은 일반적으로 애플리케이션의 특정 부분에서만 일어나는 것이 아니라 전역에서 일어납니다. React Intl 라이브러리는 내부적으로 intl이라는 객체를 이용해서 메세지를 포멧팅하는데요. 따라서 반드시 <IntlProvider/> 컴포넌트로 애플리케이션의 최상위 컴포넌트를 감싸서 intl 객체가 모든 하위 컴포넌트에서 접근 가능하도록 만들어줘야 합니다.

App.jsx
import { IntlProvider } from "react-intl";
import enUsMsg from "./lang/en-US.json";
import koMsg from "./lang/ko.json";
import zhMsg from "./lang/zh.json";
import Page from "./Page";

const locale = localStorage.getItem("locale") ?? "ko";
const messages = { "en-US": enUsMsg, ko: koMsg, zh: zhMsg }[locale];

function App() {
  return (
    <IntlProvider locale={locale} messages={messages}>
      <Page />
    </IntlProvider>
  );
}

export default App;

<IntlProvider/> 컴포넌트는 localemessages를 prop으로 받는데요. locale prop에는 사용자의 지역/언어에 해당하는 locale 코드를 messages prop에는 이 locale 코드에 해당하는 메세지 데이터를 넘김니다.

FormattedMessage 컴포넌트

먼저, 로컬 스토리지에 저장되어 있는 locale 코드에 부합하는 메세지를 보여주는 <Page/> 컴포넌트를 구현해보겠습니다.

React Intl 라이브러리는 JSX로 다국어 메세지를 보여줄 수 있도록 <FormattedMessage/> 컴포넌트를 제공하고 있습니다. <FormattedMessage/> 컴포넌트에 id prop으로 메세지 식별자를 넘기면, 위에서 <IntlProvider/> 컴포넌트의 message prop으로 넘겼던 객체로 부터 메세지를 찾습니다.

Page.jsx
import { FormattedMessage } from "react-intl";
import Form from "./Form";

function Page() {
  return (
    <main>
      <h1>
        <FormattedMessage id="title" />
      </h1>
      <p>
        <FormattedMessage
          id="info"
          defaultMessage="메세지를 찾을 수 없습니다. (locale: {locale})"
          values={{ locale: localStorage.getItem("locale") }}
        />
      </p>
      <Form />
    </main>
  );
}

export default Page;

만약에 메세지 객체에 해당 메세지 식별자(ID)에 대한 메세지가 존재하지 않는다면 이 메세지 식별자를 자체를 메세지 문자열 대신에 화면에 보여지게 됩니다. 예를 들어, 중국어 메세지 객체에는 title이라는 메세지 식별자에 해당하는 중국어 메세지가 존재하지 않기 때문에, locale이 zh일 때, id prop으로 넘겼던 title 자체가 메세지로 쓰이게 됩니다.

대부분의 경우, 메세지 식별자가 메세지 문자열 대신에 사용되는 것을 원하지 않기 때문에, defaultMessage prop을 사용해서 이를 방지하기 것이 권장됩니다. 예를 들어, 두 번째로 <FormattedMessage/> 컴포넌트가 사용된 부분을 보면 defaultMessage prop에 메세지를 찾을 수 없습니다. (locale: {locale})을 넘기고 있습니다. 따라서, locale이 zh일 때, 중국어 메세지 객체에 info 메세지 식별자에 대한 메세지가 누락되어 있더라도, 메세지를 찾을 수 없습니다. (locale: zh)가 화면에 보여지게 되는 것입니다.

마지막으로 사용된 values prop은 메세지 템플릿 안에 사용된 변수에 대입할 값을 넘기기 위해서 사용되었습니다.

useIntl() & formatMessage()

이제, 로컬 스토리지에 저장되어 있는 locale 값을 변경할 수 있는 <Form/> 컴포넌트를 구현해보겠습니다. 선택란(select)을 통해서 다른 locale을 선택하고 저장 버튼(button)을 클릭하면 해당 locale 코드에 부합하는 메세지가 화면에 나타나도록 하겠습니다.

저장 버튼이 아이콘으로 되어 있기 때문에 웹 접근성 향상을 위해서 aria-label 속성을 통해 버튼 이름을 설정해주려고 하는데요. HTML 속성으로는 문자열만 사용할 수 있기 때문에 <FormattedMessage/> 컴포넌트를 사용할 수 없는 문제가 발생합니다.

이럴 경우에는, React Intl 라이브러리에서 제공하는 useIntl() 훅(hook)을 통해 우선 intl 객체를 얻습니다. 그 다음에 intl 객체의 formatMessage() 함수를 호출하면 얻은 메세지 문자열을 aria-label 속성에 세팅해주면 됩니다.

Form.jsx
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";

function Form() {
  const [locale, setLocale] = useState(localStorage.getItem("locale") ?? "ko");
  const intl = useIntl();

  return (
    <form onSubmit={() => localStorage.setItem("locale", locale)}>
      <label htmlFor="locale">
        <FormattedMessage
          id="label"
          defaultMessage="Language (default message)"
        />
      </label>
      <select
        id="locale"
        value={locale}
        onChange={({ target: { value } }) => setLocale(value)}
      >
        <option value="ko">ko</option>
        <option value="en-US">en-US</option>
        <option value="zh">zh</option>
      </select>

      <button
        aria-label={intl.formatMessage({
          id: "button",
          defaultMessage: "Save"
        })}
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
          className="feather feather-save"
        >
          <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
          <polyline points="17 21 17 13 7 13 7 21"></polyline>
          <polyline points="7 3 7 8 15 8"></polyline>
        </svg>
      </button>
    </form>
  );
}

export default Form;

전체 코드

본 포스팅에서 작성한 코드는 아래에서 확인하고 직접 실행해보실 수 있습니다. (locale 코드를 선택한 후에 저장 아이콘을 클릭해보세요.)

마치면서

지금까지 React Intl 라이브러리를 이용해서 React 애플리케이션에서 다국어 지원을 하는 방법에 대해서 간단히 살펴보았습니다. 정리를 해보면 <IntlProvider/> 컴포넌트를 통해 애플리케이션 최상위에서 사용자의 언어/지역(locale) 코드와 메세지 데이터를 설정해주면, <FormattedMessage/> 컴포넌트 또는 intl.formatMessage() 함수를 통해서 해당 언어/지역에 부합하는 메세지를 화면에 보여줄 수 있습니다.