ZIZIZIG

글로벌 서비스 성공을 위한 i18n(국제화) 전략

ZIZIZIG Admin
2025년 10월 13일조회 890개 댓글
글로벌 서비스 성공을 위한 i18n(국제화) 전략

글로벌 서비스 성공을 위한 i18n(국제화) 전략

웹 서비스가 글로벌 시장으로 확장하려면 단순히 번역만으로는 부족합니다. 진정한 국제화(i18n, internationalization)는 언어, 문화, 법률, 결제 시스템, UX까지 모든 것을 현지화하는 것을 의미합니다. Airbnb, Netflix, Spotify 같은 글로벌 서비스들이 어떻게 200개 이상의 국가에서 서비스하는지 그 비밀을 파헤쳐봅니다.

i18n vs l10n: 차이점 이해하기

i18n (Internationalization, 국제화):
소프트웨어를 다양한 언어와 지역에 쉽게 적응시킬 수 있도록 설계하는 것

l10n (Localization, 현지화):
특정 언어와 문화에 맞게 실제로 번역하고 조정하는 것

i18n: "설계"
├─ 텍스트를 하드코딩하지 않고 변수로 처리
├─ 날짜/시간/숫자 형식을 동적으로 변경 가능하게
├─ RTL(Right-to-Left) 언어 지원
└─ 문화적 차이를 고려한 UI 설계

l10n: "실행"
├─ 한국어 번역
├─ 일본어 번역
├─ 아랍어 번역 (RTL 레이아웃 적용)
└─ 각 지역의 결제 수단 연동

프론트엔드 i18n 구현

Next.js 14 + next-intl

가장 강력한 Next.js i18n 솔루션입니다.

// middleware.ts
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  locales: ['en', 'ko', 'ja', 'zh'],
  defaultLocale: 'en',
  localeDetection: true // 브라우저 언어 자동 감지
});

export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)']
};
// app/[locale]/page.tsx
import {useTranslations} from 'next-intl';

export default function HomePage() {
  const t = useTranslations('HomePage');
  
  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('description', {count: 100})}</p>
      <p>{t('welcome', {name: 'John'})}</p>
    </div>
  );
}
// messages/en.json
{
  "HomePage": {
    "title": "Welcome to our site",
    "description": "We have {count} products",
    "welcome": "Hello, {name}!"
  }
}

// messages/ko.json
{
  "HomePage": {
    "title": "사이트에 오신 것을 환영합니다",
    "description": "{count}개의 상품이 있습니다",
    "welcome": "{name}님, 안녕하세요!"
  }
}

Vue i18n (Nuxt 3)

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],
  i18n: {
    locales: [
      { code: 'en', iso: 'en-US', file: 'en.json', name: 'English' },
      { code: 'ko', iso: 'ko-KR', file: 'ko.json', name: '한국어' },
      { code: 'ja', iso: 'ja-JP', file: 'ja.json', name: '日本語' },
    ],
    defaultLocale: 'en',
    strategy: 'prefix_except_default',
    detectBrowserLanguage: {
      useCookie: true,
      cookieKey: 'i18n_redirected',
      redirectOn: 'root'
    }
  }
})
<template>
  <div>
    <h1>{{ $t('welcome') }}</h1>
    <p>{{ $t('productsCount', { count: products.length }) }}</p>
    
    <!-- 언어 전환 -->
    <select v-model="$i18n.locale">
      <option value="en">English</option>
      <option value="ko">한국어</option>
      <option value="ja">日本語</option>
    </select>
  </div>
</template>

<script setup>
const { locale } = useI18n();
</script>

백엔드 i18n 처리

Accept-Language 헤더 활용

// server/api/products.get.ts
export default defineEventHandler(async (event) => {
  const acceptLanguage = getHeader(event, 'accept-language');
  const locale = parseAcceptLanguage(acceptLanguage) || 'en';
  
  const products = await prisma.product.findMany({
    select: {
      id: true,
      translations: {
        where: { locale },
        select: { name: true, description: true }
      }
    }
  });
  
  return products.map(p => ({
    id: p.id,
    name: p.translations[0]?.name || p.name,
    description: p.translations[0]?.description || p.description
  }));
});

데이터베이스 다국어 설계

방법 1: JSON 컬럼 (간단한 경우)

