Flutter 튜토리얼 62편: 배포 준비와 코드 난독화
개요
Flutter 앱을 스토어에 배포하기 전에 수행해야 할 준비 작업을 알아봅니다. 코드 난독화로 앱을 보호하고, 빌드 플레이버1로 개발/스테이징/프로덕션 환경을 분리하는 방법을 배웁니다.
graph TB
subgraph "배포 준비 과정"
A[코드 난독화] --> B[리버스 엔지니어링 방지]
C[빌드 플레이버] --> D[환경별 분리]
E[에셋 최적화] --> F[앱 크기 감소]
end
1. 코드 난독화
Why: 왜 코드 난독화가 필요한가요?
앱 바이너리를 분석하면 클래스명, 함수명 등을 확인할 수 있습니다. 난독화를 통해 코드를 이해하기 어렵게 만들어 리버스 엔지니어링2을 방지할 수 있습니다.
What: 코드 난독화란 무엇인가요?
코드 난독화는 컴파일된 Dart 바이너리의 심볼(함수명, 클래스명 등)을 알아보기 어려운 이름으로 대체하는 과정입니다.
| 원본 코드 | 난독화 후 |
|---|---|
MaterialApp | ex |
Scaffold | ey |
MyHomePage | ez |
calculateTotal | a1 |
지원되는 빌드 타겟
| 타겟 | 지원 |
|---|---|
apk | O |
appbundle | O |
ios / ipa | O |
macos | O |
linux | O |
windows | O |
web | X (대신 Minification 적용) |
How: 코드 난독화 적용 방법
기본 난독화 명령
# Android APK 빌드flutter build apk --obfuscate --split-debug-info=./symbols/android
# Android App Bundle 빌드flutter build appbundle --obfuscate --split-debug-info=./symbols/android
# iOS IPA 빌드flutter build ipa --obfuscate --split-debug-info=./symbols/ios
# macOS 빌드flutter build macos --obfuscate --split-debug-info=./symbols/macos
# Linux 빌드flutter build linux --obfuscate --split-debug-info=./symbols/linux
# Windows 빌드flutter build windows --obfuscate --split-debug-info=./symbols/windows심볼 파일 구조
symbols/├── android/│ ├── app.android-arm.symbols│ ├── app.android-arm64.symbols│ └── app.android-x64.symbols├── ios/│ └── app.ios-arm64.symbols└── macos/ └── app.macos-arm64.symbols난독화된 스택 트레이스 해석
크래시 리포트에서 난독화된 스택 트레이스를 받았을 때, 원래 심볼로 변환할 수 있습니다.
# 스택 트레이스 파일과 심볼 파일을 사용하여 원본으로 변환flutter symbolize \ -i ./crash_stacktrace.txt \ -d ./symbols/android/app.android-arm64.symbols난독화 맵 생성
원본 이름과 난독화된 이름의 매핑을 JSON 파일로 생성할 수 있습니다.
flutter build apk \ --obfuscate \ --split-debug-info=./symbols/android \ --extra-gen-snapshot-options=--save-obfuscation-map=./symbols/android/map.json생성된 맵 파일 예시:
["MaterialApp", "ex", "Scaffold", "ey", "MyHomePage", "ez"]Watch out: 주의사항
// 이 코드는 난독화 후 실패할 수 있습니다!expect(foo.runtimeType.toString(), equals('Foo'));
// 대신 이렇게 사용하세요expect(foo, isA<Foo>());- 심볼 파일 백업 필수: 크래시 분석을 위해 반드시 보관하세요.
- 리플렉션 주의:
runtimeType.toString()등은 난독화 후 동작이 달라질 수 있습니다. - Enum 이름: 현재 Enum 이름은 난독화되지 않습니다.
- 릴리스 빌드만: 난독화는 릴리스 빌드에서만 작동합니다.
- 보안의 한계: 난독화는 리소스를 암호화하지 않으며, 완전한 보안을 보장하지 않습니다.
2. 빌드 플레이버 (Android)
Why: 왜 플레이버가 필요한가요?
개발, 스테이징, 프로덕션 환경마다 다른 API 엔드포인트, 앱 아이콘, 앱 이름 등을 사용해야 할 때 플레이버가 필요합니다.
What: 플레이버란 무엇인가요?
플레이버는 동일한 코드베이스에서 다양한 버전의 앱을 빌드할 수 있게 해주는 기능입니다.
graph TB
subgraph "빌드 변형 (Build Variants)"
A[Product Flavors] --> C[stagingDebug]
A --> D[stagingRelease]
B[Build Types] --> C
B --> D
A --> E[productionDebug]
A --> F[productionRelease]
B --> E
B --> F
end
| 구성 요소 | 설명 | 예시 |
|---|---|---|
| Product Flavor | 앱의 변형 | staging, production |
| Build Type | 빌드 유형 | debug, release |
| Build Variant | 조합 결과 | stagingDebug, productionRelease |
How: Android 플레이버 설정
1단계: build.gradle.kts 수정
android { // ... 기존 설정 ...
buildTypes { getByName("debug") { // debug 설정 } getByName("release") { // release 설정 } }
// 플레이버 차원 정의 flavorDimensions += "default"
// 플레이버 정의 productFlavors { create("dev") { dimension = "default" applicationIdSuffix = ".dev" versionNameSuffix = "-dev" resValue( type = "string", name = "app_name", value = "My App (Dev)" ) } create("staging") { dimension = "default" applicationIdSuffix = ".staging" versionNameSuffix = "-staging" resValue( type = "string", name = "app_name", value = "My App (Staging)" ) } create("production") { dimension = "default" // production은 기본 applicationId 사용 resValue( type = "string", name = "app_name", value = "My App" ) } }}2단계: AndroidManifest.xml 수정
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:label="@string/app_name" android:icon="@mipmap/ic_launcher" ...> <!-- 앱 내용 --> </application></manifest>3단계: 플레이버별 아이콘 설정
android/app/src/├── main/│ └── res/│ └── mipmap-*/│ └── ic_launcher.png # 기본 아이콘├── dev/│ └── res/│ └── mipmap-*/│ └── ic_launcher.png # dev 전용 아이콘├── staging/│ └── res/│ └── mipmap-*/│ └── ic_launcher.png # staging 전용 아이콘└── production/ └── res/ └── mipmap-*/ └── ic_launcher.png # production 전용 아이콘4단계: 플레이버 빌드 및 실행
# 플레이버로 실행flutter run --flavor devflutter run --flavor stagingflutter run --flavor production
# 플레이버로 APK 빌드flutter build apk --flavor devflutter build apk --flavor stagingflutter build apk --flavor production
# 플레이버로 App Bundle 빌드flutter build appbundle --flavor production기본 플레이버 설정
flutter: default-flavor: dev이제 flutter run 명령만 입력해도 자동으로 dev 플레이버가 선택됩니다.
플레이버별 에셋 번들링
flutter: assets: - assets/common/ - path: assets/dev/ flavors: - dev - path: assets/staging/ flavors: - staging - path: assets/production/ flavors: - production3. 빌드 플레이버 (iOS)
iOS 플레이버 설정
iOS에서는 Xcode의 Scheme과 Configuration을 사용하여 플레이버를 구현합니다.
1단계: Xcode에서 Configuration 생성
- Xcode에서 프로젝트를 엽니다.
- Project Navigator에서 Runner 프로젝트를 선택합니다.
- Info 탭에서 Configurations를 확인합니다.
- Debug, Release를 복제하여 새 Configuration을 만듭니다.
- Debug-dev, Debug-staging, Debug-production
- Release-dev, Release-staging, Release-production
2단계: Scheme 생성
- Product > Scheme > Manage Schemes를 선택합니다.
- Runner 스킴을 복제하여 각 플레이버별 스킴을 만듭니다.
- dev, staging, production
3단계: Build Settings 수정
각 Configuration별로 다른 Bundle Identifier와 Display Name을 설정합니다.
// Build SettingsPRODUCT_BUNDLE_IDENTIFIER = com.example.app$(BUNDLE_ID_SUFFIX)
// User-Defined Settings (각 Configuration별로 다르게 설정)BUNDLE_ID_SUFFIX = .dev // Debug-dev, Release-devBUNDLE_ID_SUFFIX = .staging // Debug-staging, Release-stagingBUNDLE_ID_SUFFIX = // Debug-production, Release-production4단계: Info.plist 수정
<!-- ios/Runner/Info.plist --><key>CFBundleDisplayName</key><string>$(DISPLAY_NAME)</string>4. Dart 코드에서 플레이버 사용
플레이버 감지
import 'package:flutter/foundation.dart';
enum AppFlavor { dev, staging, production }
class FlavorConfig { static AppFlavor get appFlavor { // Flutter CLI에서 --flavor 옵션으로 전달된 값을 가져옵니다 const flavor = String.fromEnvironment('FLAVOR', defaultValue: 'production');
switch (flavor) { case 'dev': return AppFlavor.dev; case 'staging': return AppFlavor.staging; case 'production': default: return AppFlavor.production; } }
static String get apiBaseUrl { switch (appFlavor) { case AppFlavor.dev: return 'https://dev-api.example.com'; case AppFlavor.staging: return 'https://staging-api.example.com'; case AppFlavor.production: return 'https://api.example.com'; } }
static bool get enableLogging { return appFlavor != AppFlavor.production; }}플레이버별 main.dart 분리
import 'package:my_app/app.dart';import 'package:my_app/config/flavor_config.dart';
void main() { FlavorConfig.init(AppFlavor.dev); runApp(const MyApp());}
// lib/main_staging.dartimport 'package:my_app/app.dart';import 'package:my_app/config/flavor_config.dart';
void main() { FlavorConfig.init(AppFlavor.staging); runApp(const MyApp());}
// lib/main_production.dartimport 'package:my_app/app.dart';import 'package:my_app/config/flavor_config.dart';
void main() { FlavorConfig.init(AppFlavor.production); runApp(const MyApp());}플레이버별 실행 명령
# 다른 main 파일로 실행flutter run --flavor dev -t lib/main_dev.dartflutter run --flavor staging -t lib/main_staging.dartflutter run --flavor production -t lib/main_production.dart5. 종합 예제: 배포 준비 체크리스트
import 'package:flutter/material.dart';import 'package:flutter/foundation.dart';
void main() { // 릴리스 모드에서만 에러 핸들링 설정 if (kReleaseMode) { FlutterError.onError = (details) { // 크래시 리포팅 서비스로 에러 전송 // Crashlytics, Sentry 등 }; }
runApp(const MyApp());}
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return MaterialApp( title: 'My App', debugShowCheckedModeBanner: false, // 릴리스에서 배너 제거 theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ), home: const DeploymentInfoPage(), ); }}
class DeploymentInfoPage extends StatelessWidget { const DeploymentInfoPage({super.key});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('배포 정보')), body: ListView( padding: const EdgeInsets.all(16), children: [ _buildInfoCard( '빌드 모드', _getBuildMode(), Icons.build, ), const SizedBox(height: 12), _buildInfoCard( '프로파일 모드', kProfileMode ? '활성화' : '비활성화', Icons.speed, ), const SizedBox(height: 12), _buildInfoCard( '릴리스 모드', kReleaseMode ? '활성화' : '비활성화', Icons.rocket_launch, ), const SizedBox(height: 24), _buildChecklistCard(), ], ), ); }
String _getBuildMode() { if (kReleaseMode) return 'Release'; if (kProfileMode) return 'Profile'; return 'Debug'; }
Widget _buildInfoCard(String title, String value, IconData icon) { return Card( child: ListTile( leading: Icon(icon, size: 40), title: Text(title), subtitle: Text(value), ), ); }
Widget _buildChecklistCard() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '배포 전 체크리스트', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), _buildCheckItem('코드 난독화 적용', kReleaseMode), _buildCheckItem('심볼 파일 백업', true), _buildCheckItem('버전 번호 업데이트', true), _buildCheckItem('API 키 프로덕션용 사용', kReleaseMode), _buildCheckItem('디버그 로그 제거', kReleaseMode), _buildCheckItem('테스트 완료', true), ], ), ), ); }
Widget _buildCheckItem(String text, bool checked) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Icon( checked ? Icons.check_circle : Icons.radio_button_unchecked, color: checked ? Colors.green : Colors.grey, ), const SizedBox(width: 8), Text(text), ], ), ); }}배포 전 체크리스트
| 항목 | 설명 | 확인 |
|---|---|---|
| 코드 난독화 | --obfuscate --split-debug-info 적용 | [ ] |
| 심볼 파일 백업 | 크래시 분석용 심볼 파일 보관 | [ ] |
| 버전 번호 | pubspec.yaml의 version 업데이트 | [ ] |
| 앱 아이콘 | 모든 해상도의 아이콘 준비 | [ ] |
| 스플래시 화면 | 플랫폼별 스플래시 화면 설정 | [ ] |
| API 키 | 프로덕션 API 키 사용 확인 | [ ] |
| 디버그 코드 제거 | print 문, 테스트 코드 제거 | [ ] |
| 권한 설정 | 필요한 권한만 요청하도록 설정 | [ ] |
| 테스트 | 릴리스 빌드로 전체 테스트 | [ ] |
마무리
이번 튜토리얼에서는 Flutter 앱 배포 준비에 필요한 코드 난독화와 빌드 플레이버 설정을 살펴보았습니다.
핵심 요약
| 기능 | 핵심 포인트 |
|---|---|
| 코드 난독화 | --obfuscate --split-debug-info 옵션 사용 |
| 스택 트레이스 해석 | flutter symbolize 명령으로 원본 복원 |
| Android 플레이버 | build.gradle.kts에서 productFlavors 설정 |
| iOS 플레이버 | Xcode의 Scheme과 Configuration 사용 |
다음 단계
- 63편에서는 Android/iOS 스토어 배포를 다룹니다.
- 각 플랫폼의 스토어 정책을 미리 확인해보세요.
- CI/CD 파이프라인에 난독화와 플레이버를 통합해보세요.
참고 자료
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!
Flutter 튜토리얼 62편: 배포 준비와 코드 난독화
https://moodturnpost.net/posts/flutter/flutter-deployment-prep/ 작성자
Moodturn
게시일
2026-01-08