57.1 국제화가 번역이랑 다른가요?
제가 팀에서 국제화(internationalization, 여러 언어와 지역을 지원하도록 앱을 준비하는 일) 얘기를 꺼내면, 열에 아홉은 "아, 번역 말하는 거죠?"라고 해요. 반은 맞고 반은 틀려요. 번역은 국제화의 일부일 뿐이거든요. 국제화는 훨씬 넓어요.
이렇게 생각해보세요. 화면에 "1,234,567원"이라고 찍혀 있어요. 이걸 영어권 사용자에게 보여주려면 글자만 "won"으로 바꾸면 될까요? 아니에요. 미국이면 $1,234,567.89처럼 소수점과 통화 기호 위치가 다르고, 독일이면 1.234.567,89처럼 점과 쉼표가 아예 뒤바뀌어요. 인도는 12,34,567처럼 세 자리가 아니라 두 자리씩 끊기까지 해요. 문장만 번역해선 절대 안 나오는 차이죠.
그래서 국제화는 "이 앱이 어느 나라, 어느 언어로도 자연스럽게 보이도록 뼈대를 짜두는 일"이에요. 번역은 그 뼈대 위에 얹는 살이고요. 뼈대가 없으면 아무리 번역을 잘해도 어색한 화면이 나와요.
57.2 그럼 i18n이랑 l10n은 뭐가 달라요?
현장에서 i18n과 l10n이라는 줄임말을 자주 봐요. 처음 보면 암호 같죠. i18n은 internationalization의 앞 i와 뒤 n 사이에 글자가 18개라서 i18n이에요. l10n은 localization(지역화)의 l과 n 사이가 10개라서 l10n이고요. 개발자들의 게으른 작명이죠.
둘의 관계가 중요해요. i18n(국제화)은 준비, l10n(지역화)은 실제 채워 넣기예요. 국제화는 개발자가 "여기 문구는 나중에 어느 언어로도 갈아 끼울 수 있게, 날짜와 숫자는 지역 규칙을 따르게" 코드를 짜두는 일이에요. 한 번만 잘 해두면 되죠. 지역화는 그 준비된 자리에 한국어, 일본어, 아랍어 같은 실제 값을 넣는 일이에요. 언어를 늘릴 때마다 반복돼요.
비유하면 국제화는 여러 규격의 전구가 다 들어가는 소켓을 만드는 일이고, 지역화는 그 소켓에 맞는 전구를 하나씩 끼우는 일이에요. 소켓을 잘못 만들면 전구를 아무리 사와도 안 들어가요. 그래서 순서가 중요해요. 국제화가 먼저, 지역화가 나중이에요.
57.3 하드코딩이 왜 지옥이 돼요?
국제화 안 된 코드의 가장 흔한 죄가 하드코딩(hardcoding, 문구나 값을 코드 속에 직접 박아 넣기)이에요. 예를 들면 이런 코드요.
상황: 안내 문구를 코드에 그대로 박아 넣었어요.
function showWelcome(user) {
return "안녕하세요, " + user.name + "님!";
}
보기엔 멀쩡하죠. 그런데 영어를 지원하려는 순간 지옥이 열려요. 이 "안녕하세요"가 코드 수백 군데에 흩어져 있거든요. 버튼, 알림, 오류 메시지, 이메일 제목까지요. 언어를 하나 늘릴 때마다 개발자가 코드를 다 뒤져서 if문으로 언어 분기를 치면, 코드는 순식간에 누더기가 돼요.
그래서 국제화의 첫걸음은 문구를 코드에서 뜯어내는 거예요. 문구에 열쇠(key)를 붙여 바깥 사전으로 빼내면 이렇게 돼요.
상황: 문구 대신 열쇠를 쓰고, 실제 문장은 사전에서 꺼내요.
function showWelcome(user) {
return t("welcome", { name: user.name });
}
// 사전(ko): { "welcome": "안녕하세요, {name}님!" }
// 사전(en): { "welcome": "Hello, {name}!" }
이제 언어를 늘려도 코드는 안 건드려요. 사전 파일만 하나 추가하면 되죠. 이 구조를 처음부터 깔아두는 것, 그게 국제화예요. 하드코딩된 문자열은 나중에 반드시 되갚아야 하는 빚이라고 저는 팀에 말해요.
57.4 숫자랑 돈은 왜 나라마다 다르게 보여야 해요?
문구만 문제가 아니에요. 숫자, 돈, 퍼센트도 지역마다 규칙이 달라요. 다행히 자바스크립트에는 이걸 알아서 처리해주는 Intl(International의 줄임, 브라우저와 Node에 기본 내장된 국제화 도구)이 있어요. 직접 만든 예제를 실제로 돌려봤어요.
상황: 같은 금액을 나라별 통화 규칙으로 찍어봐요.
const amount = 1234567.89;
new Intl.NumberFormat("ko-KR", { style: "currency", currency: "KRW" }).format(amount);
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(amount);
new Intl.NumberFormat("hi-IN", { style: "currency", currency: "INR" }).format(amount);
결과:
ko-KR: ₩1,234,568
en-US: $1,234,567.89
de-DE: 1.234.567,89 €
hi-IN: ₹12,34,567.89
보세요. 한국 원화는 소수점이 사라지고, 독일은 기호가 뒤에 붙고 점과 쉼표가 반대예요. 인도는 12,34,567처럼 자릿수 끊는 법 자체가 달라요. 이걸 손으로 if문 짜서 맞추려 했다면 나라 수만큼 버그가 났을 거예요. 규칙을 내가 아는 척하지 말고, Intl에게 맡기는 것이 국제화의 핵심 태도예요.
57.5 언어만 바꾸면 다 되는 거 아니에요?
"그럼 문구랑 숫자만 신경 쓰면 끝이네요?" 아쉽게도 아니에요. 국제화가 챙겨야 할 갈래가 생각보다 많아요. 제가 실무에서 데인 것들만 꼽아볼게요.
날짜와 시간이 있어요. 3/9가 미국에선 3월 9일인데 유럽에선 9월 3일로 읽혀요. 여기에 시간대(timezone)까지 얽히면 같은 순간이 나라마다 다른 날짜로 찍혀요. 복수형(plural)도 골칫거리예요. 영어는 item 하나면 그냥 item, 둘이면 items로 s가 붙는데, 언어마다 규칙이 제각각이에요. 글자 방향도 있어요. 아랍어와 히브리어는 오른쪽에서 왼쪽으로(RTL) 흘러서, 레이아웃을 통째로 뒤집어야 해요.
그 밖에 정렬 순서(같은 알파벳도 언어마다 사전 순서가 다름), 글자 길이(독일어는 단어가 길어서 버튼이 터지고, 중국어는 짧아서 허전함)까지 있어요. 이 갈래들을 이 시리즈에서 하나씩 풀어갈 거예요. 지금은 "번역은 빙산의 일각"이라는 감만 잡으면 충분해요.
57.6 처음부터 국제화를 고려하면 뭐가 달라져요?
제가 가장 강조하고 싶은 건 타이밍이에요. 국제화는 처음에 넣으면 싸고, 나중에 넣으면 비싸요. 이건 제 뼈아픈 경험이에요.
예전에 "일단 한국어로만 빨리 출시하고, 영어는 나중에" 했던 프로젝트가 있었어요. 반년 뒤 영어를 넣으려니, 문구가 코드 천 군데 넘게 박혀 있고, 날짜는 손으로 "2026년 3월"처럼 조립해뒀고, 화면 폭은 한글 길이에만 맞춰져 있었어요. 국제화를 끼워 넣는 데 새 기능 하나 만드는 것보다 더 오래 걸렸어요. 처음부터 t() 함수 하나만 끼고 시작했으면 며칠이면 될 일이었죠.
그래서 지금은 팀에 이렇게 말해요. 당장 한 언어만 쓰더라도, 문구는 사전으로 빼고 숫자와 날짜는 Intl로 찍자. 언어가 하나여도 이렇게 해두면 손해가 없고, 나중에 언어를 늘릴 때 파일 하나 추가로 끝나요. 국제화는 미래의 나에게 주는 선물이에요.
57.7 오늘 내용을 정리해볼게요
국제화가 뭔지 큰 그림을 그렸어요.
국제화(i18n)는 번역보다 넓은 개념이에요. 여러 언어와 지역으로 자연스럽게 보이도록 뼈대를 짜두는 준비가 국제화, 그 자리에 실제 언어를 채우는 게 지역화(l10n)였죠. 가장 큰 적은 하드코딩이라, 문구는 열쇠를 붙여 사전으로 빼고, 숫자와 돈은 규칙을 아는 척하지 말고 Intl에 맡기는 게 핵심이었어요.
그리고 국제화가 챙길 갈래는 문구를 넘어 날짜, 시간대, 복수형, 글자 방향, 정렬까지 넓게 뻗어 있고, 무엇보다 처음에 깔면 싸고 나중에 넣으면 비싸다는 걸 기억하세요. 당장 한 언어만 쓰더라도 사전과 Intl을 깔아두면, 미래의 내가 훨씬 편해져요.