model Product {
  id          Int    @id @default(autoincrement())
  name        Json   // { "en": "Laptop", "ko": "노트북", "ja": "ノートパソコン" }
  description Json
  price       Int
}

방법 2: 번역 테이블 (복잡한 경우, 추천)

model Product {
  id           Int                 @id @default(autoincrement())
  sku          String              @unique
  price        Int
  translations ProductTranslation[]
}

model ProductTranslation {
  id          Int     @id @default(autoincrement())
  productId   Int
  locale      String  // 'en', 'ko', 'ja'
  name        String
  description String
  product     Product @relation(fields: [productId], references: [id])
  
  @@unique([productId, locale])
  @@index([locale])
}

날짜/시간 현지화

import { format } from 'date-fns';
import { ko, ja, enUS } from 'date-fns/locale';

const locales = { en: enUS, ko, ja };

function formatDate(date: Date, locale: string) {
  return format(date, 'PPP', { locale: locales[locale] });
}

formatDate(new Date(), 'en'); // "December 13, 2025"
formatDate(new Date(), 'ko'); // "2025년 12월 13일"
formatDate(new Date(), 'ja'); // "2025年12月13日"

Relative Time (상대 시간)

import { formatDistanceToNow } from 'date-fns';

formatDistanceToNow(pastDate, { locale: ko, addSuffix: true });
// "3시간 전"

formatDistanceToNow(pastDate, { locale: ja, addSuffix: true });
// "3時間前"

숫자 및 통화 현지화

// Intl.NumberFormat 사용
const formatCurrency = (amount: number, currency: string, locale: string) => {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  }).format(amount);
};

formatCurrency(1234567, 'USD', 'en-US'); // "$1,234,567.00"
formatCurrency(1234567, 'KRW', 'ko-KR'); // "₩1,234,567"
formatCurrency(1234567, 'JPY', 'ja-JP'); // "¥1,234,567"
formatCurrency(1234567, 'EUR', 'de-DE'); // "1.234.567,00 €"

숫자 포맷팅

// 단위 표시
const formatNumber = (num: number, locale: string) => {
  if (num >= 1000000) {
    return (num / 1000000).toFixed(1) + 
           (locale === 'ko' ? '백만' : locale === 'ja' ? '百万' : 'M');
  }
  return num.toLocaleString(locale);
};

formatNumber(1500000, 'en'); // "1.5M"
formatNumber(1500000, 'ko'); // "1.5백만"
formatNumber(1500000, 'ja'); // "1.5百万"

RTL (Right-to-Left) 언어 지원

아랍어, 히브리어 등은 오른쪽에서 왼쪽으로 씁니다.

/* globals.css */
[dir="rtl"] {
  direction: rtl;
}

[dir="rtl"] .container {
  text-align: right;
}

/* Logical Properties 사용 (추천) */
.button {
  margin-inline-start: 10px; /* LTR: margin-left, RTL: margin-right */
  padding-inline: 20px 10px;
}
// Next.js에서 dir 속성 동적 설정
<html lang={locale} dir={['ar', 'he'].includes(locale) ? 'rtl' : 'ltr'}>

SEO와 다국어

hreflang 태그

Google에게 언어별 페이지를 알려줍니다.

<link rel="alternate" hreflang="en" href="https://example.com/en/products" />
<link rel="alternate" hreflang="ko" href="https://example.com/ko/products" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/products" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/products" />

Sitemap 생성

// sitemap.ts
export default async function sitemap() {
  const locales = ['en', 'ko', 'ja'];
  const routes = ['/', '/products', '/about'];
  
  return routes.flatMap(route =>
    locales.map(locale => ({
      url: `https://example.com/${locale}${route}`,
      lastModified: new Date(),
      alternates: {
        languages: Object.fromEntries(
          locales.map(l => [l, `https://example.com/${l}${route}`])
        )
      }
    }))
  );
}

성능 최적화

번역 파일 코드 스플리팅

// 동적 로드
const loadMessages = async (locale: string) => {
  const messages = await import(`@/messages/${locale}.json`);
  return messages.default;
};

// Tree Shaking으로 사용하지 않는 번역 제거
import { useTranslations } from 'next-intl';

// ❌ 모든 번역 로드
const t = useTranslations();

