Ver3.5 iOS 국제화와 NSLocalizedString을활용한 다국어 지원 완전 정복 🌍

iOS 국제화와 NSLocalizedString을
활용한 다국어 지원 완전 정복 🌍
야, 솔직히 말해봐. 앱 만들 때 "일단 한국어로만 만들고 나중에 다국어 추가하지 뭐~" 이런 생각 해본 적 있지? 🙋 나도 그랬어. 근데 나중에 다국어 추가하려고 코드 열어보면... 하드코딩된 한국어 문자열이 수백 개야. 그때 느끼는 그 절망감... 😭
오늘은 그런 실수를 처음부터 방지하고, iOS에서 국제화(Internationalization, i18n)와 현지화(Localization, l10n)를 제대로 구현하는 방법을 친구한테 설명하듯 쉽고 재미있게 알려줄게. NSLocalizedString부터 시작해서 Localizable.strings, stringsdict, SwiftUI의 LocalizedStringKey까지 — 전부 다 커버할 거야!
앱스토어에서 글로벌 출시를 꿈꾸는 개발자라면, 이 글 하나로 다국어 지원의 모든 것을 정리할 수 있을 거야. 자, 시작해보자! 🚀
먼저 용어부터 정리하고 가자. 이 두 개념, 헷갈리는 사람 엄청 많아.
국제화(i18n)는 "다국어를 지원할 수 있는 뼈대를 만드는 것"이고,
현지화(l10n)는 "그 뼈대에 실제 번역 살을 붙이는 것"이야.
국제화 없이 현지화는 불가능해. 순서가 중요해!
재밌는 거 알려줄게. i18n은 "internationalization"에서 첫 글자 i와 마지막 글자 n 사이에 글자가 18개라서 i18n이야. 마찬가지로 l10n은 "localization"에서 l과 n 사이에 10개 글자. 개발자들의 귀여운 줄임말 센스 😄
Apple은 iOS 초창기부터 국제화를 굉장히 중요하게 여겼어. Foundation 프레임워크 자체가 국제화를 염두에 두고 설계됐거든. NSLocale, DateFormatter, NumberFormatter 같은 클래스들이 다 그 증거야.
Apple이 제공하는 국제화 도구들을 크게 나누면 이렇게 돼:
| 도구/API | 역할 |
|---|---|
NSLocalizedString |
문자열 현지화의 핵심 매크로 |
Localizable.strings |
번역 문자열 저장 파일 |
stringsdict |
복수형(plural) 처리 파일 |
NSLocale |
지역 설정 정보 접근 |
DateFormatter |
날짜/시간 현지화 |
NumberFormatter |
숫자/통화 현지화 |
Xcode Localization Catalog |
번역 작업 관리 도구 |
NSLocalizedString은 iOS 다국어 지원의 심장이야. 이걸 제대로 이해하면 나머지는 다 따라와. 자, 파헤쳐보자!
가장 기본적인 형태는 이래:
// 기본 형태
let greeting = NSLocalizedString("hello_greeting", comment: "메인 화면 인사말")
// 실제로 이렇게 많이 써
label.text = NSLocalizedString("welcome_message", comment: "환영 메시지")
// Swift에서는 이런 축약형도 자주 사용
let title = NSLocalizedString("nav_title_home", comment: "홈 화면 네비게이션 타이틀")
사실 NSLocalizedString은 함수가 아니라 매크로(Macro)야. Objective-C 시절부터 내려온 매크로인데, Swift에서도 그대로 사용 가능해. 실제로 이렇게 정의돼 있어:
// NSLocalizedString의 실제 정의 (Foundation 프레임워크)
#define NSLocalizedString(key, comment) \
[NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil]
// 더 완전한 버전
func NSLocalizedString(
_ key: String,
tableName: String? = nil,
bundle: Bundle = Bundle.main,
value: String = "",
comment: String
) -> String
NSLocalizedString의 comment 파라미터는 번역가를 위한 설명이야. 코드에서는 무시되지만, genstrings 도구로 .strings 파일을 생성할 때 주석으로 포함돼. 번역가가 맥락을 이해하는 데 엄청 중요하니까 절대 빈 문자열로 두지 마!
| 파라미터 | 타입 | 설명 |
|---|---|---|
key |
String | 번역 파일에서 찾을 키값. 고유해야 함 |
tableName |
String? | 사용할 .strings 파일명 (nil이면 Localizable.strings) |
bundle |
Bundle | 번역 파일이 있는 번들 (기본: mainBundle) |
value |
String | 키를 못 찾았을 때 반환할 기본값 |
comment |
String | 번역가를 위한 컨텍스트 설명 |
// 1. 기본 사용
let text1 = NSLocalizedString("button_ok", comment: "확인 버튼")
// 2. 특정 테이블 지정 (Settings.strings 파일 사용)
let text2 = NSLocalizedString("privacy_title",
tableName: "Settings",
comment: "설정 화면 개인정보 제목")
// 3. 특정 번들 지정 (프레임워크/익스텐션에서 유용)
let text3 = NSLocalizedString("error_message",
bundle: Bundle(for: MyClass.self),
comment: "에러 메시지")
// 4. 기본값 지정 (키 없을 때 폴백)
let text4 = NSLocalizedString("new_feature_title",
value: "새로운 기능",
comment: "새 기능 타이틀")
// 5. 변수 삽입 (String format 활용)
let userName = "김철수"
let template = NSLocalizedString("welcome_user",
comment: "사용자 환영 메시지 (%@는 이름)")
let text5 = String(format: template, userName)
// Localizable.strings: "welcome_user" = "%@님, 환영합니다!";
실무에서는 NSLocalizedString을 매번 쓰기 번거로워서 이런 확장을 많이 써:
// String 확장으로 편하게 사용하기
extension String {
var localized: String {
return NSLocalizedString(self, comment: "")
}
func localized(with arguments: CVarArg...) -> String {
return String(format: self.localized, arguments: arguments)
}
}
// 사용 예시
label.text = "button_ok".localized
label.text = "welcome_user".localized(with: "김철수")
// 또는 enum으로 관리하는 패턴
enum L10n {
enum Button {
static let ok = NSLocalizedString("button_ok", comment: "확인")
static let cancel = NSLocalizedString("button_cancel", comment: "취소")
static let save = NSLocalizedString("button_save", comment: "저장")
}
enum Error {
static let networkFail = NSLocalizedString("error_network",
comment: "네트워크 오류")
}
}
// 사용
button.setTitle(L10n.Button.ok, for: .normal)
String 확장의 localized는 편리하지만, comment가 빈 문자열이 돼서 번역가에게 컨텍스트를 제공하지 못해. 소규모 프로젝트나 혼자 번역할 때는 괜찮지만, 전문 번역 서비스를 이용할 때는 comment를 제대로 채우는 게 좋아.
Localizable.strings는 번역 문자열을 저장하는 핵심 파일이야. 이 파일 구조를 제대로 이해해야 다국어 지원이 매끄럽게 돌아가.
Localizable로 지정
/* Localizable.strings (Korean) */
/* 파일 인코딩: UTF-16 (Xcode 기본) */
/* ===== 공통 버튼 ===== */
"button_ok" = "확인";
"button_cancel" = "취소";
"button_save" = "저장";
"button_delete" = "삭제";
"button_back" = "뒤로";
/* ===== 메인 화면 ===== */
"main_title" = "홈";
"main_welcome" = "안녕하세요!";
"main_subtitle" = "오늘도 좋은 하루 되세요";
/* ===== 포맷 문자열 ===== */
/* %@는 사용자 이름으로 대체됩니다 */
"welcome_user" = "%@님, 환영합니다!";
/* %d는 알림 개수로 대체됩니다 */
"notification_count" = "알림 %d개";
/* ===== 에러 메시지 ===== */
"error_network" = "네트워크 연결을 확인해주세요.";
"error_unknown" = "알 수 없는 오류가 발생했습니다.";
"error_auth_failed" = "인증에 실패했습니다. 다시 시도해주세요.";
/* Localizable.strings (English) */
/* ===== 공통 버튼 ===== */
"button_ok" = "OK";
"button_cancel" = "Cancel";
"button_save" = "Save";
"button_delete" = "Delete";
"button_back" = "Back";
/* ===== 메인 화면 ===== */
"main_title" = "Home";
"main_welcome" = "Hello!";
"main_subtitle" = "Have a great day";
/* ===== 포맷 문자열 ===== */
"welcome_user" = "Welcome, %@!";
"notification_count" = "%d notifications";
/* ===== 에러 메시지 ===== */
"error_network" = "Please check your network connection.";
"error_unknown" = "An unknown error occurred.";
"error_auth_failed" = "Authentication failed. Please try again.";
키 이름을 어떻게 짓느냐에 따라 프로젝트 관리가 천국과 지옥으로 갈려. 실무에서 검증된 패턴들을 소개할게:
| 패턴 | 예시 | 특징 |
|---|---|---|
| 화면_요소 | login_button_title |
화면별 그룹화, 직관적 |
| 카테고리.요소 | button.ok |
계층 구조 표현 (단, 점은 키에 포함됨) |
| SCREENAME_ELEMENT | HOME_TITLE |
대문자 스타일, 명확한 구분 |
| snake_case | home_screen_title |
가장 일반적, 가독성 좋음 ✅ |
팀 프로젝트에서는 반드시 키 네이밍 규칙을 문서화해야 해. 나중에 번역 파일이 수백 줄이 되면 규칙 없이는 관리가 불가능해져. 추천 패턴:
[화면명]_[컴포넌트]_[상태/타입]예:
login_button_submit, profile_label_username, error_alert_network_title
코드에서 NSLocalizedString을 사용한 모든 키를 자동으로 추출해주는 genstrings 명령어가 있어:
# 터미널에서 실행
# 프로젝트 루트 디렉토리에서
find . -name "*.swift" | xargs genstrings -o en.lproj/
# 특정 테이블 지정
find . -name "*.swift" | xargs genstrings -o en.lproj/ -s "NSLocalizedString"
# Objective-C 파일도 포함
find . \( -name "*.swift" -o -name "*.m" \) | xargs genstrings -o en.lproj/
이 명령어를 실행하면 코드에서 사용된 모든 NSLocalizedString 키가 자동으로 Localizable.strings 파일에 추가돼. 번역 작업 시작 전에 꼭 실행해봐!
// 1. Project Navigator에서 프로젝트 파일 선택
// 2. PROJECT 섹션 선택 (TARGETS 아님!)
// 3. Info 탭 클릭
// 4. Localizations 섹션에서 + 버튼 클릭
// 5. 추가할 언어 선택
// 이렇게 하면 자동으로 해당 언어의 .lproj 폴더가 생성돼
// 예: ko.lproj, ja.lproj, zh-Hans.lproj 등
앱 이름, 권한 요청 메시지 같은 시스템 레벨 문자열은 InfoPlist.strings 파일에서 관리해:
/* InfoPlist.strings (Korean) */
/* 앱 이름 현지화 */
"CFBundleDisplayName" = "나의 앱";
/* 카메라 권한 요청 메시지 */
"NSCameraUsageDescription" = "프로필 사진 촬영을 위해 카메라 접근이 필요합니다.";
/* 사진 라이브러리 권한 */
"NSPhotoLibraryUsageDescription" = "사진을 업로드하기 위해 사진 라이브러리 접근이 필요합니다.";
/* 위치 권한 */
"NSLocationWhenInUseUsageDescription" = "주변 가게를 찾기 위해 위치 정보가 필요합니다.";
/* 알림 권한 */
"NSUserNotificationUsageDescription" = "중요한 소식을 알려드리기 위해 알림 권한이 필요합니다.";
권한 요청 메시지(
NSCameraUsageDescription 등)를 현지화하지 않으면 앱스토어 심사에서 거절될 수 있어. 특히 일본, 중국 시장을 타겟으로 한다면 해당 언어로 반드시 번역해서 넣어야 해!
이게 진짜 많은 개발자들이 놓치는 부분이야. "1개", "2개", "3개"는 한국어에서 별 문제가 없어. 근데 영어로 가면? "1 item", "2 items" — 단수/복수가 달라지잖아. 러시아어는 더 복잡해서 1, 2~4, 5~20이 다 달라. 이걸 처리하는 게 .stringsdict 파일이야!
.stringsdict는 XML 기반의 Property List 파일이야:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 키: NSLocalizedString에서 사용할 키 -->
<key>items_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@items@</string>
<key>items</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<!-- 복수형 규칙 -->
<key>zero</key>
<string>항목 없음</string>
<key>one</key>
<string>항목 1개</string>
<key>other</key>
<string>항목 %d개</string>
</dict>
</dict>
</dict>
</plist>
<!-- en.lproj/Localizable.stringsdict -->
<key>items_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@items@</string>
<key>items</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d item</string> <!-- 1 item -->
<key>other</key>
<string>%d items</string> <!-- 2+ items -->
</dict>
</dict>
// stringsdict는 NSLocalizedString으로 그냥 사용하면 돼!
// iOS가 알아서 복수형 규칙을 적용해줌
let count = 5
let format = NSLocalizedString("items_count", comment: "항목 개수")
let result = String(format: format, count)
// 한국어: "항목 5개"
// 영어: "5 items"
// 더 복잡한 예시: 여러 변수 포함
// stringsdict 키: "messages_from_user"
// NSStringLocalizedFormatKey: "%1$@ sent %2$#@messages@"
let userName = "김철수"
let msgCount = 3
let msgFormat = NSLocalizedString("messages_from_user", comment: "")
let msgResult = String(format: msgFormat, userName, msgCount)
// 영어: "김철수 sent 3 messages"
| 언어 | 복수형 카테고리 | 규칙 |
|---|---|---|
| 한국어, 일본어, 중국어 | other만 | 복수형 구분 없음 |
| 영어, 독일어, 스페인어 | one, other | 1개 vs 나머지 |
| 프랑스어 | one, other | 0~1 vs 나머지 |
| 러시아어, 폴란드어 | one, few, many, other | 매우 복잡한 규칙 |
| 아랍어 | zero, one, two, few, many, other | 6가지 형태! |
.stringsdict 파일은 .strings 파일보다 우선순위가 높아. 같은 키가 두 파일에 있으면 .stringsdict가 사용돼. 그리고 .stringsdict에서 zero 카테고리를 지원하지 않는 언어(영어 등)에서는 zero를 정의해도 무시될 수 있어.
SwiftUI는 국제화를 훨씬 더 우아하게 처리해. LocalizedStringKey라는 타입 덕분에 문자열 리터럴이 자동으로 현지화돼. 이게 얼마나 편한지 보여줄게!
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
// 자동으로 Localizable.strings에서 "hello_greeting" 키를 찾아!
Text("hello_greeting")
// 이것도 자동 현지화
Text("welcome_message")
// 변수 삽입도 가능
Text("welcome_user \("김철수")")
// Localizable.strings: "welcome_user %@" = "%@님, 환영합니다!";
// 현지화 비활성화하고 싶을 때
Text(verbatim: "이 텍스트는 번역 안 됨")
}
}
}
// LocalizedStringKey 타입 명시
let key: LocalizedStringKey = "button_ok"
// 커스텀 뷰에서 LocalizedStringKey 받기
struct CustomButton: View {
let title: LocalizedStringKey
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
// 사용
CustomButton(title: "button_ok") {
print("확인 버튼 탭")
}
// String.LocalizationValue (iOS 16+)
// 더 타입 안전한 방식
let greeting = String(localized: "hello_greeting",
comment: "메인 화면 인사말")
Xcode 15부터는 String Catalog라는 새로운 방식이 도입됐어. .xcstrings 파일 하나로 모든 언어의 번역을 관리할 수 있어서 훨씬 편해!
// String Catalog (.xcstrings) 사용 시
// Xcode 15+ 에서 File → New → String Catalog 생성
// 코드에서는 기존과 동일하게 사용
Text("hello_greeting")
NSLocalizedString("hello_greeting", comment: "인사말")
// String Catalog의 장점:
// 1. 모든 언어 번역을 하나의 파일에서 관리
// 2. Xcode UI에서 직접 편집 가능
// 3. 번역 상태 추적 (번역됨/미번역/검토 필요)
// 4. 자동으로 코드에서 키 추출
// 5. 복수형 처리도 UI에서 직접 설정
import SwiftUI
struct LocalizedFormatsView: View {
let date = Date()
let price = 29900.0
let count = 1234567
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// 날짜 자동 현지화
Text(date, style: .date)
// 한국어: "2024년 1월 15일"
// 영어: "January 15, 2024"
// 날짜+시간
Text(date, style: .dateTime)
// 통화 현지화
Text(price, format: .currency(code: "KRW"))
// 한국어: "₩29,900"
// 영어: "₩29,900" (통화 코드 기준)
// 숫자 현지화
Text(count, format: .number)
// 한국어: "1,234,567"
// 독일어: "1.234.567" (점이 천단위 구분자)
}
}
}
SwiftUI의
Text는 문자열 리터럴을 받으면 자동으로 LocalizedStringKey로 처리해. 그래서 Text("button_ok")라고 쓰면 자동으로 현지화된 문자열을 보여줘. 반면 Text(someVariable)처럼 변수를 넣으면 현지화가 안 돼. 이 차이를 꼭 기억해!
텍스트 번역만 하면 끝이라고 생각하면 큰 오산이야. 날짜 표기, 숫자 포맷, 통화 기호 — 이런 것들도 나라마다 다 달라. 이걸 제대로 처리해야 진짜 현지화가 완성돼!
import Foundation
// ❌ 잘못된 방법 - 하드코딩
let badFormatter = DateFormatter()
badFormatter.dateFormat = "yyyy년 MM월 dd일" // 한국어만 됨!
// ✅ 올바른 방법 - 시스템 로케일 사용
let goodFormatter = DateFormatter()
goodFormatter.dateStyle = .long
goodFormatter.timeStyle = .short
// locale은 자동으로 현재 기기 설정 따라감
let date = Date()
let formatted = goodFormatter.string(from: date)
// 한국어 기기: "2024년 1월 15일 오후 3:30"
// 영어 기기: "January 15, 2024 at 3:30 PM"
// 독일어 기기: "15. Januar 2024 um 15:30 Uhr"
// 특정 로케일 강제 지정
let japaneseFormatter = DateFormatter()
japaneseFormatter.locale = Locale(identifier: "ja_JP")
japaneseFormatter.dateStyle = .full
let japaneseDate = japaneseFormatter.string(from: date)
// "2024年1月15日 月曜日"
// 상대적 날짜 표현 (iOS 13+)
let relativeFormatter = RelativeDateTimeFormatter()
relativeFormatter.unitsStyle = .full
let relativeDate = relativeFormatter.localizedString(for: date,
relativeTo: Date())
// "방금 전", "3분 전", "어제" 등 자동 현지화
import Foundation
// 숫자 현지화
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
let number = 1234567.89
let formattedNumber = numberFormatter.string(from: NSNumber(value: number))
// 한국어: "1,234,567.89"
// 독일어: "1.234.567,89" (소수점과 천단위 구분자가 반대!)
// 인도: "12,34,567.89" (인도식 자릿수 구분)
// 통화 현지화
let currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currency
currencyFormatter.currencyCode = "KRW"
let price = 29900.0
let formattedPrice = currencyFormatter.string(from: NSNumber(value: price))
// "₩29,900"
// 퍼센트 현지화
let percentFormatter = NumberFormatter()
percentFormatter.numberStyle = .percent
let ratio = 0.756
let formattedPercent = percentFormatter.string(from: NSNumber(value: ratio))
// 한국어: "75.6%"
// 일부 유럽 언어: "75,6 %" (공백 포함)
// 서수 표현 (iOS 9+)
let ordinalFormatter = NumberFormatter()
ordinalFormatter.numberStyle = .ordinal
let rank = 3
let formattedRank = ordinalFormatter.string(from: NSNumber(value: rank))
// 영어: "3rd"
// 한국어: "3번째"
// 프랑스어: "3e"
import Foundation
// 거리 단위 현지화
let distance = Measurement(value: 5.0, unit: UnitLength.kilometers)
let formatter = MeasurementFormatter()
formatter.unitOptions = .naturalScale
// 미국 로케일 (마일 사용)
formatter.locale = Locale(identifier: "en_US")
print(formatter.string(from: distance))
// "3.107 mi" (자동으로 킬로미터 → 마일 변환!)
// 한국 로케일 (킬로미터 사용)
formatter.locale = Locale(identifier: "ko_KR")
print(formatter.string(from: distance))
// "5 km"
// 온도 단위
let temp = Measurement(value: 100.0, unit: UnitTemperature.celsius)
formatter.locale = Locale(identifier: "en_US")
print(formatter.string(from: temp))
// "212°F" (섭씨 → 화씨 자동 변환!)
미국은 야드파운드법을 사용해. 거리는 마일, 온도는 화씨, 무게는 파운드.
MeasurementFormatter를 사용하면 이런 변환을 자동으로 처리해줘서 정말 편해. 하드코딩으로 단위를 처리하면 미국 사용자들이 엄청 불편해할 거야!
아랍어, 히브리어, 페르시아어는 오른쪽에서 왼쪽으로 읽어. 이걸 RTL(Right-to-Left)이라고 해. 중동 시장을 노린다면 반드시 알아야 해!
좋은 소식이 있어! iOS는 아랍어/히브리어 로케일에서 자동으로 레이아웃을 미러링해줘. Auto Layout을 제대로 사용하면 별도 코드 없이도 RTL이 동작해!
// ✅ RTL 자동 지원 - leading/trailing 사용
view.leadingAnchor.constraint(equalTo: parent.leadingAnchor, constant: 16)
view.trailingAnchor.constraint(equalTo: parent.trailingAnchor, constant: -16)
// LTR: leading = 왼쪽, trailing = 오른쪽
// RTL: leading = 오른쪽, trailing = 왼쪽 (자동 미러링!)
// ❌ RTL 지원 안 됨 - left/right 사용
view.leftAnchor.constraint(equalTo: parent.leftAnchor, constant: 16)
view.rightAnchor.constraint(equalTo: parent.rightAnchor, constant: -16)
// left/right는 항상 물리적 방향이라 RTL에서 미러링 안 됨
// SwiftUI는 기본적으로 RTL 자동 지원
HStack {
Image(systemName: "person.circle")
Text("사용자 이름")
Spacer()
Image(systemName: "chevron.right")
}
// RTL에서는 자동으로 오른쪽부터 배치됨
// 특정 뷰만 RTL 강제 적용
Text("مرحبا")
.environment(\.layoutDirection, .rightToLeft)
// 현재 레이아웃 방향 확인
if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft {
// RTL 처리
}
// 1. 이미지 미러링 - 방향성 있는 아이콘
// SF Symbols는 자동으로 RTL 미러링 지원
Image(systemName: "arrow.right")
// RTL에서 자동으로 "arrow.left"처럼 보임
// 커스텀 이미지 미러링
Image("custom_arrow")
.flipsForRightToLeftLayoutDirection(true)
// 2. 텍스트 정렬
// ❌ 하드코딩
label.textAlignment = .left
// ✅ 자연스러운 정렬 (RTL에서 자동으로 오른쪽 정렬)
label.textAlignment = .natural
// 3. 스택뷰 방향
// UIStackView는 자동으로 RTL 처리
// semanticContentAttribute 설정으로 제어 가능
stackView.semanticContentAttribute = .forceLeftToRight // 강제 LTR
stackView.semanticContentAttribute = .forceRightToLeft // 강제 RTL
stackView.semanticContentAttribute = .unspecified // 자동
이론은 충분히 배웠으니, 이제 실제 프로젝트에서 써먹을 수 있는 실전 팁들을 알려줄게. 재능넷 같은 플랫폼에서 다국어 앱을 개발할 때도 이런 패턴들이 엄청 유용해!
번역하면 텍스트 길이가 달라져. 영어 "OK"가 독일어로 "Bestätigen"이 되는 것처럼. 레이아웃이 이걸 수용해야 해:
// ❌ 고정 너비 버튼 - 번역 텍스트가 잘릴 수 있음
button.frame = CGRect(x: 0, y: 0, width: 80, height: 44)
// ✅ 동적 크기 버튼
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 80).isActive = true
button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
// SwiftUI에서는 기본적으로 동적 크기
Button("button_ok") { }
.padding(.horizontal, 16)
.frame(minWidth: 80)
// 텍스트 길이 변화 대비 팁:
// - 독일어: 영어 대비 약 30% 길어짐
// - 핀란드어: 영어 대비 약 60% 길어짐
// - 중국어/일본어: 영어 대비 약 30% 짧아짐
// → 레이아웃에 최소 40% 여유 공간 확보 권장
// SwiftGen 같은 도구로 타입 안전한 번역 키 생성
// SwiftGen이 자동으로 이런 코드를 생성해줌:
// Generated by SwiftGen
internal enum L10n {
internal enum Button {
internal static let ok = L10n.tr("Localizable", "button.ok")
internal static let cancel = L10n.tr("Localizable", "button.cancel")
}
internal enum Error {
internal static let network = L10n.tr("Localizable", "error.network")
}
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
let format = BundleToken.bundle.localizedString(forKey: key,
value: nil,
table: table)
return String(format: format, locale: Locale.current, arguments: args)
}
}
// 사용 - 오타 없이 안전하게!
label.text = L10n.Button.ok
label.text = L10n.Error.network
// 번역 누락 감지 스크립트 (Python)
// 이런 스크립트를 CI/CD에 추가하면 좋아
/*
import re
import sys
def parse_strings_file(filepath):
keys = set()
with open(filepath, 'r', encoding='utf-16') as f:
content = f.read()
pattern = r'"([^"]+)"\s*='
keys = set(re.findall(pattern, content))
return keys
base_keys = parse_strings_file('en.lproj/Localizable.strings')
ko_keys = parse_strings_file('ko.lproj/Localizable.strings')
missing = base_keys - ko_keys
if missing:
print(f"번역 누락 키: {missing}")
sys.exit(1)
*/
// Xcode에서도 번역 누락 경고 설정 가능
// Build Settings → Missing Localization → Warning
사용자가 앱 내에서 직접 언어를 바꿀 수 있게 하고 싶다면? iOS 13+에서는 시스템 설정에서 앱별 언어 설정이 가능하지만, 앱 내에서 직접 구현하려면 이렇게 해:
// 앱 내 언어 변경 구현
class LanguageManager {
static let shared = LanguageManager()
private let languageKey = "AppLanguage"
var currentLanguage: String {
get { UserDefaults.standard.string(forKey: languageKey) ?? "ko" }
set { UserDefaults.standard.set(newValue, forKey: languageKey) }
}
func setLanguage(_ language: String) {
currentLanguage = language
// Bundle 교체로 언어 변경
Bundle.setLanguage(language)
// 앱 재시작 또는 루트 뷰 재로드 필요
NotificationCenter.default.post(name: .languageChanged, object: nil)
}
}
// Bundle 확장 - 언어 강제 변경
extension Bundle {
static func setLanguage(_ language: String) {
defer {
object_setClass(Bundle.main, PrivateBundle.self)
}
objc_setAssociatedObject(
Bundle.main,
&bundleKey,
language,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
// 사용
LanguageManager.shared.setLanguage("en") // 영어로 변경
LanguageManager.shared.setLanguage("ja") // 일본어로 변경
앱 내 언어 변경은 구현이 복잡하고 버그가 생기기 쉬워. iOS 13+에서는 시스템 설정 → 앱 이름 → 언어에서 앱별 언어를 설정할 수 있어. 가능하면 이 시스템 기능을 활용하고, 앱 내 언어 변경은 정말 필요한 경우에만 구현하는 걸 추천해.
번역 작업을 전문 번역가에게 맡기고 싶다면 Xcode의 Localization Export/Import 기능을 활용해:
// Xcode에서 번역 파일 내보내기
// Product → Export Localizations...
// → .xcloc 파일 생성 (번역가에게 전달)
// 번역 완료 후 가져오기
// Product → Import Localizations...
// → 번역된 .xcloc 파일 선택
// .xcloc 파일 구조
// MyApp.xcloc/
// ├── contents.json
// ├── Localized Contents/
// │ └── ko.xliff ← XLIFF 형식의 번역 파일
// └── Source Contents/
// └── en.xliff ← 원본 영어 파일
// XLIFF는 국제 표준 번역 파일 형식이라
// 대부분의 번역 도구(Transifex, Lokalise 등)에서 지원해
다국어 지원 구현했다고 끝이 아니야. 제대로 동작하는지 테스트해야 해. 이 단계를 건너뛰면 출시 후에 "왜 일본어가 깨져요?" 같은 리뷰를 받게 될 거야 😅
// Xcode에서 특정 언어로 앱 실행하기
// 1. Product → Scheme → Edit Scheme
// 2. Run → Options 탭
// 3. Application Language: 테스트할 언어 선택
// 4. Application Region: 테스트할 지역 선택
// 또는 Launch Arguments로 설정
// -AppleLanguages (ko)
// -AppleLocale ko_KR
// 시뮬레이터에서 언어 변경
// Settings → General → Language & Region
// 특수 테스트 언어들:
// "Double-Length Pseudolanguage" → 텍스트 2배 길이로 레이아웃 테스트
// "Accented Pseudolanguage" → 악센트 문자로 폰트 테스트
// "Bounded String Pseudolanguage" → 문자열 경계 표시
// "Right-to-Left Pseudolanguage" → RTL 레이아웃 테스트
import XCTest
class LocalizationTests: XCTestCase {
// 모든 언어에서 키가 번역되었는지 확인
func testAllKeysTranslated() {
let languages = ["ko", "en", "ja", "zh-Hans"]
let baseBundle = Bundle.main
// 기준 언어(영어)의 모든 키 가져오기
guard let enPath = baseBundle.path(forResource: "Localizable",
ofType: "strings",
inDirectory: "en.lproj"),
let enDict = NSDictionary(contentsOfFile: enPath) else {
XCTFail("영어 번역 파일을 찾을 수 없음")
return
}
for language in languages {
guard let langPath = baseBundle.path(forResource: "Localizable",
ofType: "strings",
inDirectory: "\(language).lproj"),
let langDict = NSDictionary(contentsOfFile: langPath) else {
XCTFail("\(language) 번역 파일 없음")
continue
}
for key in enDict.allKeys {
XCTAssertNotNil(langDict[key],
"\(language)에서 '\(key)' 키 번역 누락")
}
}
}
// 번역 문자열이 비어있지 않은지 확인
func testNoEmptyTranslations() {
let languages = ["ko", "en", "ja"]
for language in languages {
guard let path = Bundle.main.path(forResource: "Localizable",
ofType: "strings",
inDirectory: "\(language).lproj"),
let dict = NSDictionary(contentsOfFile: path) else { continue }
for (key, value) in dict {
if let strValue = value as? String {
XCTAssertFalse(strValue.isEmpty,
"\(language)의 '\(key)' 번역이 비어있음")
}
}
}
}
}
Xcode의 Pseudolanguage 기능은 실제 번역 없이도 국제화 문제를 발견할 수 있게 해줘:
| Pseudolanguage | 용도 | 발견할 수 있는 문제 |
|---|---|---|
| Double-Length | 텍스트 2배 길이 | 레이아웃 잘림, 오버플로우 |
| Accented | 악센트 문자 추가 | 폰트 렌더링, 특수문자 처리 |
| Bounded String | 문자열에 경계 표시 | 하드코딩된 문자열 발견 |
| Right-to-Left | RTL 레이아웃 | left/right 하드코딩 문제 |
이 모드에서 실행하면 현지화된 문자열은
[# 텍스트 #] 형태로 표시되고, 하드코딩된 문자열은 그냥 그대로 보여. 이걸로 현지화 안 된 문자열을 쉽게 찾을 수 있어. 개발 초기에 꼭 한 번씩 돌려봐!
iOS의 Dynamic Type(사용자 글자 크기 설정)과 국제화를 함께 고려해야 해. 특히 일부 언어는 기본 폰트가 다를 수 있어:
// Dynamic Type + 국제화 조합
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true // Dynamic Type 자동 적용
label.numberOfLines = 0 // 여러 줄 허용 (번역 텍스트가 길어질 수 있음)
// SwiftUI에서
Text("welcome_message")
.font(.body) // 자동으로 Dynamic Type 적용
.fixedSize(horizontal: false, vertical: true) // 세로 확장 허용
// 특정 언어에 맞는 폰트 설정
func fontForCurrentLanguage() -> UIFont {
let language = Locale.current.language.languageCode?.identifier ?? "en"
switch language {
case "ja", "zh":
// 일본어/중국어는 시스템 폰트가 적합
return UIFont.systemFont(ofSize: 16)
case "ar":
// 아랍어는 특별한 폰트 고려
return UIFont(name: "GeezaPro", size: 16) ?? UIFont.systemFont(ofSize: 16)
default:
return UIFont.preferredFont(forTextStyle: .body)
}
}
// Asset Catalog에서 이미지 현지화
// Xcode Asset Catalog → 이미지 선택 → Localize 버튼 클릭
// 각 언어별로 다른 이미지 사용 가능
// 코드에서는 그냥 이름으로 사용 (자동으로 현지화된 이미지 로드)
let image = UIImage(named: "promotional_banner")
// 한국어: ko.lproj/promotional_banner.png
// 영어: en.lproj/promotional_banner.png
// 텍스트가 포함된 이미지는 언어별로 다른 이미지 사용 권장
// 예: 앱 소개 배너, 튜토리얼 이미지 등
// 동적으로 언어 확인해서 이미지 선택
func localizedImageName(_ baseName: String) -> String {
let languageCode = Locale.current.language.languageCode?.identifier ?? "en"
let localizedName = "\(baseName)_\(languageCode)"
// 현지화된 이미지가 있으면 사용, 없으면 기본 이미지
if UIImage(named: localizedName) != nil {
return localizedName
}
return baseName
}
앱 자체만 현지화하면 안 돼. 앱스토어 등록 정보도 현지화해야 해:
// App Store Connect에서 현지화할 항목들:
// - 앱 이름 (App Name)
// - 부제목 (Subtitle)
// - 설명 (Description)
// - 키워드 (Keywords) ← 검색 최적화에 매우 중요!
// - 홍보 문구 (Promotional Text)
// - 스크린샷 (Screenshots) ← 각 언어별 스크린샷 권장
// - 미리보기 동영상 (App Preview)
// Fastlane의 deliver 도구로 자동화 가능
// fastlane/metadata/
// ├── ko/
// │ ├── name.txt
// │ ├── description.txt
// │ └── keywords.txt
// ├── en-US/
// │ ├── name.txt
// │ └── description.txt
// └── ja/
// └── ...
규모가 큰 프로젝트라면 번역 일관성을 위해 용어집(Glossary)을 관리해야 해. 예를 들어 "저장"을 어떤 화면에서는 "Save", 어떤 화면에서는 "Store"로 번역하면 안 되잖아:
// 용어집 관리 예시 (JSON 형태)
/*
{
"glossary": [
{
"term": "저장",
"translations": {
"en": "Save",
"ja": "保存",
"zh-Hans": "保存",
"de": "Speichern",
"fr": "Enregistrer"
},
"context": "파일이나 데이터를 저장하는 동작",
"do_not_translate": false
},
{
"term": "앱 이름: MyApp",
"translations": {},
"context": "앱 브랜드명, 번역하지 않음",
"do_not_translate": true
}
]
}
*/
// Lokalise, Transifex, Phrase 같은 번역 관리 플랫폼에서
// 용어집 기능을 제공해. 번역가가 일관된 용어를 사용하도록 강제할 수 있어.
지금까지 배운 내용을 종합해서 간단한 다국어 앱을 처음부터 만들어보자. 이 예제를 따라하면 실제 프로젝트에 바로 적용할 수 있어!
// LocalizationManager.swift
import Foundation
final class LocalizationManager {
static let shared = LocalizationManager()
private init() {}
// 지원 언어 목록
let supportedLanguages: [(code: String, name: String)] = [
("ko", "한국어"),
("en", "English"),
("ja", "日本語"),
("zh-Hans", "简体中文")
]
// 현재 언어 코드
var currentLanguageCode: String {
return Locale.current.language.languageCode?.identifier ?? "en"
}
// 현재 언어 이름
var currentLanguageName: String {
return supportedLanguages.first {
$0.code == currentLanguageCode
}?.name ?? "English"
}
// 번역 함수
func localize(_ key: String,
table: String? = nil,
comment: String = "") -> String {
return NSLocalizedString(key,
tableName: table,
bundle: .main,
value: key,
comment: comment)
}
// 포맷 번역
func localizeFormat(_ key: String,
_ args: CVarArg...) -> String {
let format = localize(key)
return String(format: format, arguments: args)
}
}
// ContentView.swift (SwiftUI)
import SwiftUI
struct ContentView: View {
@State private var userName = ""
@State private var itemCount = 0
var body: some View {
NavigationView {
VStack(spacing: 20) {
// 자동 현지화 텍스트
Text("main_welcome")
.font(.largeTitle)
.fontWeight(.bold)
// 변수 포함 현지화
if !userName.isEmpty {
Text("welcome_user \(userName)")
.font(.title2)
}
// 텍스트 필드
TextField("placeholder_name", text: $userName)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
// 스테퍼
Stepper(value: $itemCount, in: 0...100) {
// stringsdict 복수형 처리
Text(String(format: NSLocalizedString("items_count",
comment: ""),
itemCount))
}
.padding(.horizontal)
// 날짜 현지화
Text(Date(), style: .date)
.foregroundColor(.secondary)
// 통화 현지화
Text(29900.0, format: .currency(code: "KRW"))
.foregroundColor(.secondary)
// 버튼
Button("button_save") {
// 저장 액션
}
.buttonStyle(.borderedProminent)
}
.navigationTitle("main_title")
.navigationBarTitleDisplayMode(.large)
}
}
}
/* ko.lproj/Localizable.strings */
"main_title" = "홈";
"main_welcome" = "안녕하세요! 👋";
"welcome_user %@" = "%@님, 반갑습니다!";
"placeholder_name" = "이름을 입력하세요";
"button_save" = "저장";
/* en.lproj/Localizable.strings */
"main_title" = "Home";
"main_welcome" = "Hello! 👋";
"welcome_user %@" = "Welcome, %@!";
"placeholder_name" = "Enter your name";
"button_save" = "Save";
/* ja.lproj/Localizable.strings */
"main_title" = "ホーム";
"main_welcome" = "こんにちは! 👋";
"welcome_user %@" = "%@さん、ようこそ!";
"placeholder_name" = "名前を入力してください";
"button_save" = "保存";
수많은 iOS 앱을 분석하면서 발견한 국제화 관련 흔한 실수들이야. 이거 보고 미리 방지하자!
// ❌ 절대 이렇게 하면 안 돼!
let message = "안녕하세요, " + userName + "님!"
// 영어로 번역하면 어순이 달라져서 이 방식으로는 불가능
// 영어: "Hello, " + userName + "!" ← 어순은 같지만...
// 일본어: userName + "さん、こんにちは!" ← 어순 다름!
// ✅ 올바른 방법: 포맷 문자열 사용
let format = NSLocalizedString("greeting_with_name",
comment: "%@는 사용자 이름")
let message = String(format: format, userName)
// Localizable.strings:
// "greeting_with_name" = "%@님, 안녕하세요!"; (한국어)
// "greeting_with_name" = "Hello, %@!"; (영어)
// "greeting_with_name" = "%@さん、こんにちは!"; (일본어)
// ❌ 잘못된 방법
let price = 29900
label.text = "\(price)원" // 한국어만 됨
label.text = "$\(price)" // 달러 기호 하드코딩
// ✅ 올바른 방법
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = "KRW"
label.text = formatter.string(from: NSNumber(value: price))
// SwiftUI에서
Text(29900.0, format: .currency(code: "KRW"))
혼자 다 하려고 하지 마. 좋은 도구들이 많아. 이걸 잘 활용하면 개발 시간을 엄청 줄일 수 있어!
| 도구 | 용도 | 특징 |
|---|---|---|
| SwiftGen | 코드 자동 생성 | 타입 안전한 번역 키 생성, 오타 방지 |
| iOSLocalizationEditor | 번역 파일 편집 | 여러 언어를 나란히 편집 가능 |
| Poedit | 번역 편집기 | 번역가 친화적 UI |
| Fastlane | 배포 자동화 | 앱스토어 메타데이터 현지화 자동화 |
| Bartycrouch | 번역 파일 동기화 | 코드에서 키 자동 추출 및 동기화 |
| 플랫폼 | 특징 | 가격 |
|---|---|---|
| Lokalise | Xcode 통합, AI 번역, 팀 협업 | 유료 (무료 플랜 있음) |
| Transifex | 대규모 프로젝트, 번역가 마켓플레이스 | 유료 |
| Phrase | 개발자 친화적, CI/CD 통합 | 유료 |
| Crowdin | 오픈소스 프로젝트 무료, 커뮤니티 번역 | 오픈소스 무료 |
| POEditor | 간단하고 저렴한 번역 관리 | 소규모 무료 |
번역 작업이 필요하다면 재능넷에서 전문 번역가를 찾아볼 수 있어. iOS 앱 번역 경험이 있는 번역가를 찾으면 기술적 맥락을 이해하고 더 정확한 번역을 제공해줄 거야. 특히 일본어, 아랍어 같은 특수 언어는 전문가의 도움이 정말 중요해!
// Apple 공식 문서
// https://developer.apple.com/documentation/xcode/localization
// https://developer.apple.com/documentation/foundation/nslocalizedstring
// WWDC 세션 (강력 추천!)
// WWDC 2021 - "Localize your SwiftUI app"
// WWDC 2022 - "Build global apps: Localization by example"
// WWDC 2023 - "Discover String Catalogs"
// Unicode CLDR (복수형 규칙 참고)
// https://cldr.unicode.org/index/cldr-spec/plural-rules
// iOS Human Interface Guidelines - Localization
// https://developer.apple.com/design/human-interface-guidelines/localization
Xcode 15에서 도입된 String Catalog는 기존 .strings 파일 방식을 완전히 혁신했어. 이게 왜 게임 체인저인지 설명해줄게!
| 항목 | 기존 .strings 방식 | String Catalog (.xcstrings) |
|---|---|---|
| 파일 수 | 언어당 1개 파일 | 모든 언어 1개 파일 |
| 번역 상태 추적 | 수동 확인 | 자동 추적 (번역됨/미번역/검토필요) |
| 키 자동 추출 | genstrings 수동 실행 | Xcode가 자동으로 추출 |
| 복수형 처리 | 별도 .stringsdict 파일 | 같은 파일에서 UI로 설정 |
| 편집 UI | 텍스트 편집기 | 전용 테이블 UI |
| Git 충돌 | 자주 발생 | JSON 기반으로 충돌 감소 |
// 1. File → New → String Catalog 생성
// 파일명: Localizable.xcstrings
// 2. 코드에서는 기존과 동일하게 사용
// NSLocalizedString, Text(), String(localized:) 모두 동일
// 3. Xcode가 자동으로 코드에서 키를 추출해서 카탈로그에 추가
// Build 시 자동으로 동기화됨
// String Catalog의 JSON 구조 (내부적으로 이렇게 저장됨)
/*
{
"sourceLanguage" : "ko",
"strings" : {
"button_ok" : {
"comment" : "확인 버튼",
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "확인"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "OK"
}
},
"ja" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "OK"
}
}
}
}
},
"version" : "1.0"
}
*/
// String Catalog에서 복수형 처리
// Xcode UI에서 직접 설정 가능
// 코드에서는 동일하게 사용
let count = 5
let text = String(localized: "items_count \(count)",
comment: "항목 개수")
기존
.strings 파일을 String Catalog로 마이그레이션하려면:Xcode에서
.strings 파일 우클릭 → Migrate to String Catalog 선택!자동으로 변환해줘서 엄청 편해. 단, Xcode 15 이상에서만 가능해.
자, 오늘 정말 많은 내용을 다뤘어! 처음에는 복잡해 보이지만, 핵심은 간단해:
1. 처음부터 NSLocalizedString 사용 — 나중에 추가하려면 지옥이야
2. comment 파라미터 성실히 작성 — 번역가를 위한 배려
3. 포맷 문자열로 변수 삽입 — 문자열 연결 절대 금지
4. stringsdict로 복수형 처리 — "1 items" 같은 실수 방지
5. leading/trailing 앵커 사용 — RTL 자동 지원
6. DateFormatter/NumberFormatter 활용 — 날짜·숫자 자동 현지화
7. Pseudolanguage 테스트 — 출시 전 반드시 확인
글로벌 앱 시장은 정말 크고, 다국어 지원 하나로 사용자 수가 몇 배로 늘어날 수 있어. 한국어만 지원하는 앱과 5개 언어를 지원하는 앱의 잠재 사용자 수 차이를 생각해봐. 처음부터 제대로 구조를 잡으면 나중에 언어 추가하는 게 정말 쉬워져.
그리고 번역 작업이 필요하다면 재능넷 같은 플랫폼에서 전문 번역가의 도움을 받는 것도 좋은 방법이야. 기계 번역보다 훨씬 자연스럽고 문화적 맥락을 잘 반영한 번역을 받을 수 있거든.
자, 이제 너의 앱을 전 세계로 날려보자! 🌍✈️
관련 키워드
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.

댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.