이 글은 누구를 위한 것인가
- 국내 이커머스를 해외로 확장하려는 팀
- 크로스보더 진출 시 세금·통관 처리 방법이 막막한 개발자
- 언어·통화 현지화를 기술적으로 구현해야 하는 엔지니어
들어가며
크로스보더 이커머스에서 실패하는 이유는 보통 기술이 아니다. "달러로 결제가 되긴 하는데 환율이 고정되어 있어서 손해", "VAT를 모르고 있다가 나중에 추징", "배송 추적이 안 돼서 CS 폭주" 같은 운영 문제들이다.
기술적으로 제대로 구현하면 이런 문제들을 자동화할 수 있다. 이 글에서는 현지화의 각 레이어를 실전적으로 다룬다.
이 글은 bluefoxdev.kr의 글로벌 이커머스 기술 가이드 를 참고하고, 현지화 전략 실전 구현 관점에서 확장하여 작성했습니다. 크로스보더 커머스 전략은 bluebutton.kr 에서도 다루고 있습니다.
1. 현지화 레이어 구조
[크로스보더 현지화 4개 레이어]
1. 언어 (Language)
- 상품명, 설명, UI 텍스트
- 고객 리뷰, 이메일, 알림
- 법적 문서 (이용약관, 개인정보)
2. 통화 (Currency)
- 가격 표시 (실시간 환율)
- 결제 통화 (현지 통화 결제)
- 정산 통화 (판매자는 원화로)
- 환율 리스크 관리
3. 세금 (Tax)
- 부가세(VAT/GST): EU 국가별 세율 다름
- 수입관세: 품목·금액·국가별 차등
- 세금 영수증 발행
4. 물류 (Logistics)
- 현지 배송사 연동
- 통관 서류 자동 생성
- 반품 물류 (역물류)
- 배송 추적 통합
2. 통화 및 가격 현지화
// 실시간 환율 기반 가격 표시
interface CurrencyConfig {
code: string; // 'USD', 'EUR', 'JPY'
symbol: string; // '$', '€', '¥'
precision: number; // 소수점 자리 (JPY는 0)
locale: string; // 'en-US', 'de-DE', 'ja-JP'
}
const CURRENCIES: Record<string, CurrencyConfig> = {
KRW: { code: 'KRW', symbol: '₩', precision: 0, locale: 'ko-KR' },
USD: { code: 'USD', symbol: '$', precision: 2, locale: 'en-US' },
EUR: { code: 'EUR', symbol: '€', precision: 2, locale: 'de-DE' },
JPY: { code: 'JPY', symbol: '¥', precision: 0, locale: 'ja-JP' },
SGD: { code: 'SGD', symbol: 'S$', precision: 2, locale: 'en-SG' },
};
// 환율 서비스 (Open Exchange Rates API 기반)
class ExchangeRateService {
private cache: Map<string, { rate: number; cachedAt: Date }> = new Map();
private CACHE_TTL = 3600 * 1000; // 1시간
async getRate(from: string, to: string): Promise<number> {
const key = `${from}-${to}`;
const cached = this.cache.get(key);
if (cached && Date.now() - cached.cachedAt.getTime() < this.CACHE_TTL) {
return cached.rate;
}
const response = await fetch(
`https://openexchangerates.org/api/latest.json?app_id=${process.env.OXR_API_KEY}&base=${from}&symbols=${to}`
);
const data = await response.json();
const rate = data.rates[to];
this.cache.set(key, { rate, cachedAt: new Date() });
return rate;
}
}
// 가격 변환 및 포맷
async function formatPrice(
amountKRW: number,
targetCurrency: string
): Promise<string> {
const config = CURRENCIES[targetCurrency];
if (!config) throw new Error(`Unsupported currency: ${targetCurrency}`);
const rate = await exchangeRateService.getRate('KRW', targetCurrency);
const converted = amountKRW * rate;
return new Intl.NumberFormat(config.locale, {
style: 'currency',
currency: config.code,
minimumFractionDigits: config.precision,
maximumFractionDigits: config.precision,
}).format(converted);
}
3. VAT/세금 자동 계산
# 국가별 VAT 세율 (2026년 기준)
VAT_RATES = {
'DE': 0.19, # 독일
'FR': 0.20, # 프랑스
'GB': 0.20, # 영국
'IT': 0.22, # 이탈리아
'ES': 0.21, # 스페인
'JP': 0.10, # 일본
'AU': 0.10, # 호주
'SG': 0.09, # 싱가포르
'US': None, # 미국: 주별 세율 (별도 처리)
'KR': 0.10, # 한국
}
# EU de minimis 규정 (2021년부터 €150 이하도 VAT 부과)
EU_COUNTRIES = {'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES',
'FI', 'FR', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU',
'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK'}
class TaxCalculator:
def calculate_tax(
self,
subtotal: float,
destination_country: str,
product_category: str,
customer_vat_number: str = None
) -> dict:
"""
국가별 세금 계산
"""
# B2B VAT 면세 (EU 내 VAT 번호 제공 시)
if customer_vat_number and destination_country in EU_COUNTRIES:
if self._validate_vat_number(customer_vat_number):
return {
'vat_rate': 0,
'vat_amount': 0,
'note': 'B2B VAT reverse charge',
}
vat_rate = VAT_RATES.get(destination_country, 0)
if vat_rate is None:
# 미국: 주별 세율 (간략화)
vat_rate = 0.08 # 평균 세율 (실제로는 주소 기반 계산 필요)
# 일부 품목 면세 (예: 어린이 옷, 식품)
if self._is_zero_rated(product_category, destination_country):
vat_rate = 0
vat_amount = round(subtotal * vat_rate, 2)
return {
'vat_rate': vat_rate,
'vat_amount': vat_amount,
'total': subtotal + vat_amount,
'country': destination_country,
}
def _is_zero_rated(self, category: str, country: str) -> bool:
"""부가세 면세 품목 확인"""
zero_rated = {
'GB': ['food', 'baby_clothes', 'books'],
'IE': ['food', 'children_shoes'],
}
return category in zero_rated.get(country, [])
def calculate_import_duty(
self,
product_value: float,
destination_country: str,
hs_code: str,
) -> dict:
"""수입 관세 계산"""
# 면세 한도 (de minimis)
de_minimis = {
'US': 800, # USD
'JP': 10000, # JPY
'AU': 1000, # AUD
'CA': 20, # CAD (낮음)
'GB': 135, # GBP
'EU': 150, # EUR
}
threshold = de_minimis.get(destination_country, 0)
if product_value <= threshold:
return {'duty_rate': 0, 'duty_amount': 0, 'duty_free': True}
# HS 코드 기반 세율 조회 (실제는 외부 API 사용)
duty_rate = self._lookup_duty_rate(hs_code, destination_country)
duty_amount = round(product_value * duty_rate, 2)
return {
'duty_rate': duty_rate,
'duty_amount': duty_amount,
'hs_code': hs_code,
'duty_free': False,
}
4. 통관 서류 자동 생성
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
def generate_customs_declaration(order: dict) -> bytes:
"""CN22/CN23 세관 신고서 자동 생성"""
buffer = BytesIO()
c = canvas.Canvas(buffer, pagesize=A4)
# CN22 양식 (소형 패킷, 300g 이하 / EUR 300 이하)
c.setFont("Helvetica-Bold", 14)
c.drawString(50, 800, "Customs Declaration / Déclaration en douane")
c.drawString(50, 785, "CN 22")
# 발송인 정보
c.setFont("Helvetica", 10)
c.drawString(50, 760, f"From: {order['sender_name']}")
c.drawString(50, 748, f" {order['sender_address']}")
c.drawString(50, 736, f" Korea, Republic of")
# 수취인 정보
c.drawString(50, 716, f"To: {order['recipient_name']}")
c.drawString(50, 704, f" {order['recipient_address']}")
c.drawString(50, 692, f" {order['recipient_city']}, {order['recipient_country']}")
# 내용물
y = 660
c.setFont("Helvetica-Bold", 10)
c.drawString(50, y, "Contents / Contenu")
for item in order['items']:
y -= 18
c.setFont("Helvetica", 9)
c.drawString(50, y, f"{item['name_en']}")
c.drawString(300, y, f"Qty: {item['quantity']}")
c.drawString(380, y, f"Value: {item['currency']} {item['unit_value']:.2f}")
c.drawString(480, y, f"HS: {item['hs_code']}")
# 총액
y -= 30
c.setFont("Helvetica-Bold", 10)
c.drawString(50, y, f"Total Value: {order['currency']} {order['total_value']:.2f}")
# 서명
y -= 40
c.drawString(50, y, f"Date: {order['ship_date']}")
c.drawString(200, y, "Sender's signature: _______________")
c.save()
buffer.seek(0)
return buffer.getvalue()
5. 현지화 체크리스트
[해외 진출 전 기술 체크리스트]
통화/결제:
□ 현지 통화 표시 (실시간 환율 연동)
□ 현지 결제 수단 지원 (PayPal, Alipay, Klarna 등)
□ 환율 고정 기간 정책 (주문 후 N시간 동안 환율 보장)
언어:
□ 상품명/설명 번역 (전문 번역 vs MT+후편집)
□ UI 전체 번역 (RTL 언어 고려)
□ 고객 이메일/알림 현지화
□ 리뷰/CS 언어 지원
세금:
□ 국가별 VAT 자동 계산
□ 세금 포함/별도 표시 (국가마다 다름)
□ B2B VAT 면세 처리
□ 세금 영수증 자동 발행
물류:
□ DDP vs DDU 선택 (배달완납 vs 착불관세)
□ HS 코드 매핑
□ 통관 서류 자동 생성
□ 국제 배송 추적 통합
□ 반품 정책 및 역물류
마무리
크로스보더 이커머스에서 "일단 배송은 되는데..."로 시작하면 VAT 미납 추징, 관세 분쟁, CS 폭주로 이어진다. 특히 EU 시장은 VAT OSS(One Stop Shop) 등록이 의무화되었고, 이를 무시하면 법적 리스크가 크다.
첫 해외 진출 시장은 언어·세금·물류가 상대적으로 단순한 일본이나 미국 단일 주로 시작해 경험을 쌓은 다음, EU나 동남아로 확장하는 전략이 리스크를 낮춘다.