// ✅ 필요한 네임스페이스만 로드
const t = useTranslations('ProductPage');

CDN과 Edge 배포

// Vercel Edge Functions로 언어별 콘텐츠 캐싱
export const config = {
  runtime: 'edge',
};

export default async function handler(req: Request) {
  const locale = getLocale(req);
  const cacheKey = `page-${locale}`;
  
  const cached = await cache.get(cacheKey);
  if (cached) return cached;
  
  const html = await renderPage(locale);
  await cache.set(cacheKey, html, { ttl: 3600 });
  
  return html;
}

번역 워크플로우

1. 개발자 친화적: JSON 파일

// en.json
{
  "common": {
    "save": "Save",
    "cancel": "Cancel"
  },
  "products": {
    "title": "Our Products",
    "addToCart": "Add to Cart"
  }
}

2. 번역가 친화적: Crowdin / Lokalise

  • 웹 인터페이스에서 번역 작업
  • 컨텍스트 스크린샷 제공
  • 번역 메모리로 일관성 유지
  • GitHub 연동으로 자동 PR 생성

3. 자동 번역: DeepL API

async function autoTranslate(text: string, targetLang: string) {
  const response = await fetch('https://api-free.deepl.com/v2/translate', {
    method: 'POST',
    headers: {
      'Authorization': `DeepL-Auth-Key ${process.env.DEEPL_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      text: [text],
      target_lang: targetLang.toUpperCase()
    })
  });
  
  const data = await response.json();
  return data.translations[0].text;
}

// 초벌 번역 생성 후 사람이 리뷰

결제 시스템 현지화

국가별 결제 수단

const paymentMethods = {
  US: ['card', 'paypal', 'apple_pay'],
  KR: ['card', 'kakaopay', 'naverpay', 'toss'],
  JP: ['card', 'konbini', 'paypay'],
  CN: ['alipay', 'wechatpay'],
  IN: ['upi', 'paytm', 'card'],
};

function getAvailablePayments(country: string) {
  return paymentMethods[country] || ['card'];
}

Stripe Multi-Currency

const stripe = new Stripe(process.env.STRIPE_KEY);

await stripe.paymentIntents.create({
  amount: calculateAmount(price, currency),
  currency: currency.toLowerCase(),
  payment_method_types: getPaymentMethods(country),
  metadata: {
    locale: locale,
    country: country
  }
});

법률 및 규정 준수

GDPR (유럽)

  • 쿠키 동의 배너
  • 데이터 다운로드/삭제 요청 기능
  • 개인정보 처리방침 번역

중국

  • ICP 라이센스 필요
  • 중국 내 서버 호스팅
  • 알리바바 클라우드 사용

한국

  • 개인정보보호법 준수
  • 통신판매업 신고
  • 에스크로 서비스 (상황에 따라)

실전 프로젝트 구조

my-app/
├── messages/
│   ├── en.json
│   ├── ko.json
│   ├── ja.json
│   └── zh.json
├── middleware.ts (언어 감지)
├── app/
│   └── [locale]/
│       ├── layout.tsx
│       ├── page.tsx
│       └── products/
│           └── page.tsx
├── components/
│   ├── LanguageSwitcher.tsx
│   └── LocalizedLink.tsx
└── lib/
    ├── i18n.ts
    └── payment-localization.ts

GMI의 글로벌 서비스 구축

GMI는 100% 온라인으로 진행되는 글로벌 서비스 개발을 전문으로 합니다:

제공 서비스:

  • 다국어 아키텍처 설계
  • 번역 워크플로우 구축 (Crowdin 연동)
  • 결제 시스템 현지화 (Stripe, Toss, Alipay 등)
  • SEO 최적화 (hreflang, sitemap)
  • 법률 자문 연결 (GDPR, 개인정보보호법)
  • 글로벌 CDN 및 Edge 배포

실제 경험:

  • 한국, 일본, 중국, 미국 시장 진출 프로젝트 다수
  • 10개 이상 언어 지원 서비스 구축
  • 월 100만 PV 글로벌 트래픽 처리

글로벌 시장 진출을 계획 중이시라면 GMI에 문의하세요.

공유하기

댓글 1개

Z
ZIZIZIG Admin2025. 12. 13.

good