공용 스크립트 제작기🎈10 - 로컬라이징 시스템 편

Unity 프로젝트의 공용으로 사용할 수 있는 스크립트를 제작하면서 어떻게 만들었는지, 어떤 점에 신경썼는지 설명하는 글입니다.
요즘 게임 제작에서 다국어 지원은 선택이 아닌 필수가 되어가고 있는데, 매번 번역 CSV를 만들어서 다시 시스템을 제작하고 하기보다는 역시 이것도 만들어두고 쓰자 싶어서 하나 만들게 되었다. 이번에 만든 공용 스크립트는 기존에 만들어두었는 CSV 파서 시스템을 기반으로 한다. 다른 시스템들과 마찬가지로 중앙에서 관리하고, 언어 변경 시 자동으로 UI가 갱신된다.
즉, CSV 파일에서 번역을 관리하고, 컴포넌트를 붙이면 자동으로 텍스트가 갱신되며, 에디터에서도 실시간으로 미리보기할 수 있는 시스템이다. 그리고 언어마다 다른 폰트도 자동으로 적용되어야 해서 그 부분까지 신경썼다.
💡예시
게임에 메인 메뉴 버튼이 필요하다고 해보자. 그럴 때 먼저 CSV 파일을 작성한다.
Assets/Data/CSV/LocalizationData.csv:
Key,Korean,English
UI_BTN_START,시작,Start
UI_BTN_OPTIONS,옵션,Options
UI_BTN_QUIT,종료,Quit
UI_SCORE,점수: {0},Score: {0}
그 뒤, Unity 에디터에서 TMP_Text에 LocalizedText 컴포넌트를 추가해주고, Key를 입력하면 끝이다. 그러면 바로 에디터에서 미리보기가 가능하고, 폰트를 설정해두었다면 폰트도 변경된다.
코드에서 직접 조회하는 방법도 있긴 한데 추천은 안한다.
string startText = LocalizationManager.Instance.GetText("UI_BTN_START");
포맷팅의 경우 다음과 같이 사용한다.
public class ScoreUI : MonoBehaviour
{
[SerializeField] private LocalizedText scoreText;
public void UpdateScore(int score)
{
// "점수: {0}" → "점수: 1000"
scoreText.SetFormattedText(score);
}
}
언어를 바꾸고 싶을 땐 한 줄이면 된다.
LocalizationManager.Instance.SetLanguage(LanguageType.English);
📊 클래스 다이어그램
Facade 패턴을 사용해서 사용자에게는 간단한 API를 제공하지만, 내부적으로는 여러 컴포넌트로 분리되어 있다.
LocalizationManager (Facade - EagerSingleton)
├── LocalizationDataProvider (CSV 데이터 관리)
├── LanguagePreferences (언어 설정 관리)
└── LocalizationFontProvider (폰트 관리)
LocalizedText (MonoBehaviour)
LocalizationSettings (ScriptableObject)
LanguageType (Enum)
📁 시스템 아키텍처
핵심 컴포넌트
LocalizationManager (EagerSingleton)
- 역할: 사용자에게 간단한 API를 제공하는 중앙 관리자
- 주요 기능:
- 텍스트 조회 (GetText)
- 포맷팅 지원 (GetText with params)
- 언어 변경 (SetLanguage)
- 현재 언어 조회 (CurrentLanguage)
- 폰트 조회 (GetCurrentFont)
- 언어 변경 이벤트 발행 (OnLanguageChanged)
- 내부 컴포넌트:
- LocalizationDataProvider: CSV 데이터 관리
- LanguagePreferences: 언어 설정 관리
- LocalizationFontProvider: 폰트 관리
- 초기화:
- CSVManager.Initialize() 이후에 호출
- InitializeLocalizeCSVAsync(): 비동기 초기화
LocalizationDataProvider
- 역할: CSV 데이터 로딩 및 텍스트 조회 전담
- 주요 기능:
- BuildLocalizationDictionary(): CSV 데이터를 Dictionary로 캐싱
- GetText(): 언어별로 알맞은 Text를 찾아옴
- missingKeyCache: 누락 키 문자열 재사용
- 에디터 전용 CSV 캐시 (파일 변경 감지)
LanguagePreferences
- 역할: 언어 설정 관리
- 주요 기능:
- LoadLanguage(): PlayerPrefs에서 저장된 언어 로드
- SetLanguage(): 언어 변경 및 PlayerPrefs 저장
- DetectSystemLanguage(): 시스템 언어 자동 감지
LocalizationFontProvider
- 역할: 폰트 로딩 및 제공
- 주요 기능:
- LoadSettingsAsync(): Addressable로 LocalizationSettings 비동기 로드
- GetFont(): 현재 언어에 맞는 폰트 반환
- 에디터 전용 AssetDatabase 로딩
LocalizationSettings (ScriptableObject)
- 역할: 언어별 폰트 설정 (Addressable로 로딩)
- 주요 기능:
- 언어별 TMP 폰트 매핑
- Dictionary 캐싱으로 빠른 조회
LocalizedText (MonoBehaviour 컴포넌트)
- 역할: TMP_Text에 자동으로 번역된 텍스트 표시
- 주요 기능:
- Key만 설정하면 자동으로 번역 텍스트 표시
- 언어 변경 시 자동 갱신 (이벤트 구독)
- 언어별 폰트 자동 적용
- 에디터 실시간 미리보기 (ExecuteAlways)
- 포맷팅 지원 (SetFormattedText)
- 포맷팅은 당연한 얘기지만 다른 스크립트에서 수동으로 넣어줘야한다.
- 수동 갱신 (RefreshText)
- 생명주기:
- Awake: TMP_Text 컴포넌트 캐싱 및 이벤트 구독
- OnValidate: 에디터에서 Key 변경 시 즉시 갱신
- OnLanguageChanged: 언어 변경 이벤트 수신 → 텍스트 및 폰트 갱신
- OnDestroy: 이벤트 구독 해제
LanguageType (Enum)
- 역할: 지원 언어 정의
- 확장:
- Enum에 언어 추가
- CSV에 해당 언어 컬럼 추가
- CSVParser로 LocalizationData 재생성
🔄 데이터 흐름
초기화 단계
- 게임 시작 → CSVManager.Initialize() 호출
↓ - LocalizationManager.InitializeLocalizeCSVAsync() 호출 (비동기)
↓ - [LanguagePreferences.LoadLanguage] 언어 설정 로드
- PlayerPrefs에서 저장된 언어 로드
- 저장된 값이 없으면 DetectSystemLanguage() 호출
- 기타 언어 → LanguageType.English (기본값)
↓
- [LocalizationDataProvider.BuildLocalizationDictionary] CSV 데이터 캐싱
- CSVManager.GetTable() 호출
- Dictionary<string, LocalizationData>로 변환
- 중복 키 검사
↓
- [LocalizationFontProvider.LoadSettingsAsync] 폰트 설정 로드 (비동기)
- AddressableLoader로 LocalizationSettings 로드
- Address: “Assets/Data/Settings/LocalizationSettings.asset”
- 언어별 폰트 Dictionary 빌드
↓
- [OnLanguageChanged 이벤트 발행] 모든 LocalizedText 컴포넌트 자동 갱신
↓ - [완료] LocalizationManager 사용 가능
UI 표시 프로세스 (LocalizedText)
- LocalizedText 컴포넌트가 GameObject에 추가됨
↓ - [Awake] 초기화
- TMP_Text 컴포넌트 캐싱
- OnLanguageChanged 이벤트 구독 (런타임만)
- UpdateText() 호출
- UpdateFont() 호출
↓
- [UpdateText] 텍스트 갱신
- GetLocalizationValue 헬퍼로 환경 분기
- 에디터 모드: GetTextInEditor(key) → CSV 캐시에서 로드
- 런타임 모드: GetText(key) → Dictionary 조회
- TMP_Text.text에 적용
↓
- [UpdateFont] 폰트 적용
- 에디터 모드: GetCurrentFontInEditor() → AssetDatabase 로드
- 런타임 모드: GetCurrentFont() → LocalizationSettings에서 조회
- TMP_Text.font에 적용
↓
- [완료] UI에 번역된 텍스트 및 폰트 표시
언어 변경 프로세스
- SetLanguage(LanguageType) 호출
↓ - [중복 체크] 같은 언어면 무시
↓ - [LanguagePreferences.SetLanguage] 언어 변경 및 저장
- currentLanguage 업데이트
- PlayerPrefs.SetInt(LANGUAGE_PREFS_KEY, (int)language)
- PlayerPrefs.Save()
↓
- [이벤트 발행] OnLanguageChanged?.Invoke(language)
↓ - [구독자 갱신] 모든 LocalizedText 컴포넌트
- OnLanguageChanged 핸들러 실행
- UpdateText() → 새 언어의 텍스트로 갱신
- UpdateFont() → 새 언어의 폰트로 갱신
↓
- [완료] 모든 UI가 새 언어로 갱신됨
에디터 미리보기 프로세스
- Inspector에서 LocalizedText.Key 변경
↓ - [OnValidate] Unity가 자동 호출
- lastValidatedKey와 비교하여 실제 변경 여부 확인
↓
- lastValidatedKey와 비교하여 실제 변경 여부 확인
- [UpdateText] 텍스트 갱신
- GetTextInEditor(key) 호출
- CSV 캐시 확인 (LastWriteTime 비교)
- 파일이 변경되었거나 캐시가 없으면 CSV 파일 로드
- 헤더 파싱 → Key 및 언어 컬럼 인덱스 찾기
- 데이터 행 검색 → 해당 Key의 텍스트 반환
- TMP_Text.text에 적용
↓
- [UpdateFont] 폰트 갱신
- GetCurrentFontInEditor() 호출
- AssetDatabase.LoadAssetAtPath로 LocalizationSettings 로드
- GetEditorLanguage()로 에디터 언어 감지
- 해당 언어의 폰트 반환
- TMP_Text.font에 적용
↓
- [완료] 에디터에서 즉시 미리보기 확인!
🎯 신경 쓴 부분
에디터 실시간 미리보기
ExecuteAlways + OnValidate로 에디터에서 즉시 확인
[ExecuteAlways] // 에디터 모드에서도 실행
public class LocalizedText : MonoBehaviour
{
private void OnValidate()
{
// Key 변경 시 즉시 텍스트 업데이트
if (!Application.isPlaying)
{
string text = LocalizationManager.Instance.GetTextInEditor(key);
this.text.text = text;
}
}
}
- 코드 수정 없이 Inspector에서 Key만 입력
- 저장하지 않아도 즉시 미리보기
- 번역 오타를 바로 확인 가능
자동 폰트 시스템 (Addressable + ScriptableObject)
언어마다 다른 폰트를 자동으로 적용하며, Addressable로 비동기 로딩한다.
// LocalizationSettings.cs (ScriptableObject)
public TMP_FontAsset GetFont(LanguageType language)
{
if (fontDict.TryGetValue(language, out var font))
{
return font;
}
return null;
}
- LocalizationSettings를 Addressable로 비동기 로딩
- 에디터: AssetDatabase 동기 로딩 (미리보기용)
- 런타임: AddressableLoader 비동기 로딩
- LocalizedText가 자동으로 폰트 적용
- 언어 변경 시 텍스트와 함께 폰트도 갱신
- 별도 코드 작성 불필요
포맷팅 지원
동적 값을 삽입할 수 있는 string.Format 지원
// CSV: "점수: {0}"
public string GetText(string key, params object[] args)
{
string format = GetText(key);
try
{
return string.Format(format, args);
}
catch (FormatException e)
{
Debug.LogError($"포맷 오류: {key}\n{e.Message}");
return format;
}
}
- 점수, 레벨, 아이템 개수 등 동적 데이터 표시
- LocalizedText.SetFormattedText()로 간편 사용
- 에러 처리로 안전성 보장
에디터 전용 CSV 캐시 시스템
CSV 파일 변경을 감지하여 자동으로 캐시 갱신
private bool HasCSVFileChanged()
{
var fileInfo = new FileInfo(csvPath);
if (fileInfo.LastWriteTime != lastCsvModifiedTime)
{
lastCsvModifiedTime = fileInfo.LastWriteTime;
return true;
}
return false;
}
- LastWriteTime으로 파일 변경 감지
- 변경 시에만 CSV 파일 재로드
- 불필요한 파일 읽기 방지
- 에디터 성능 향상
결론
컴포넌트만 붙이면 자동으로 텍스트와 폰트가 갱신되니 편하다. 처음엔 단순하게 만들었는데, 점점 리팩토링하면서 Facade 패턴으로 내부 구조를 분리했다. 에디터에서는 CSV 파일 변경 감지 캐시 시스템으로 불필요한 파일 읽기를 방지했고, Addressable로 폰트를 비동기 로딩하도록 개선했다.
여기에 욕심 좀 내자면 Text UI의 width/height가 길이에 따라 자동조절되고, 줄바꿈이 자유로웠으면 좋겠다는 점인데 이거는 로컬라이징이랑은 별개니까 다음 기회에 해보는걸로.