다국어 React 앱을 국제화(i18n)로 구축하는 방법

발행: (2025년 12월 8일 오후 04:03 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Internationalization (i18n)이란?

Internationalization (i18n)은 애플리케이션을 설계하고 개발하는 과정으로, 엔지니어링 변경 없이도 다양한 언어와 지역에 쉽게 적용될 수 있도록 합니다.

용어 i18n은 “internationalization”이라는 단어에서 첫 번째 “i”와 마지막 “n” 사이에 18개의 글자가 있기 때문에 붙여졌습니다.

i18n의 핵심 요소

  • Translation – 텍스트 문자열을 다른 언어로 변환하기
  • Localization – 특정 지역에 맞게 콘텐츠를 조정하기(날짜, 숫자, 통화 등)
  • Pluralization – 각 언어별로 단수/복수 형태를 올바르게 처리하기

React 앱에서 i18n이 중요한 이유

  • Global Reach – 사용자의 모국어로 서비스를 제공해 새로운 시장으로 확장
  • Better User Experience – 사용자는 자신의 언어로 된 앱을 선호합니다(연구에 따르면 75 %가 모국어로 제품을 구매하는 것을 선호)
  • Competitive Advantage – 많은 앱이 적절한 국제화를 구현하지 못하고 있어 차별화된 경쟁력을 가질 수 있음
  • Legal Requirements – 일부 국가에서는 앱을 현지 언어로 제공하도록 요구함
  • SEO Benefits – 다국어 콘텐츠가 다양한 지역에서 검색 순위를 높여줌
  • Increased Conversions – 사용자는 콘텐츠가 자신의 언어일 때 행동을 완료할 확률이 높아짐

사전 준비 사항

  • React와 React Hooks에 대한 기본 지식
  • Node.js와 npm이 설치된 환경
  • VS Code와 같은 코드 편집기

프로젝트 설정

Vite를 사용해 새로운 React 애플리케이션을 생성합니다:

npm create vite@latest react-i18n-demo -- --template react
cd react-i18n-demo

필요한 i18n 패키지를 설치합니다:

npm install i18next react-i18next i18next-browser-languagedetector

개발 서버를 실행합니다:

npm run dev

앱이 이제 실행 중이어야 합니다.

Note: 이 튜토리얼은 스타일링 라이브러리를 사용하지 않습니다. 간단히 인라인 스타일을 정의했지만 실제 애플리케이션에서는 CSS를 별도 파일로 분리하거나 Tailwind CSS와 같은 프레임워크를 사용하는 것이 좋습니다.

기본 CSS 교체

/* src/index.css */
:root {
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
}

문제점: 하드코딩된 문자열

import { useState } from 'react';

function ProductCard({ product }) {
  return (
    <>
      {/* ❌ Hardcoded English strings */}
      <h3>{product.name}</h3>

      <p>Price: ${product.price}</p>

      <p>In stock: {product.stock} items</p>

      {product.stock === 0 && (
        <p>Out of stock</p>
      )}

      {product.stock === 1 && (
        <p>Only 1 item left!</p>
      )}

      {product.stock > 1 && (
        <p>In stock</p>
      )}

      <button>Add to cart</button>
    </>
  );
}

export default ProductCard;

이 접근 방식의 문제점

  • 번역 지원이 없음 – 모든 텍스트가 영어로만 제공
  • 유지 보수 어려움 – 텍스트를 변경하려면 여러 컴포넌트를 수정해야 함
  • 복수형 처리 미흡 – “1 item” vs “2 items”가 하드코딩됨
  • 현지화 부재 – 날짜와 숫자 형식이 고정됨
  • 확장성 문제 – 새로운 언어를 추가하려면 모든 컴포넌트를 수정해야 함

해결책: react-i18next

react-i18next는 React 국제화를 위한 가장 인기 있고 강력한 솔루션입니다. 주요 기능은 다음과 같습니다.

  • 간단한 번역 API
  • 자동 언어 감지
  • 복수형 지원
  • 중첩 번역
  • 날짜·숫자·통화 포맷팅
  • 번역 파일의 지연 로딩
  • TypeScript 지원

번역 파일 만들기

utils/locales 폴더에 별도의 JavaScript 파일로 번역을 구성합니다:

utils/
  locales/
    en/
      translation.js
    es/
      translation.js
    fr/
      translation.js

English (utils/locales/en/translation.js)

export default {
  en: {
    translation: {
      "product.price": "Price",
      "product.inStock": "In stock: {{count}} items",
      "product.outOfStock": "Out of stock",
      "product.onlyOneLeft": "Only 1 item left!",
      "product.addToCart": "Add to cart",

      "profile.welcome": "Welcome, {{name}}!",
      "profile.memberSince": "Member since: {{date}}",
      "profile.loyaltyPoints": "You have {{points}} loyalty points",
      "profile.orders_zero": "You have no orders",
      "profile.orders_one": "You have 1 order",
      "profile.orders_other": "You have {{count}} orders",
      "profile.editProfile": "Edit profile",
    },
  },
};

Spanish (utils/locales/es/translation.js)

export default {
  es: {
    translation: {
      "product.price": "Precio",
      "product.inStock": "En stock: {{count}} artículos",
      "product.outOfStock": "Agotado",
      "product.onlyOneLeft": "¡Solo queda 1 artículo!",
      "product.addToCart": "Añadir al carrito",

      "profile.welcome": "¡Bienvenido, {{name}}!",
      "profile.memberSince": "Miembro desde: {{date}}",
      "profile.loyaltyPoints": "Tienes {{points}} puntos de fidelidad",
      "profile.orders_zero": "No tienes pedidos",
      "profile.orders_one": "Tienes 1 pedido",
      "profile.orders_other": "Tienes {{count}} pedidos",
      "profile.editProfile": "Editar perfil",
    },
  },
};

French (utils/locales/fr/translation.js)

export default {
  fr: {
    translation: {
      "product.price": "Prix",
      "product.inStock": "En stock: {{count}} articles",
      "product.outOfStock": "Rupture de stock",
      "product.onlyOneLeft": "Plus qu'un article !",
      "product.addToCart": "Ajouter au panier",

      "profile.welcome": "Bienvenue, {{name}} !",
      "profile.memberSince": "Membre depuis : {{date}}",
      "profile.loyaltyPoints": "Vous avez {{points}} points de fidélité",
      "profile.orders_zero": "Vous n'avez aucune commande",
      "profile.orders_one": "Vous avez 1 commande",
      "profile.orders_other": "Vous avez {{count}} commandes",
      "profile.editProfile": "Modifier le profil",
    },
  },
};
Back to Blog

관련 글

더 보기 »