React에서 다국어(i18n) 처리하기 (With ICU format)

2025. 6. 8. 18:07·Frontend/Tech Insight
728x90
SMALL

이 글은 React에서 다국어화(internationalization, i18n)를 구현하는 방법을 다룹니다.

다국어화란?

다국어화는 애플리케이션을 여러 언어와 지역에서 사용할 수 있도록 설계하고 구현하는 과정입니다. 'i18n'이라는 용어는 'internationalization'의 첫 글자 'i'와 마지막 글자 'n' 사이에 18개의 글자가 있다고 해서 만들어진 약어입니다.

환경 설정하기

필요한 패키지 설치

가장 널리 사용되는 React 다국어화 라이브러리인 react-intl을 사용하겠습니다.

npm install react-intl
# 또는
yarn add react-intl

기본 설정

src/App.js 파일을 열고 다국어 설정을 위한 기본 구조를 추가합니다. 먼저 애플리케이션의 최상위에서 IntlProvider를 설정해야 합니다.

 

locale 적용 gif

// App.js
import React, { useState } from 'react';
import { FormattedMessage, IntlProvider } from 'react-intl';

// 메시지 정의
const messages = {
  ko: {
    'app.welcome': '안녕하세요!',
    'app.description': 'React 다국어화 예제입니다.',
    'button.click': '클릭하세요',
  },
  en: {
    'app.welcome': 'Hello!',
    'app.description': 'This is a React i18n example.',
    'button.click': 'Click me',
  },
};

function App() {
  const [locale, setLocale] = useState('ko');

  return (
    <IntlProvider
      locale={locale}
      messages={messages[locale]}
      defaultLocale="ko"
    >
      <div>
        <button onClick={() => setLocale('ko')}>한국어</button>
        <button onClick={() => setLocale('en')}>English</button>
      </div>

      <FormattedMessage id="app.welcome" />
    </IntlProvider>
  );
}

export default App;

 

기본 사용 방법

가장 기본적인 텍스트 번역은 FormattedMessage 컴포넌트를 사용합니다.

<FormattedMessage id="app.welcome" />

다른 방법으로는 useIntl 훅을 사용할 수 있습니다.

import React from 'react';
import { useIntl } from 'react-intl';

function MyComponent() {
  const intl = useIntl();

  const handleClick = () => {
    // 알림창에 번역된 메시지 표시
    alert(intl.formatMessage({ id: 'button.click' }));
  };

  return (
    <button 
      title={intl.formatMessage({ id: 'button.tooltip' })}
      onClick={handleClick}
    >
      {intl.formatMessage({ id: 'button.click' })}
    </button>
  );
}

심화 기능

텍스트에 변수 사용하기

문장에 10개, 홍길동님 같이 서버에서 전달받은 데이터를 사용해야 하는 경우가 있습니다. 이런 경우, 메시지 파일에 변수를 사용할 수 있습니다.

const messages = {
  ko: {
    'text.name': '{name}님',
    'text.count': '{count}개',
  },
  ...
};

<FormattedMessage id="text.name" values={{ name: '홍길동' }} /> // 홍길동님
<FormattedMessage id="text.count" values={{ count: 10 }} /> // 10개

HTML 태그와 스타일링

작업을 하다 보면 요구사항 중 텍스트에 링크를 걸어야 하거나 특정 텍스트에 강조 또는 색상 변경 같은 요구사항이 있습니다.
이를 해결하기 위해 두 가지 방법이 있습니다.

HTML 태그와 스타일링 예제

values 속성에 함수 사용하기

const messages = {
  ko: {
    'text.with.bold': '이것은 <bold>굵은 텍스트</bold>가 포함된 문장입니다.',
    'text.with.link': '<link>여기를 클릭</link>하면 구글로 이동합니다.',
    'text.mixed': '<bold>중요:</bold> <link>문서</link>를 확인하세요.',
  },
};

