Flutter 앱에 다국어 골격을 초기에 구축한 이유와 구조
목차
Flutter 앱에 i18n 골격을 처음 박아 넣은 날 작업 회고.
왜 지금 i18n 골격을 잡았나
다국어 지원을 나중에 붙이면 어떻게 되는지는 경험해 본 팀이면 다 안다. 하드코딩된 한국어 문자열이 컴포넌트 전역에 퍼져 있고, 그걸 나중에 긁어내는 작업이 기능 개발보다 더 오래 걸린다. 이번엔 그 상황을 원천 차단하기 위해 기능 개발이 본격화되기 전에 i18n 구조부터 잡았다.
팀에 "지금 당장 영어 서비스 할 거냐"는 질문이 항상 나온다. 대답은 "아직은 아니다". 그런데 그게 이유가 되어서 i18n을 뒤로 미루는 건 팀장 입장에서 동의하기 어렵다. 골격만 잡아두면 이후 신규 문자열을 ARB에 추가하는 건 개발자 입장에서 추가 비용이 거의 없다. 반면 나중에 레트로핏하는 비용은 선형이 아니라 기하급수로 늘어난다.
작업 내용 — 파일별로 뭘 했나
이번 커밋에서 건드린 파일은 총 6개. 크게 세 레이어로 나뉜다.
| 레이어 | 파일 | 역할 |
|---|---|---|
| 설정 | app/l10n.yaml |
flutter gen-l10n 동작 설정 (arb-dir, template-arb-file, output-class 등) |
| 소스 ARB | app_en.arb, app_ko.arb |
실제 번역 문자열 원본. 개발자·번역가가 직접 편집하는 파일 |
| 생성 코드 | app_localizations.dart, app_localizations_en.dart, app_localizations_ko.dart |
flutter gen-l10n이 ARB를 읽고 자동 생성한 Dart 클래스 |
l10n.yaml 하나로 gen 동작을 전부 제어하는 구조라 팀원이 새 ARB 키를 추가할 때 별도 가이드 없이도 따라올 수 있다. 설정 예시는 대략 이런 모양이었다.
# app/l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
template-arb-file을 영어로 잡은 건 의도적인 선택이다. 한국어를 템플릿으로 잡으면 나중에 영어 기준 키 네이밍이 어색해지는 경우가 생긴다. 서비스 언어가 한국어 우선이더라도 키 정의는 영어 ARB 기준으로 가져가는 게 장기적으로 관리하기 편하다.
ARB 파일 자체는 이번엔 골격이라 키가 많지 않다. 앱 이름, 공통 버튼 레이블, 에러 메시지 몇 개 정도. 중요한 건 구조와 컨벤션을 이 시점에 못 박아두는 것이다.
// app_en.arb (예시 패턴)
{
"@@locale": "en",
"appTitle": "My App",
"@appTitle": {
"description": "The title of the application"
},
"commonConfirm": "Confirm",
"commonCancel": "Cancel",
"errorGeneric": "Something went wrong. Please try again."
}
// app_ko.arb (같은 키, 한국어 값)
{
"@@locale": "ko",
"appTitle": "내 앱",
"commonConfirm": "확인",
"commonCancel": "취소",
"errorGeneric": "오류가 발생했습니다. 다시 시도해주세요."
}
생성된 AppLocalizations 클래스는 커밋에 포함은 했지만 .gitignore에 넣을지 여부를 팀 내에서 논의했다. 결론은 포함. CI에서 gen 단계를 별도로 돌리지 않는 이상, 생성 파일을 커밋하지 않으면 클론 직후 빌드가 깨진다. 팀 온보딩 경험을 희생시키면서까지 "generated 파일은 커밋하지 않는다"는 원칙을 고집할 필요는 없다고 판단했다.
코드리뷰 포인트로 잡은 것들
이 골격을 팀에 공유하면서 앞으로 문자열 추가 시 지켜야 할 규칙을 PR 설명에 같이 적었다.
- 하드코딩 문자열 금지: UI 레이어에서 한국어 리터럴이 보이면 리뷰에서 반려
- 키 네이밍 컨벤션:
{도메인}{의미}카멜케이스. 예)loginEmailHint,profileEditTitle @메타데이터 작성: description은 최소한 한 줄. 번역가 입장에서 컨텍스트가 있어야 한다- 플레이스홀더 있는 문자열:
{name}형태로 ARB에 선언하고@블록에placeholders명시
마지막 항목이 은근히 자주 빠진다. 동적 값이 들어가는 문자열을 단순 String.replaceAll로 처리하다가 나중에 복수형이나 성별 처리가 필요해지면 다시 뜯게 된다. 처음부터 ARB 플레이스홀더로 선언해두면 intl 패키지의 Intl.message 레벨 기능을 그대로 쓸 수 있다.
회고
솔직히 이런 "골격 잡기" 작업은 기능 티켓이 아니라서 스프린트 우선순위에서 밀리기 쉽다. 이번에 타이밍을 잡을 수 있었던 건 기능 개발 첫 스프린트 시작 전 버퍼가 있었기 때문이다. 타이밍이 맞았다.
팀원들 입장에서 처음엔 AppLocalizations.of(context)!.commonConfirm 이게 "확인" 한 글자보다 길다는 불만이 나올 수 있다. 근데 그 불만이 나올 시점엔 이미 구조가 팀에 익숙해져 있고, 자동완성이 다 잡아줘서 실제 타이핑 비용은 크지 않다. 첫 주만 넘기면 별 얘기 없어진다. 항상 그랬다.
다음 스텝은 각 화면 개발 시 자연스럽게 ARB 키가 채워지는 것. 별도 i18n 스프린트를 잡을 필요 없이 기능 PR마다 문자열이 ARB에 쌓이는 구조가 되면 이 골격이 제 역할을 다 한 거다.
끝.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.