오늘날 글로벌 시장에서 모바일 애플리케이션의 성공 여부는 단순히 뛰어난 기능을 제공하는 것을 넘어, 전 세계 다양한 문화권의 사용자들에게 얼마나 깊이 있는 공감대와 직관적인 사용성을 제공하느냐에 달려 있습니다. 이 거대한 흐름의 중심에는 '국제화(Internationalization, i18n)'와 '현지화(Localization, l10n)'라는 두 가지 핵심 기술이 자리 잡고 있습니다. Flutter는 이 두 가지 과제를 해결하기 위해 매우 강력하고 체계적인 다국어 처리 시스템을 제공하며, 개발자가 글로벌 사용자를 효과적으로 공략할 수 있도록 돕습니다. 이 글에서는 Flutter의 다국어 처리 시스템을 기초부터 프로덕션 수준의 고급 전략까지 심도 있게 파헤쳐 보겠습니다. 단순한 텍스트 번역을 넘어, 사용자의 마음에 진정으로 와닿는 현지화 경험을 구현하는 구체적인 방법과 철학을 제시합니다.
국제화(i18n)와 현지화(l10n), 개념부터 바로잡기
본격적인 구현에 앞서, 종종 혼용되어 사용되지만 실제로는 명확히 구분되는 두 가지 핵심 개념인 국제화와 현지화를 정확히 이해하는 것이 무엇보다 중요합니다. 이 둘의 관계를 올바르게 정립하는 것이 성공적인 글로벌 앱 개발의 첫걸음입니다.
- 국제화 (Internationalization, i18n): 이름에서 알 수 있듯('i'와 'n' 사이에 18개의 글자), 국제화는 애플리케이션의 '구조'를 특정 언어나 문화권에 종속되지 않도록 설계하는 엔지니어링 과정입니다. 이것은 건축의 '설계도'와 같습니다. 어떤 나라의 가구나 가전제품이 들어와도 수용할 수 있도록 유연한 구조를 만드는 작업이죠. 코드에 텍스트를 직접 하드코딩하는 대신 외부 리소스 파일에서 동적으로 불러오도록 설계하는 것, 날짜, 시간, 숫자, 통화 형식이 지역마다 다르다는 사실을 인지하고 이를 처리할 수 있는 기반을 마련하는 것 등이 모두 국제화에 해당합니다. 즉, 현지화를 위한 준비 작업이며, 한 번 잘 구축해두면 새로운 언어를 추가하는 비용을 획기적으로 줄여줍니다.
- 현지화 (Localization, l10n): 'l'과 'n' 사이에 10개의 글자가 있는 이 용어는, 국제화된 앱을 특정 지역 또는 언어(로케일, Locale)에 맞게 '적응'시키는 구체적인 실행 과정입니다. 설계도가 완성된 집에 각 나라의 문화에 맞는 가구와 벽지를 채워 넣는 '인테리어' 작업에 비유할 수 있습니다. 여기에는 텍스트 번역, 현지 문화에 맞는 이미지나 아이콘 교체, 해당 지역의 표준에 맞는 날짜/통화 형식 적용, 심지어는 법률적 요구사항을 반영하는 작업까지 포함됩니다. 목표는 단순히 정보를 전달하는 것을 넘어, 사용자가 "이 앱은 처음부터 우리를 위해 만들어졌구나"라고 느끼게 만드는 것입니다.
결론적으로, 국제화는 '가능성'을 만드는 과정이고, 현지화는 그 가능성을 '현실'로 만드는 과정입니다. 견고한 국제화 아키텍처 없이는 효율적이고 일관된 현지화가 불가능하며, 이 둘의 조화가 글로벌 시장에서의 성공을 좌우합니다.
금융, 건강, 법률 등 사용자의 삶에 중대한 영향을 미치는 YMYL 분야의 앱에서는 정확하고 신뢰도 높은 현지화가 단순한 사용자 경험을 넘어 법적, 윤리적 책임과 직결됩니다. 잘못된 통화 기호, 오해의 소지가 있는 법률 용어 번역은 심각한 문제를 야기할 수 있습니다. 따라서 이러한 앱일수록 단순 기계 번역을 넘어 전문적인 검수를 거친 고품질의 현지화가 필수적입니다.
| 구분 | 국제화 (i18n) | 현지화 (l10n) |
|---|---|---|
| 목표 | 다양한 언어와 지역을 지원할 수 있는 유연한 소프트웨어 아키텍처 설계 | 특정 언어와 문화권 사용자에게 최적화된 경험 제공 |
| 단계 | 개발 초기 단계 (설계, 아키텍처링) | 개발 이후 또는 동시 진행 단계 (콘텐츠 적응) |
| 주요 작업 | - UI에서 텍스트 분리 - 시간대, 통화, 숫자 형식 처리 기반 마련 - 오른쪽에서 왼쪽으로 쓰는(RTL) 언어 지원 설계 |
- 텍스트 번역 - 이미지, 아이콘 등 리소스 교체 - 날짜/시간/숫자 형식 지정 - 문화적 뉘앙스 및 관용구 반영 |
| 관련 담당자 | 소프트웨어 개발자, 아키텍트 | 번역가, 현지화 전문가, UX/UI 디자이너, QA 테스터 |
| 비유 | 건물의 골격과 배관 설계 | 각 세대의 인테리어 및 가구 배치 |
Flutter 다국어 처리의 핵심 아키텍처
Flutter는 공식적으로 지원하는 도구와 패키지를 통해 매우 체계적인 다국어 처리 파이프라인을 제공합니다. 이 시스템의 핵심 구성요소와 데이터 흐름을 이해하면 전체 과정을 더욱 명확하게 파악할 수 있습니다.
flutter_localizations패키지: Flutter SDK에 기본적으로 포함되어 있으며, Material 및 Cupertino 위젯들이 필요로 하는 기본적인 현지화 값들(예: '취소', '확인' 버튼 텍스트, 날짜 선택기의 요일 등)을 제공합니다. 우리가 직접 모든 것을 번역할 필요 없이, Flutter가 기본 UI 요소들의 현지화를 책임지도록 해줍니다.intl패키지: Dart 팀에서 공식적으로 제공하는 국제화 라이브러리의 핵심입니다. 개발자가 직접 작성하는 애플리케이션 고유의 텍스트, 날짜/숫자 포매팅, 복수형 처리 등 복잡한 국제화 기능을 위한 강력한 API를 제공합니다.- ARB (Application Resource Bundle) 파일:
.arb확장자를 가진 JSON 형식의 파일로, 현지화할 문자열을 '키-값' 형태로 저장하는 곳입니다. 각 언어별로 별도의 ARB 파일이 존재하며, 번역가와의 협업을 위한 표준화된 형식을 제공합니다. JSON 기반이므로 사람이 읽고 쓰기 쉬우며, 다양한 도구에서 지원된다는 장점이 있습니다. flutter gen-l10n코드 생성 도구: Flutter SDK에 내장된 이 도구는 ARB 파일의 내용을 읽어 Dart 코드를 자동으로 생성하는 마법사 역할을 합니다. 이 도구 덕분에 우리는 ARB 파일에 정의된 키를 타입-세이프(type-safe)한 Dart 클래스의 메서드나 게터(getter)로 편리하게 접근할 수 있으며, 오타로 인한 런타임 에러를 컴파일 시점에 방지할 수 있습니다.
데이터 흐름 요약:
개발자는.arb파일에 번역할 텍스트를 정의합니다 (Source of Truth). →flutter gen-l10n명령어를 실행하면 → Flutter 빌드 시스템이 이 파일을 분석하여AppLocalizations라는 Dart 클래스를 자동 생성합니다. → 개발자는MaterialApp에 이 클래스를 사용하도록 설정하고 → UI 코드 내에서AppLocalizations.of(context)를 통해 현재 언어에 맞는 텍스트를 안전하고 편리하게 가져와 사용합니다.
1단계: 견고한 다국어 환경 구축하기
이론을 바탕으로, 이제 실제 프로젝트에 다국어 처리 환경을 구축하는 구체적인 단계를 밟아보겠습니다. 초기 설정이 견고할수록 프로젝트가 커져도 유지보수가 쉬워집니다.
1.1. 프로젝트 설정 및 의존성 추가
가장 먼저, 프로젝트가 다국어 처리에 필요한 패키지들을 인식하고 관련 도구를 활성화하도록 pubspec.yaml 파일을 수정해야 합니다.
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
# 1. Material & Cupertino 위젯의 기본 현지화 값을 제공합니다.
# 예: Calendar 위젯의 '월', '화' 같은 텍스트
flutter_localizations:
sdk: flutter
# 2. 다국어 메시지, 날짜/숫자 포매팅 등 국제화 핵심 기능을 제공합니다.
# 항상 최신 버전을 확인하고 사용하는 것이 좋습니다. (https://pub.dev/packages/intl)
intl: ^0.18.1
# ... 기타 의존성
# flutter 섹션에 generate: true를 추가하여 코드 생성 기능을 활성화합니다.
flutter:
uses-material-design: true
# 3. 이 설정이 Flutter의 최신 l10n 도구를 활성화하는 핵심입니다.
generate: true
여기서 generate: true 설정은 매우 중요합니다. 이 옵션이 Flutter의 빌드 시스템에게 ARB 파일을 감시하고 변경이 있을 때마다 자동으로 Dart 코드를 생성하도록 지시합니다. 과거에는 별도의 스크립트를 실행해야 했지만, 이 설정 덕분에 개발 과정이 훨씬 간소화되었습니다.
파일을 수정한 후, 터미널에서 다음 명령을 실행하여 패키지를 프로젝트에 설치합니다.
flutter pub get
1.2. l10n.yaml 구성 파일 상세 분석
다음으로, 코드 생성 도구에게 "어디에 있는 ARB 파일을 읽어서", "어떤 이름의 Dart 파일로 만들지" 알려주는 설정 파일이 필요합니다. 프로젝트 루트 디렉터리에 l10n.yaml 파일을 생성하고 아래 내용을 작성합니다.
# l10n.yaml
# 다국어 리소스 파일(.arb)들이 위치할 디렉터리를 지정합니다.
# 관례적으로 'lib/l10n'을 많이 사용합니다.
arb-dir: lib/l10n
# 모든 언어 파일의 기준이 되는 템플릿 ARB 파일을 지정합니다.
# 여기에 정의된 키를 기준으로 다른 언어 파일의 누락된 키를 검사합니다.
# 일반적으로 앱의 기본 개발 언어(예: 영어 'en' 또는 한국어 'ko')로 지정합니다.
template-arb-file: app_ko.arb
# 자동 생성될 현지화 관련 Dart 파일의 이름을 지정합니다.
# 이 파일에는 'AppLocalizations' 클래스가 포함됩니다.
output-localization-file: app_localizations.dart
# (선택 사항) 생성될 클래스의 이름을 커스터마이징할 수 있습니다.
# 기본값은 'AppLocalizations' 입니다.
# output-class: L10n
# (선택 사항) Getter가 null을 반환할 수 있도록 할지 결정합니다.
# true로 설정하면 AppLocalizations.of(context)가 nullable이 됩니다.
# 특별한 이유가 없다면 기본값인 false를 유지하는 것이 좋습니다.
# nullable-getter: false
이 파일은 다국어 처리 시스템의 '설정 센터' 역할을 합니다. 프로젝트의 규모가 커지거나 특정 규칙을 적용하고 싶을 때 이 파일을 수정하여 코드 생성 동작을 제어할 수 있습니다.
1.3. ARB 파일 작성의 기술: 단순 번역을 넘어서
이제 실제 번역 텍스트를 담을 ARB 파일을 작성할 차례입니다. l10n.yaml에서 지정한 lib/l10n 디렉터리를 생성하고, 그 안에 템플릿 파일(app_ko.arb)과 지원할 다른 언어 파일(app_en.arb, app_ja.arb 등)을 만듭니다.
ARB 파일은 단순한 '키-값' 저장을 넘어, 번역가와의 효율적인 협업과 복잡한 언어 규칙 처리를 위한 강력한 기능들을 제공합니다.
lib/l10n/app_ko.arb (한국어 - 템플릿 파일)
{
"@@locale": "ko",
"@@last_modified": "2025-11-17T14:30:00.000Z",
"pageHomeTitle": "홈 화면",
"@pageHomeTitle": {
"description": "홈 화면의 앱바에 표시되는 제목 텍스트입니다."
},
"welcomeMessage": "안녕하세요, {userName}님!",
"@welcomeMessage": {
"description": "사용자의 이름을 포함하여 보여주는 환영 메시지입니다.",
"placeholders": {
"userName": {
"type": "String",
"example": "홍길동"
}
}
},
"itemCount": "{count,plural, =0{아이템이 없습니다.} =1{아이템이 1개 있습니다.} other{아이템이 {count}개 있습니다.}}",
"@itemCount": {
"description": "인벤토리의 아이템 개수에 따라 다른 메시지를 표시합니다.",
"placeholders": {
"count": {
"type": "int",
"format": "compact"
}
}
},
"userStatus": "{gender,select, male{그는 온라인 상태입니다.} female{그녀는 온라인 상태입니다.} other{그들은 온라인 상태입니다.}}",
"@userStatus": {
"description": "사용자의 성별에 따라 다른 상태 메시지를 보여줍니다.",
"placeholders": {
"gender": {
"type": "String"
}
}
}
}
lib/l10n/app_en.arb (영어)
{
"@@locale": "en",
"pageHomeTitle": "Home Screen",
"welcomeMessage": "Hello, {userName}!",
"itemCount": "{count,plural, =0{There are no items.} =1{There is one item.} other{There are {count} items.}}",
"userStatus": "{gender,select, male{He is online.} female{She is online.} other{They are online.}}"
}
ARB 파일 작성 핵심 포인트
@@locale: 이 파일이 어떤 로케일을 위한 것인지 명시하는 필수 메타데이터입니다.@key: 키에 대한 메타데이터 블록입니다.description은 번역가에게 문맥을 설명하는 데 필수적입니다. "Save"라는 단어가 '저장하다'인지 '구하다'인지 알려주는 역할을 합니다.placeholders정보는 번역 도구가 변수 타입을 인지하고 검증하는 데 도움을 줍니다.{placeholderName}: 코드에서 동적으로 전달할 값을 위한 변수입니다. 생성된 Dart 코드에서는 함수의 파라미터가 됩니다.- ICU Message Format:
{변수, 타입, 조건}형태로 작성되는 강력한 기능입니다.plural: 수량에 따른 복수형 처리. 영어는 단수/복수만 있지만, 폴란드어, 러시아어 등은 더 복잡한 복수형 규칙을 가집니다. ICU 형식을 사용하면 이러한 언어 규칙을 ARB 파일 내에서 선언적으로 처리할 수 있습니다.select: 특정 값(주로 문자열)에 따라 다른 메시지를 선택합니다. 성별, 상태 등 분기가 필요한 경우에 매우 유용합니다.
2단계: 자동 생성 코드와 애플리케이션 통합
리소스 파일 작성이 완료되었다면, 이제 Flutter의 자동화 도구를 사용하여 이 리소스들을 코드에서 안전하게 사용할 수 있는 형태로 변환하고 애플리케이션에 통합할 차례입니다.
2.1. `flutter gen-l10n` 명령어의 마법
ARB 파일을 생성하거나 수정한 뒤 파일을 저장하면, VS Code나 Android Studio의 Flutter 플러그인이 변경을 감지하고 자동으로 코드 생성 프로세스를 실행합니다. 만약 자동으로 실행되지 않거나 수동으로 실행하고 싶다면, 프로젝트 터미널에서 다음 명령을 입력하면 됩니다.
flutter gen-l10n
이 명령이 성공적으로 실행되면, .dart_tool/flutter_gen/gen_l10n/ 디렉터리 내에 l10n.yaml에서 설정한 app_localizations.dart 파일과 언어별 구현 파일들(app_localizations_ko.dart, app_localizations_en.dart 등)이 생성됩니다.
주의:.dart_tool디렉터리는 빌드 과정에서 생성되는 임시 파일들을 담는 곳입니다. 따라서 이 디렉터리 안의 파일들을 직접 수정해서는 안 되며, 버전 관리 시스템(Git 등)에서도 무시(.gitignore)해야 합니다. 모든 변경은 원본인.arb파일에서 이루어져야 합니다.
생성된 app_localizations.dart 파일을 살짝 들여다보면, ARB 파일의 키들이 어떻게 Dart 코드로 변환되었는지 알 수 있습니다.
- `pageHomeTitle` → `String get pageHomeTitle` (게터)
- `welcomeMessage` → `String welcomeMessage(String userName)` (메서드)
- `itemCount` → `String itemCount(int count)` (메서드)
- `userStatus` → `String userStatus(String gender)` (메서드)
2.2. MaterialApp 설정: 앱의 언어 두뇌 만들기
생성된 코드를 앱 전체에서 사용하려면, 애플리케이션의 최상위 위젯인 MaterialApp(또는 CupertinoApp)에 현지화 관련 설정을 주입해야 합니다. 이 설정은 앱이 어떤 언어를 지원하는지, 그리고 해당 언어 데이터를 어떻게 로드할지 알려주는 역할을 합니다.
main.dart 파일을 열고 MaterialApp 위젯을 다음과 같이 수정합니다.
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
// 자동 생성된 파일을 import 합니다. 경로가 정해져 있습니다.
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Localization Demo', // 이 title은 OS 레벨에서 사용되므로, 현지화가 복잡합니다.
// 1. 현지화 데이터 로더(Delegate) 목록 설정
localizationsDelegates: const [
// 개발자가 작성한 ARB 파일 기반의 현지화를 위한 델리게이트
AppLocalizations.delegate,
// Material 위젯의 기본 텍스트(e.g., 'OK', 'Cancel') 현지화
GlobalMaterialLocalizations.delegate,
// 위젯의 텍스트 방향(LTR/RTL) 등 일반적인 현지화
GlobalWidgetsLocalizations.delegate,
// Cupertino(iOS 스타일) 위젯의 기본 텍스트 현지화
GlobalCupertinoLocalizations.delegate,
],
// 2. 앱이 지원하는 언어 목록 선언
supportedLocales: const [
Locale('ko'), // 한국어, 국가 코드 없음
Locale('en'), // 영어, 국가 코드 없음
// 예: Locale('en', 'US') -> 미국식 영어
],
// 3. (권장) 로케일 결정 로직 커스터마이징
localeResolutionCallback: (locale, supportedLocales) {
// 사용자의 기기 로케일이 null이 아닌 경우
if (locale != null) {
for (var supportedLocale in supportedLocales) {
// 기기의 언어 코드가 지원 목록에 있는지 확인
if (supportedLocale.languageCode == locale.languageCode) {
// 국가 코드까지 일치하는 경우 (e.g., en_US)가 있다면 우선적으로 사용하고,
// 없다면 언어 코드만 일치하는 것(e.g., en)을 사용합니다.
// 이 예제에서는 언어 코드만 비교하여 단순화했습니다.
return supportedLocale;
}
}
}
// 기기 로케일을 지원하지 않거나, locale이 null인 경우
// 지원 목록의 첫 번째 언어(여기서는 'ko')를 기본값으로 사용
return supportedLocales.first;
},
home: const MyHomePage(),
);
}
}
위 설정은 앱이 시작될 때 사용자의 기기 언어 설정을 확인하고, 우리가 supportedLocales에 정의한 언어 중 일치하는 것이 있으면 해당 언어로 앱을 표시합니다. 만약 일치하는 언어가 없으면 지정된 기본값(여기서는 한국어)으로 앱을 보여줍니다.
3단계: UI에 생명 불어넣기 (위젯에서 리소스 사용)
모든 설정이 완료되었습니다. 이제 UI 코드에서 실제로 현지화된 문자열을 가져와 사용하는 방법을 알아봅니다. Flutter의 위젯 시스템 덕분에 이 과정은 매우 직관적입니다.
3.1. 기본 접근법: `AppLocalizations.of(context)`
AppLocalizations 객체는 BuildContext를 통해 접근할 수 있습니다. 위젯 트리 상에서 현재 위젯의 위치(context)를 기준으로 가장 가까운 Localizations 위젯을 찾아 해당 로케일에 맞는 AppLocalizations 인스턴스를 반환해 줍니다.
// home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
String _gender = 'other';
void _incrementCounter() {
setState(() {
_counter++;
});
}
void _changeGender() {
setState(() {
if (_gender == 'male') _gender = 'female';
else if (_gender == 'female') _gender = 'other';
else _gender = 'male';
});
}
@override
Widget build(BuildContext context) {
// 1. 현재 context에 맞는 AppLocalizations 인스턴스를 가져옵니다.
// MaterialApp 하위에서는 null이 될 수 없으므로 `!`를 사용하여 non-nullable로 만듭니다.
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// 2. 'pageHomeTitle' 키에 해당하는 문자열 사용
title: Text(l10n.pageHomeTitle),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 3. 'welcomeMessage' 키에 해당하는 문자열 사용 (플레이스홀더 포함)
Text(
l10n.welcomeMessage('개발자'),
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 20),
// 4. 'itemCount' 키에 해당하는 복수형 문자열 사용
Text(
l10n.itemCount(_counter),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 20),
// 5. 'userStatus' 키에 해당하는 select 형식 문자열 사용
Text(
l10n.userStatus(_gender),
style: Theme.of(context).textTheme.bodyLarge,
),
ElevatedButton(
onPressed: _changeGender,
child: Text('성별 변경'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: '증가', // Tooltip도 당연히 현지화 대상이 되어야 합니다.
child: const Icon(Icons.add),
),
);
}
}
3.2. 코드 가독성 향상: `extension` 활용법 (Best Practice)
모든 위젯에서 `AppLocalizations.of(context)!`를 반복적으로 작성하는 것은 번거롭고 코드를 지저분하게 만들 수 있습니다. Dart의 강력한 기능인 extension을 사용하면 이 과정을 훨씬 더 간결하고 우아하게 만들 수 있습니다.
프로젝트의 공통 유틸리티 폴더(예: `lib/extensions`)에 다음과 같은 extension 파일을 생성합니다.
// lib/extensions/localization_extension.dart
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension LocalizationExtension on BuildContext {
// 이제 context.l10n 으로 바로 접근 가능
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
이제 위젯 파일에서 이 extension 파일을 import하기만 하면, `context.l10n`이라는 짧은 코드로 `AppLocalizations` 인스턴스에 즉시 접근할 수 있습니다.
// home_page.dart (수정)
// ...
// extension 파일을 import 합니다.
import 'package:your_app_name/extensions/localization_extension.dart';
// ... build 메서드 내부
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 훨씬 간결하고 읽기 쉬워진 코드
title: Text(context.l10n.pageHomeTitle),
),
body: Center(
child: Column(
// ...
Text(context.l10n.welcomeMessage('개발자')),
Text(context.l10n.itemCount(_counter)),
// ...
),
),
// ...
);
}
이 방법은 코드의 가독성을 크게 향상시키고, 보일러플레이트 코드를 줄여주므로 모든 Flutter 프로젝트에 적용하는 것을 강력히 권장합니다.
3.3. `BuildContext`가 없는 곳에서 문자열 사용하기 (심화)
ViewModel, BLoC, Service 클래스 등 위젯 트리에 직접적으로 속하지 않는 곳에서는 `BuildContext`에 접근할 수 없습니다. 이런 경우 현지화된 문자열은 어떻게 가져와야 할까요? 몇 가지 해결책이 있습니다.
- UI 레이어에서 문자열 전달하기 (권장): 가장 깨끗하고 테스트하기 쉬운 방법입니다. 비즈니스 로직(ViewModel, BLoC)은 로직 처리와 상태 관리만 담당하고, UI에 필요한 문자열은 위젯 단에서 `context.l10n`을 통해 가져와 전달하는 방식입니다. 예를 들어, 네트워크 에러가 발생하면 ViewModel은 에러 코드나 타입을 상태로 노출하고, 위젯은 그 상태를 보고 `context.l10n.networkError` 와 같은 적절한 메시지를 화면에 표시합니다. 이는 관심사의 분리(Separation of Concerns) 원칙에도 부합합니다.
- `GlobalKey`를 이용한 접근 (필요시 사용): 앱 전체에서 접근 가능한 `Navigator`의 `BuildContext`를 활용하는 방법입니다.
이 방법은 편리하지만, 서비스 로직이 UI 컨텍스트에 직접 의존하게 되어 결합도가 높아지므로 신중하게 사용해야 합니다. 주로 앱 전체적으로 사용되는 스낵바나 다이얼로그를 띄우는 유틸리티 함수 등에서 유용합니다.// main.dart // 1. GlobalKey 생성 및 등록 final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); void main() { // 필요하다면 서비스 로케이터(GetIt 등)에 등록 // GetIt.I.registerSingleton(navigatorKey); runApp(MyApp(navigatorKey: navigatorKey)); } class MyApp extends StatelessWidget { final GlobalKey<NavigatorState> navigatorKey; const MyApp({super.key, required this.navigatorKey}); @override Widget build(BuildContext context) { return MaterialApp( navigatorKey: navigatorKey, // MaterialApp에 key 연결 // ... l10n 설정 ); } } // 서비스 클래스에서 사용 class MyService { void showMessage() { final context = navigatorKey.currentContext; if (context != null) { final message = AppLocalizations.of(context)!.pageHomeTitle; print(message); // "홈 화면" } } }
4단계: 프로덕션급 현지화 전략 (고급 주제)
기본적인 다국어 처리를 넘어, 실제 프로덕션 환경에서는 더욱 복잡하고 섬세한 요구사항에 직면하게 됩니다. 앱 내에서 동적으로 언어를 변경하거나, 숫자 및 날짜 형식을 현지에 맞게 표시하고, 텍스트를 넘어선 리소스를 현지화하는 방법들을 다룹니다.
4.1. `intl` 패키지를 활용한 정교한 포매팅
intl 패키지의 진정한 강점은 각 로케일의 문화적 관습에 맞는 숫자, 통화, 날짜/시간 포매팅 기능에 있습니다. 이를 직접 구현하는 것은 매우 어렵고 오류가 발생하기 쉽습니다.
날짜 및 시간 포매팅 (`DateFormat`)
import 'package:intl/intl.dart';
// ... 위젯의 build 메서드 내부 ...
final now = DateTime.now();
final currentLocale = Localizations.localeOf(context).toString(); // "ko_KR" 또는 "en_US"
// 다양한 기본 형식을 사용
final formattedDate1 = DateFormat.yMMMMd(currentLocale).format(now); // 2025년 11월 17일 / November 17, 2025
final formattedDate2 = DateFormat.yMMMEd(currentLocale).format(now); // 2025년 11월 17일 월요일 / Mon, Nov 17, 2025
final formattedTime = DateFormat.jms(currentLocale).format(now); // 오후 2:30:15 / 2:30:15 PM
// 직접 형식을 지정하여 커스터마이징
final customFormat = DateFormat('yyyy/MM/dd (E) HH:mm', currentLocale).format(now); // 2025/11/17 (월) 14:30
Text('날짜: $formattedDate1');
숫자 및 통화 포매팅 (`NumberFormat`)
import 'package:intl/intl.dart';
// ... 위젯의 build 메서드 내부 ...
final price = 1234567.89;
final currentLocale = Localizations.localeOf(context).toString();
// 천 단위 구분 기호 적용
final formattedNumber = NumberFormat.decimalPattern(currentLocale).format(price);
// 한국어 ('ko'): 1,234,567.89
// 프랑스어 ('fr'): 1 234 567,89
// 통화 형식 적용
final formattedCurrencyKR = NumberFormat.currency(
locale: 'ko_KR',
symbol: '₩',
).format(price); // ₩1,234,568 (반올림됨)
final formattedCurrencyUS = NumberFormat.currency(
locale: 'en_US',
symbol: '\$',
).format(price); // $1,234,567.89
Text('가격: $formattedCurrencyKR');
이처럼 intl 패키지를 사용하면 국가별로 다른 소수점, 천 단위 구분 기호, 통화 기호 위치 등을 걱정할 필요 없이 일관되고 정확한 현지화 경험을 제공할 수 있습니다.
4.2. 앱 내에서 동적 언어 변경 구현하기
사용자가 기기 설정과 무관하게 앱 설정 화면에서 직접 언어를 변경하는 기능은 사용자 경험을 크게 향상시킵니다. 이를 구현하려면 앱의 상태를 관리할 수 있는 상태 관리 솔루션(Provider, Riverpod, BLoC 등)이 필요합니다. 여기서는 Riverpod를 사용한 예제를 보여드리겠습니다.
- 언어 설정 저장 및 로드 (Persistence): 사용자가 선택한 언어를 앱을 껐다 켜도 유지하려면 `shared_preferences` 같은 로컬 저장소에 저장해야 합니다.
- 상태 관리 클래스(Notifier) 생성: 현재 `Locale`을 상태로 관리하고, 변경 및 저장을 책임지는 클래스를 만듭니다.
// lib/providers/locale_provider.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>((ref) { return LocaleNotifier(); }); class LocaleNotifier extends StateNotifier<Locale> { LocaleNotifier() : super(const Locale('ko')) { // 기본값 한국어 _loadLocale(); } void _loadLocale() async { final prefs = await SharedPreferences.getInstance(); final languageCode = prefs.getString('languageCode') ?? 'ko'; state = Locale(languageCode); } void setLocale(Locale newLocale) async { if (state == newLocale) return; state = newLocale; final prefs = await SharedPreferences.getInstance(); await prefs.setString('languageCode', newLocale.languageCode); } } - `main.dart` 수정: `ProviderScope`로 앱을 감싸고, `MaterialApp`이 Provider의 상태를 구독하도록 합니다.
// main.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; // ... void main() { runApp( const ProviderScope( // Riverpod 사용을 위해 최상위를 ProviderScope로 감쌉니다. child: MyApp(), ), ); } // MyApp은 이제 ConsumerWidget이 되어야 합니다. class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { // localeProvider를 구독(watch)하여 변경사항을 감지합니다. final currentLocale = ref.watch(localeProvider); return MaterialApp( locale: currentLocale, // Provider가 제공하는 Locale을 사용합니다. // ... 기존 delegates 및 supportedLocales 설정 ... home: const MyHomePage(), ); } } - 언어 변경 UI 구현: 설정 화면 등에서 Provider의 메서드를 호출하여 언어를 변경합니다.
// settings_page.dart class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { // AppLocalizations.supportedLocales는 자동 생성된 파일에서 제공합니다. final supportedLocales = AppLocalizations.supportedLocales; final currentLocale = ref.watch(localeProvider); return Scaffold( appBar: AppBar(title: const Text("언어 설정")), body: ListView.builder( itemCount: supportedLocales.length, itemBuilder: (context, index) { final locale = supportedLocales[index]; return RadioListTile<Locale>( title: Text(locale.languageCode.toUpperCase()), value: locale, groupValue: currentLocale, onChanged: (newLocale) { if (newLocale != null) { // Provider의 notifier를 통해 setLocale 메서드를 호출합니다. ref.read(localeProvider.notifier).setLocale(newLocale); } }, ); }, ), ); } }
이 방식을 통해, 사용자가 언어를 변경하면 `localeProvider`의 상태가 바뀌고, 이를 구독하고 있던 `MaterialApp`이 새로운 `locale` 값으로 재빌드되면서 앱 전체의 언어가 실시간으로 변경됩니다. 동시에 `shared_preferences`에 선택이 저장되어 다음 실행 시에도 유지됩니다.
4.3. 텍스트를 넘어선 현지화: 이미지와 레이아웃
진정한 현지화는 텍스트 번역에서 끝나지 않습니다. 문화적 차이를 반영한 이미지, 그리고 언어의 특성을 고려한 레이아웃 조정까지 포함해야 합니다.
로케일별 이미지 제공
특정 문화권에만 의미가 있는 프로모션 배너나 아이콘이 있을 수 있습니다. 이 경우, 로케일별로 다른 이미지를 제공하는 전략을 사용할 수 있습니다.
assets/
images/
promo_banner.png # 기본 이미지
ko/
promo_banner.png # 한국어 사용자에게 보여줄 이미지
ja/
promo_banner.png # 일본어 사용자에게 보여줄 이미지
이러한 구조를 만들고, 현재 로케일에 맞는 이미지 경로를 동적으로 반환하는 헬퍼 함수를 만들면 편리합니다.
String getLocaleImagePath(BuildContext context, String baseName) {
final locale = Localizations.localeOf(context).languageCode; // 'ko', 'en', 'ja'
final localPath = 'assets/images/$locale/$baseName';
final defaultPath = 'assets/images/$baseName';
// 여기서 실제 파일 존재 여부를 확인하는 로직을 추가하면 더 견고해집니다.
// 이 예제에서는 단순 경로 반환으로 가정합니다.
// 예: if (await rootBundle.load(localPath) != null) return localPath;
// 하지만 매번 파일 존재를 확인하는 것은 성능에 영향을 줄 수 있으므로,
// 지원하는 로케일 목록을 기반으로 경로를 결정하는 것이 더 효율적입니다.
// 지원하는 로케일의 경우 해당 경로 반환
if (['ko', 'ja'].contains(locale)) {
return localPath;
}
// 그 외에는 기본 경로 반환
return defaultPath;
}
// 사용 예시
Image.asset(getLocaleImagePath(context, 'promo_banner.png'))
오른쪽에서 왼쪽으로(RTL) 레이아웃 지원
아랍어, 히브리어 등은 글을 오른쪽에서 왼쪽으로 씁니다(RTL). Flutter는 `MaterialApp`의 로케일 설정에 따라 이를 자동으로 처리해 줍니다. 예를 들어 `Locale('ar')`를 설정하면, `Row` 위젯의 자식 순서가 자동으로 반전되고, `leading`/`trailing` 속성도 그에 맞게 동작합니다.
개발자는 `left`/`right` 대신 `start`/`end`와 같은 논리적 속성을 사용하여 레이아웃을 구성하는 습관을 들이는 것이 중요합니다. - `Padding.only(left: 8.0)` → `Padding.only(start: 8.0)` - `Row`의 `mainAxisAlignment: MainAxisAlignment.start`
이렇게 하면 LTR, RTL 환경 모두에서 의도한 대로 동작하는 유연한 UI를 만들 수 있습니다.
결론: 단순 번역을 넘어선 사용자 경험의 완성
지금까지 Flutter의 강력한 국제화 및 현지화 시스템을 구축하고 활용하는 전 과정을 개념부터 프로덕션 수준의 고급 전략까지 깊이 있게 살펴보았습니다. intl 패키지와 flutter gen-l10n 도구를 중심으로 한 체계적인 파이프라인은 유지보수가 용이하며 확장 가능한 다국어 처리 시스템의 근간이 됩니다. ARB 파일의 메타데이터를 통해 번역가와 원활하게 협업하고, ICU 메시지 형식을 통해 복수형이나 성별과 같은 복잡한 언어 규칙에 유연하게 대응하며, 숫자와 날짜 포매팅으로 세심한 현지화 경험을 완성할 수 있습니다.
성공적인 글로벌 앱은 여러 언어를 단순히 '지원'하는 것을 넘어, 각 문화권의 사용자가 '자신을 위해 만들어졌다'고 느끼게 하는 디테일에서 결정됩니다. 텍스트뿐만 아니라 이미지, 레이아웃, 그리고 문화적 뉘앙스까지 고려하는 총체적인 접근 방식이 필요합니다. Flutter가 제공하는 견고한 기반 위에서 이러한 섬세한 차이를 구현하는 것은 더 이상 막막하고 복잡한 작업이 아닙니다. 이 글에서 다룬 전략들을 바탕으로, 여러분의 Flutter 애플리케이션이 전 세계 사용자들의 마음을 사로잡는 강력한 글로벌 경쟁력을 갖추게 되기를 진심으로 바랍니다.
Post a Comment