<div>
  {/* 굵은 텍스트 */}
  <p>
    <FormattedMessage
      id="text.with.bold"
      values={{
        bold: chunks => <strong>{chunks}</strong>,
        count: 10,
      }}
    />
  </p>

  {/* 링크 */}
  <p>
    <FormattedMessage
      id="text.with.link"
      values={{
        link: chunks => (
          <a
            href="https://google.com"
            target="_blank"
            rel="noopener noreferrer"
          >
            {chunks}
          </a>
        ),
      }}
    />
  </p>

  {/* 혼합 사용 */}
  <p>
    <FormattedMessage
      id="text.mixed"
      values={{
        bold: chunks => (
          <strong style={{ color: 'red' }}>{chunks}</strong>
        ),
        link: chunks => (
          <a href="/docs" className="doc-link">
            {chunks}
          </a>
        ),
      }}
    />
  </p>
</div>

richTextElements 사용하기

richTextElements 속성은 미리 특정 태그에 대해 추상화하여 자동으로 변환해주는 속성입니다.

richTextElements 예제 이미지

const messages = {
  ko: {
    'welcome.message': '<b>환영합니다!</b> <link>가이드</link>를 확인하세요.',
    'important.note': '<highlight>중요한</highlight> 공지사항입니다.',
  },
  ...
};

<IntlProvider
  ...
  defaultRichTextElements={{
    b: chunks => <strong>{chunks}</strong>,
    link: chunks => <a href="/docs">{chunks}</a>,
    highlight: chunks => <span style={{ color: 'red' }}>{chunks}</span>,
  }}
>
  <FormattedMessage id="welcome.message" />
  <FormattedMessage id="important.note" />
</IntlProvider>

ICU MessageFormat 이해하기

ICU MessageFormat은 국제화 메시지를 위한 표준 형식입니다.

ICU MessageFormat 기본 문법

기본 구조는 다음과 같습니다: {변수명, 타입, 형식}

복수형 처리 (Plural)

복수형 처리는 다음과 같은 형식으로 할 수 있습니다:

const messages = {
  ko: {
    'items.count': '{count, plural, =0 {항목이 없습니다} =1 {항목 1개} other {항목 #개}}'
  },
  en: {
    'items.count': '{count, plural, =0 {No items} =1 {One item} other {# items}}'
  }
};

// 사용 예시
function ItemCount({ count }) {
  return (
    <FormattedMessage
      id="items.count"
      values={{ count }}
    />
  );
}

// 결과
// count=0: "항목이 없습니다" / "No items"
// count=1: "항목 1개" / "One item"  
// count=5: "항목 5개" / "5 items"

선택형 처리 (Select)

선택형 처리는 다음과 같은 형식으로 할 수 있습니다:

const messages = {
  ko: {
    'user.greeting': '{gender, select, male {안녕하세요, {name}님!} female {안녕하세요, {name}님!} other {안녕하세요!}}'
  },
  en: {
    'user.greeting': '{gender, select, male {Hello, Mr. {name}!} female {Hello, Ms. {name}!} other {Hello!}}'
  }
};

function UserGreeting({ gender, name }) {
  return (
    <FormattedMessage
      id="user.greeting"
      values={{ gender: 'male', name: '홍길동' }}
    />
  );
}

// 결과
// gender=male: "안녕하세요, 홍길동님!" / "Hello, Mr. 홍길동!"
// gender=female: "안녕하세요, 홍길동님!" / "Hello, Ms. 홍길동!"
// gender=other: "안녕하세요!" / "Hello!"

other 조건과 누락된 조건 처리

react-intl에서 ICU MessageFormat을 사용할 때, other 조건이 없으면 메시지가 제대로 표시되지 않습니다:

// ❌ 잘못된 사용 - 메시지가 제대로 표시되지 않음
const messages = {
  ko: {
    'user.greeting': '{gender, select, male {안녕하세요, {name}님!} female {안녕하세요, {name}님!}}'
  }
};

// ✅ 올바른 사용
const messages = {
  ko: {
    'user.greeting': '{gender, select, male {안녕하세요, {name}님!} female {안녕하세요, {name}님!} other {안녕하세요!}}'
  }
};

또한, ICU에 없는 조건을 입력한다면, other에 있는 값이 표시됩니다.

// 예시: gender가 'unknown'인 경우
<FormattedMessage
  id="user.greeting"
  values={{ gender: 'unknown', name: '홍길동' }}
/>
// 결과: "안녕하세요!"

추가 고려사항

키 컨벤션

번역 키를 효과적으로 관리하기 위한 명명 규칙을 알아보겠습니다.

1. 명확하고 일관된 이름 사용

텍스트 ❌ 잘못된 예 ✅ 좋은 예
이름 form.name1 form.name_person
이름 form.name2  

2. 중첩 구조 활용

{
  "header": {
    "title": "환영합니다",
    "logo_alt": "회사 로고"
  },
  "error": {
    "required": "필수 입력 항목입니다",
    "invalid_url": "올바른 URL을 입력해주세요"
  },
  "footer": {
    "copyright": "© 2024 회사명. All rights reserved",
    "contact": {
      "phone": "02-123-4567",
      "email": "info@company.com"
    }
  }
}

3. common 카테고리 활용

여러 페이지에서 공통으로 사용되는 문자열은 common 카테고리에 저장합니다:

텍스트 ❌ 잘못된 예 ✅ 좋은 예
취소 cancel common.cancel
크레딧 credits common.credits
기본 standard common.pricing_plan.standard

4. 명명 규칙

  1. 도메인.기능.요소 형식 사용
    • 예: auth.login.button, common.errors.required
    • 이유: 번역가가 문맥을 쉽게 이해할 수 있도록 도와줍니다. 예를 들어 auth.login.button은 인증 페이지의 로그인 버튼이라는 것을 명확히 알 수 있습니다.
  2. 일관된 케이스 사용
    • 스네이크 케이스(snake_case) 추천
    • 예: user_profile.settings.button
    • 이유: 일관된 케이스 사용은 코드의 가독성을 높이고, 키를 찾고 관리하기 쉽게 만듭니다. 스네이크 케이스는 단어 사이의 구분이 명확하여 특히 번역 키와 같은 긴 문자열에 적합합니다.
  3. 의미 있는 이름 사용
    • 약어 사용 지양
    • 예: rg_frm_fnam_lb → registration_form.label.first_name
    • 이유: 의미 있는 이름은 코드의 의도를 명확히 전달하고, 유지보수를 쉽게 만듭니다. 약어는 시간이 지나면 의미를 잊기 쉽고, 새로운 팀원이 코드를 이해하는 데 어려움을 줄 수 있습니다.
  4. 명확한 컨텍스트 제공
    • 번역가가 이해하기 쉬운 이름 사용
    • 예: form.name_person vs form.name_company
    • 이유: 같은 단어라도 문맥에 따라 번역이 달라질 수 있습니다. 예를 들어 '이름'이라는 단어는 사람의 이름과 회사명에서 다른 번역이 필요할 수 있습니다. 명확한 컨텍스트를 제공하면 정확한 번역을 보장할 수 있습니다.

이러한 규칙을 따르면 번역 키를 더 효율적으로 관리하고, 번역가와 개발자 간의 협업을 원활하게 할 수 있습니다. 특히 대규모 프로젝트에서는 이러한 일관된 규칙이 코드의 품질과 유지보수성을 크게 향상시킵니다.

추천 확장 프로그램

다국어 처리를 더 효율적으로 할 수 있도록 도와주는 VS Code 확장 프로그램을 소개합니다.

i18n-ally

i18n-ally는 Lokalise에서 만든 VS Code 확장 프로그램으로, 다국어 처리를 위한 다양한 기능을 제공합니다.

주요 기능:

  1. 인라인 번역 표시
    • 코드에서 번역 키를 사용할 때 실제 번역된 텍스트를 인라인으로 표시
    • 예: <FormattedMessage id="welcome.message" /> → "안녕하세요!"
      이미지
  2. 키 자동완성
    • 번역 키 입력 시 자동완성 지원
    • 사용 가능한 모든 키를 목록으로 표시
  3. 번역 관리
    • 모든 언어의 번역을 한 곳에서 관리
    • 누락된 번역 확인 및 추가
    • 번역 리뷰 시스템 지원
      이미지
  4. 번역 추출
    • 코드에서 하드코딩된 텍스트를 번역 키로 자동 변환
    • 새로운 번역 키 생성 및 관리
  5. 문제 감지
    • 누락된 번역 키 감지
    • 잘못된 키 사용 감지
    • 번역 불일치 감지

이 확장 프로그램을 사용하면 다국어 처리가 더욱 효율적이고 안정적으로 이루어질 수 있습니다. 특히 대규모 프로젝트에서 번역 관리와 일관성 유지에 큰 도움이 됩니다.

마무리

다국어 처리는 단순히 텍스트를 번역하는 것을 넘어서는 작업입니다. 사용자에게 일관된 경험을 제공하는 것이 중요합니다. react-intl은 이러한 일관성을 유지하는 데 큰 도움이 됩니다.

예를 들어, 날짜 포맷팅을 생각해보면:

  • 한국: "2024년 3월 15일"
  • 미국: "March 15, 2024"
  • 일본: "2024年3月15日"

이처럼 각 지역마다 다른 형식을 사용하는데, react-intl의 FormattedDate 컴포넌트를 사용하면 로케일에 맞는 형식으로 자동 변환됩니다.

<FormattedDate
  value={new Date()}
  year="numeric"
  month="long"
  day="numeric"
/>

또한 단위 변환도 중요한 부분입니다:

  • 거리: km vs miles
  • 무게: kg vs pounds
  • 온도: °C vs °F

FormattedNumber를 사용하면 이러한 단위 변환도 자동으로 처리할 수 있습니다.

<FormattedNumber
  value={100}
  style="unit"
  unit="kilometer"
  unitDisplay="long"
/>
// 한국: "100킬로미터"
// 미국: "100 miles"

 

이처럼 react-intl은 단순한 텍스트 번역을 넘어, 각 지역의 문화와 관습을 반영한 일관된 사용자 경험을 제공할 수 있게 해줍니다. 이는 글로벌 서비스를 제공하는 데 있어 매우 중요한 요소입니다.

앞으로도 다국어 처리와 관련된 새로운 요구사항이 생길 수 있지만, react-intl의 다양한 기능을 활용하면 효과적으로 대응할 수 있을 것입니다. 특히 날짜, 숫자, 단위 등과 같은 복잡한 포맷팅을 일관되게 처리할 수 있다는 점이 큰 장점입니다.

 

참고

  • react-intl 문서
  • Tolgee 블로그
  • ICU MessageFormat 문서
저작자표시 (새창열림)

'Frontend > Tech Insight' 카테고리의 다른 글

Feature-based vs Layer-based 어떤 구조를 사용해야 할까?  (0) 2025.12.14
React Custom Hook은 어떻게 작동할까?  (2) 2025.08.10
Tailwind에서 조건부 클래스와 충돌 방지를 동시에 해결하기  (0) 2025.05.20
React에서 DOMPurify로 XSS 공격 방어하기  (0) 2025.04.24
Bun: 빠르고 혁신적인 JavaScript 런타임Bun이란?  (0) 2025.02.16
'Frontend/Tech Insight' 카테고리의 다른 글
  • Feature-based vs Layer-based 어떤 구조를 사용해야 할까?
  • React Custom Hook은 어떻게 작동할까?
  • Tailwind에서 조건부 클래스와 충돌 방지를 동시에 해결하기
  • React에서 DOMPurify로 XSS 공격 방어하기
끄적끄적 개발자
끄적끄적 개발자
생각나는 대로 끄적끄적, 코드 노트
  • 끄적끄적 개발자
    코딩을 끄적끄적
    끄적끄적 개발자
  • 전체
    오늘
    어제
    • 분류 전체보기 (47)
      • Frontend (43)
        • Tech Insight (24)
        • Dev Practice (7)
        • React Hook Form (4)
        • Module Federation (3)
        • Clone coding (5)
      • UIUX (2)
      • ETC (2)
  • 인기 글

  • 태그

    회고
    개발/코드품질
    개발/언어
    UI/UX
    개발/기술
    개발/정보
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
끄적끄적 개발자
React에서 다국어(i18n) 처리하기 (With ICU format)
상단으로

티스토리툴바