flavor - ๊ฐ๋ฐ/์ด์ ํ๊ฒฝ ๋ถ๋ฆฌ
๊ฐ๋ฐ ์ค์๋ ์๋ฒ ์ฃผ์๋ฅผ https://dev.example.com์ผ๋ก ์ฐ๋ค๊ฐ, ๋ฐฐํฌํ ๋ https://api.example.com์ผ๋ก ๊ณ ์น๊ณ ๋น๋ํ๋ ์ฌ๋์ด ๋ง๋ค. ๋ฌธ์ ๋ ๊ณ ์น๋ ๊ฑธ ๊น๋นกํ๋ ๋ ์ด ์จ๋ค๋ ๊ฒ์ด๋ค. ๊ฐ๋ฐ ์๋ฒ ์ฃผ์๋ก ๋ฐฐํฌํ ์ฑ์ด ๋๊ฐ๊ฑฐ๋, ๋ฐ๋๋ก ๊ฐ๋ฐ ์ค์ ์ด์ ์๋ฒ์ ํ
์คํธ ๋ฐ์ดํฐ๋ฅผ ์ฐ๊ณ ์ง์๋ฒ๋ฆฌ๋ ์ฌ๊ณ ๊ฐ ๋๋ค.
๋ ํ๋์ ๋ถํธ์ด ์๋ค. ๊ฐ๋ฐ์ฉ ์ฑ์ ํฐ์ ์ค์นํ ์ํ์์ ์ด์์ฉ ์ฑ์ ์ค์นํ๋ฉด ๊ฐ์ ์ฑ์ผ๋ก ์ทจ๊ธ๋์ด ๋ฎ์ด์์์ง๋ค. ๊ธฐ๊ธฐ์ ๋์ ๋๋ํ ๋๊ณ ๋น๊ตํด ๋ณผ ์๊ฐ ์๋ค.
class ApiConfig {
// ๋ฐฐํฌ ์ ์ ์ด๊ฑธ ๋ฐ๊ฟ์ผ ํจ... ๊น๋นกํ๋ฉด?
static const baseUrl = 'https://dev.example.com';
}์์ธ: ํ๊ฒฝ์ด ์ฝ๋ ์์ ํ๋์ฝ๋ฉ๋์ด ์๋ค
๊ฐ๋ฐ, ์ด์ ๊ฐ์ ํ๊ฒฝ์ ์ฐจ์ด๋ฅผ ์ฝ๋๋ฅผ ์ง์ ๊ณ ์ณ์ ๋ง์ถ๊ณ ์๋ค. ๊ฐ์ ์ฝ๋์์ ๋น๋ํ ๋ ํ๊ฒฝ์ ๊ณ ๋ฅด๋ ๋ฐฉ๋ฒ์ด ํ์ํ๋ค. ์ด๊ฒ flavor๋ค. ๊ฐ์ ์์ค์์ ์๋ก ๋ค๋ฅธ ์ค์ ์ ๊ฐ์ง ์ฑ์ ์ฌ๋ฌ ์ข ๋ฅ ๋ง๋ค์ด ๋ธ๋ค. ์ฑ ์ด๋ฆ, ์ฑ ID, ์์ด์ฝ, ์๋ฒ ์ฃผ์๊ฐ ํ๊ฒฝ๋ง๋ค ๋ฌ๋ผ์ง๋ค.
flavor๊ฐ ๋ฐ๊พธ๋ ๊ฒ
- ์ฑ ID(ํจํค์ง ์ด๋ฆ): ๊ฐ๋ฐ์ฉ์
com.example.myapp.dev, ์ด์์ฉ์com.example.myapp์ฒ๋ผ ๋ฌ๋ผ์ ํ ๊ธฐ๊ธฐ์ ๋์ด ๊ฐ์ด ์ค์น๋๋ค - ์ฑ ์ด๋ฆ๊ณผ ์์ด์ฝ: ํ ํ๋ฉด์์ "MyApp Dev"์ "MyApp"์ ๊ตฌ๋ถํ๋ค
- ์๋ฒ ์ฃผ์, ํค ๊ฐ์ ์ค์ ๊ฐ: Dart ์ฝ๋์์ ์ฝ๋๋ค
- Firebase ํ๋ก์ ํธ: ๊ฐ๋ฐ์ฉ Firebase์ ์ด์์ฉ Firebase๋ฅผ ๋ฐ๋ก ์ด๋ค
1๋จ๊ณ: Android - productFlavors
android/app/build.gradle(๋๋ build.gradle.kts)์ android { } ์์ ์ถ๊ฐํ๋ค. Groovy ํ์์ ์์๋ค.
android {
// ...
flavorDimensions "default"
productFlavors {
dev {
dimension "default"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
resValue "string", "app_name", "MyApp Dev"
}
prod {
dimension "default"
resValue "string", "app_name", "MyApp"
}
}
}flavorDimensions: flavor๋ฅผ ๋ฌถ๋ ์ถ์ด๋ค. ์ง๊ธ์ ํ๋๋ฉด ๋๋คapplicationIdSuffix ".dev": ์ฑ ID ๋ค์.dev๋ฅผ ๋ถ์ธ๋ค. ๊ทธ๋์ ๊ฐ๋ฐ์ฉ๊ณผ ์ด์์ฉ์ด ๋ค๋ฅธ ์ฑ์ผ๋ก ์ค์น๋๋คresValue "string", "app_name", ...: ์ฑ ์ด๋ฆ์ flavor๋ง๋ค ๋ค๋ฅด๊ฒ ์ค๋ค.AndroidManifest.xml์android:label์@string/app_name์ผ๋ก ๋ฐ๊ฟ ๋๋ค
Kotlin ์คํฌ๋ฆฝํธ(build.gradle.kts)๋ฅผ ์ฐ๋ ํ๋ก์ ํธ๋ ๊ฐ์ ๊ตฌ์กฐ๋ฅผ ์ฝํ๋ฆฐ ๋ฌธ๋ฒ์ผ๋ก ์ ๋๋ค. ์๋๋ก์ด๋์ ๋น๋ ๋ณํ(build variants)์ ์๋ ์ฌ๋์ด๋ผ๋ฉด ์ต์ํ ์ฝ๋๋ค.
2๋จ๊ณ: iOS - ์คํด๊ณผ ๋น๋ ์ค์
iOS ์ชฝ์ด ๋ ์์ด ๊ฐ๋ค. Xcode์์ ios/Runner.xcworkspace๋ฅผ ์ด๊ณ ์ด๋ ๊ฒ ํ๋ค.
- ๋น๋ ์ค์ (Configurations): ๊ธฐ์กด
Debug,Release๋ฅผ ๋ณต์ ํด์Debug-dev,Release-dev,Debug-prod,Release-prod์ฒ๋ผ flavor๋ณ๋ก ๋ง๋ ๋ค - ์คํด(Scheme): flavor ์ด๋ฆ๊ณผ ๊ฐ์ ์ด๋ฆ์ ์คํด(
dev,prod)์ ๋ง๋ค์ด์, ๊ฐ ์คํด์ด ์์ ๋น๋ ์ค์ ์ ์ฐ๊ฒ ์ฐ๊ฒฐํ๋ค - ์ฑ ID์ ์ด๋ฆ: ์ค์ ๋ณ๋ก
PRODUCT_BUNDLE_IDENTIFIER(com.example.myapp.dev),PRODUCT_NAME์ ๋ค๋ฅด๊ฒ ์ค๋ค
์คํด ์ด๋ฆ์ด flutter run --flavor ์ด๋ฆ์ ์ด๋ฆ๊ณผ ์ ํํ ๊ฐ์์ผ ํ๋ค. ์ด ์ค์ ์ ์์ผ๋ก ํ๊ธฐ ์ด๋ ค์ฐ๋ฉด flavorizr ๊ฐ์ ์๋ํ ํจํค์ง๊ฐ ๋ ํ๋ซํผ์ ์ค์ ์ ๋ง๋ค์ด์ค๋ค. ๋ค๋ง ์์ฑ ๊ฒฐ๊ณผ๊ฐ ๋ฌด์์ ๋ฐ๊ฟจ๋์ง ํ ๋ฒ์ ์ง์ ํ์ธํด์ผ ํ๋ค.
3๋จ๊ณ: ์คํ๊ณผ ๋น๋
flutter run --flavor dev
flutter run --flavor prod
flutter build appbundle --flavor prod
flutter build ipa --flavor prod--flavor ์ด๋ฆ์ด Android๋ productFlavors์ ์ด๋ฆ์, iOS๋ ์คํด์ ์ด๋ฆ์ ๊ฐ๋ฆฌํจ๋ค.
์ค์ ์ด ๋น ์ก์ ๋ ๋์ค๋ ์๋ฌ๊ฐ ์ด ๋จ๊ณ์ ๋จ๊ณจ์ด๋ค.
- Android์ flavor๋ฅผ ์ ์ํ๋๋ฐ
--flavor์์ด ์คํํ๋ฉด, flavor๋ฅผ ์ง์ ํ๋ผ๋ ์ทจ์ง์ ์๋ฌ๊ฐ ๋๋ค - iOS์ ์คํด์ด ์๋๋ฐ
--flavor๋ฅผ ์ฃผ๋ฉด, ์ฌ์ฉ์ ์ง์ ์คํด์ด ์์ด์--flavor๋ฅผ ์ธ ์ ์๋ค๋ ์ทจ์ง์ ์๋ฌ๊ฐ ๋๋ค
์๋ฌ ๋ฉ์์ง๊ฐ ์ด๋ ํ๋ซํผ์ ์ค์ ์ด ์๋์ง ์๋ ค์ฃผ๋ ๋ฉ์์ง๋ฅผ ์ฝ๊ณ ํด๋น ์ชฝ์ ํ์ธํ๋ค.
4๋จ๊ณ: Dart ์ฝ๋์์ ํ๊ฒฝ ์ฝ๊ธฐ - dart-define
๋ค์ดํฐ๋ธ ์ค์ ์ ์ฑ ID์ ์ด๋ฆ์ ๋ฐ๊ฟ ๋ฟ์ด๊ณ , ์๋ฒ ์ฃผ์ ๊ฐ์ ๊ฐ์ Dart ์ฝ๋๊ฐ ์์์ผ ํ๋ค. ๋น๋ํ ๋ ๊ฐ์ ๋๊ธฐ๋ --dart-define์ ์ด๋ค.
flutter run --flavor dev --dart-define=FLAVOR=dev
flutter run --flavor prod --dart-define=FLAVOR=prodenum Flavor { dev, prod }
class AppConfig {
final Flavor flavor;
final String baseUrl;
const AppConfig({required this.flavor, required this.baseUrl});
static AppConfig fromEnvironment() {
const name = String.fromEnvironment('FLAVOR', defaultValue: 'prod');
return switch (name) {
'dev' => const AppConfig(
flavor: Flavor.dev,
baseUrl: 'https://dev.example.com',
),
_ => const AppConfig(
flavor: Flavor.prod,
baseUrl: 'https://api.example.com',
),
};
}
}String.fromEnvironment('FLAVOR', defaultValue: 'prod')๋ ์ปดํ์ผ ๋ ๋ค์ด์จ ๊ฐ์ ์ฝ๋๋ค. ๊ฐ์ด ์์ ๋๋ฅผ ์ํด ๊ธฐ๋ณธ๊ฐ์ ๋๋ค. ๊ธฐ๋ณธ๊ฐ์ prod๋ก ๋๋ฉด ์ค์๋ก --dart-define์ ๋น ๋จ๋ ค๋ ๊ฐ๋ฐ์ฉ ์๋ฒ๊ฐ ์๋๋ผ ์ด์ ์ค์ ์ผ๋ก ๊ฐ๋ฏ๋ก, ๋ฐฐํฌ ์ฌ๊ณ ์ชฝ์ผ๋ก๋ ์์ ํ๋ค. ๋ฐ๋ ๋ฐฉํฅ ์ค์(๊ฐ๋ฐ ์ค ์ด์ ์๋ฒ ๋ฐ์ดํฐ๋ฅผ ๊ฑด๋๋ฆฌ๋ ๊ฒ)๋ defaultValue๋ฅผ dev๋ก ๋๋ ํธ์ด ์์ ํ๋ค. ์ด๋ ์ฌ๊ณ ๊ฐ ๋ ๋ฌด์์ด์ง ํ์์ ์ ํ์.
๊ฐ์ด ์ฌ๋ฌ ๊ฐ๋ฉด ํ์ผ๋ก ๋ฌถ์ด์ ๋๊ธธ ์ ์๋ค.
flutter run --flavor dev --dart-define-from-file=config/dev.json{
"FLAVOR": "dev",
"BASE_URL": "https://dev.example.com"
}์ด๋ ๊ฒ ๋ง๋ AppConfig๋ ์์กด์ฑ ์ฃผ์
๊ธ์ ๋ฐฉ์์ผ๋ก ์ฑ ์ ์ฒด์ ๋ด๋ ค์ค๋ค.
final appConfigProvider = Provider<AppConfig>(
(ref) => throw UnimplementedError(), // main์์ ๋ฐ๊ฟ ๋ผ์ด๋ค
);
void main() {
final config = AppConfig.fromEnvironment();
runApp(
ProviderScope(
overrides: [appConfigProvider.overrideWithValue(config)],
child: const MyApp(),
),
);
}
final dioProvider = Provider<Dio>(
(ref) => Dio(BaseOptions(baseUrl: ref.watch(appConfigProvider).baseUrl)),
);Dio๋ฅผ ๋ง๋๋ ๊ณณ์ด appConfigProvider์์ ์๋ฒ ์ฃผ์๋ฅผ ๋ฐ์ผ๋, ํ๊ฒฝ์ด ๋ฐ๋์ด๋ Dio๋ฅผ ๋ง๋๋ ์ฝ๋๋ ๊ทธ๋๋ก๋ค. ์ํํ ๋๋ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๊ฐ์ง ์ค์ ์ ๋ผ์ด๋ค.
์ง์ ์ ์ ๋๋๋ ๋ฐฉ๋ฒ๋ ์๋ค
--dart-define ๋์ lib/main_dev.dart, lib/main_prod.dart์ฒ๋ผ flavor๋ณ main ํ์ผ์ ๋๋ ๋ฐฉ์๋ ๋ง์ด ์ด๋ค.
// lib/main_dev.dart
void main() => bootstrap(AppConfig.dev);
// lib/main_prod.dart
void main() => bootstrap(AppConfig.prod);flutter run --flavor dev -t lib/main_dev.dart-t(--target)๊ฐ ์์ ํ์ผ์ ์ ํ๋ค. ์ค์ ์ด ์ฝ๋์ ๋ณด์ด๋ ์ฅ์ ์ด ์์ง๋ง, ๋งค๋ฒ -t๋ฅผ ๊ฐ์ด ์จ์ผ ํ๋ ๋ฒ๊ฑฐ๋ก์์ด ์๋ค. ๋ ์ค ํ๋๋ก ํ์ด ํต์ผํ๋ฉด ๋๋ค.
์ฝ์ง ํฌ์ธํธ 1: dart-define์ ๋น๋ฐ์ ์จ๊ฒจ์ฃผ์ง ์๋๋ค
--dart-define์ผ๋ก ๋ฃ์ ๊ฐ์ ๋น๋๋ ์ฑ ์์ ๊ทธ๋๋ก ๋ค์ด๊ฐ๋ค. ์ฑ์ ๋ถํดํ๋ฉด ๊บผ๋ด๋ณผ ์ ์๋ค. ๊ทธ๋ฌ๋ ์๋ฒ ์ฃผ์๋ ๊ณต๊ฐ์ฉ ํค๋ ๊ด์ฐฎ์ง๋ง, ์๋ฒ์ ๋น๋ฐ ํค๋ ๊ด๋ฆฌ์ ๋น๋ฐ๋ฒํธ๋ ๋ฃ์ผ๋ฉด ์ ๋๋ค. ๊ทธ๋ฐ ๊ฐ์ ์ฑ์ด ์๋๋ผ ์๋ฒ๊ฐ ๋ค๊ณ ์์ด์ผ ํ๋ค.
์ฝ์ง ํฌ์ธํธ 2: Firebase๋ flavor๋ง๋ค ์ค์ ํ์ผ์ด ๋ฐ๋ก ํ์ํ๋ค
๊ฐ๋ฐ๊ณผ ์ด์์ Firebase ํ๋ก์ ํธ๋ฅผ ๋๋๋ฉด google-services.json(Android)๊ณผ GoogleService-Info.plist(iOS)๋ ๋์ด๋ค. ์๋๋ก์ด๋๋ android/app/src/dev/google-services.json, android/app/src/prod/google-services.json์ฒ๋ผ flavor ํด๋ ์์ ๋๋ค. Firebase ๊ธ์ flutterfire configure๋ flavor๋ณ๋ก ๋ฐ๋ก ์คํํด์ firebase_options ํ์ผ์ ๋๋๊ฑฐ๋ ์ ํํ๊ฒ ํด์ผ ํ๋ค.
์ฝ์ง ํฌ์ธํธ 3: ์์ด์ฝ๊ณผ ์ด๋ฆ์ผ๋ก ๊ตฌ๋ณํ์ง ์์ผ๋ฉด ํท๊ฐ๋ฆฐ๋ค
๊ฐ๋ฐ์ฉ๊ณผ ์ด์์ฉ์ด ๋ ๋ค ์ค์น๋๋ฉด ํ ํ๋ฉด์์ ๊ตฌ๋ณ์ด ์ ๋๋ค. ์ฑ ์ด๋ฆ์ "Dev"๋ฅผ ๋ถ์ด๊ณ , ์์ด์ฝ ๋ชจ์๋ฆฌ์ ๋ ๋ฅผ ๋๋ฅด๋ ์์ผ๋ก ๊ฐ๋ฐ์ฉ์ ๋์ ๋๊ฒ ๋ง๋ ๋ค. ์ง๋ ๊ธ์ flutter_launcher_icons๋ flutter_launcher_icons-dev.yaml์ฒ๋ผ flavor๋ณ ์ค์ ํ์ผ์ ์ง์ํ๋ค.
Android, iOS์ ๋น๊ตํ๋ฉด
- Android:
productFlavors์buildTypes์ ์กฐํฉ์ด build variants๊ฐ ๋๋ค. Flutter์--flavor๊ฐ ์ด ์ด๋ฆ์ ๊ทธ๋๋ก ์ด๋ค - iOS: ์คํด๊ณผ ๋น๋ ์ค์ (
xcconfig)์ด ๊ฐ์ ์ญํ ์ด๋ค. Flutter์--flavor๋ ์คํด ์ด๋ฆ์ ๊ฐ๋ฆฌํจ๋ค - Compose, SwiftUI๋ UI ํ๋ ์์ํฌ๋ผ์ ํ๊ฒฝ ๋ถ๋ฆฌ๋ ๊ฐ ํ๋ซํผ์ ๋น๋ ์ค์ ์ ์ด๋ค
๋ค์ดํฐ๋ธ ๊ฐ๋ฐ์์ ํด์จ build variants์ ์คํด์ด ๊ทธ๋๋ก ์ฐ์ธ๋ค. Flutter๊ฐ ๋ํ๋ ๊ฑด --flavor ์ต์
์ผ๋ก ๋์ ํ๋์ ๋ช
๋ น์ ์ฐ๊ฒฐํด ์ฃผ๋ ๊ฒ๊ณผ, Dart ์ชฝ์์ ํ๊ฒฝ๊ฐ์ ์ฝ๋ --dart-define์ด๋ค.
์ ๋ฆฌ
- ์๋ฒ ์ฃผ์๋ฅผ ์ฝ๋์์ ๊ณ ์ณ๊ฐ๋ฉฐ ๋ฐฐํฌํ๋ฉด ๊ฐ๋ฐ์ฉ๊ณผ ์ด์์ฉ์ด ๋ค์์ด๋ ์ฌ๊ณ ๊ฐ ๋๋ฏ๋ก, ๊ฐ์ ์ฝ๋์์ ๋น๋ํ ๋ ํ๊ฒฝ์ ๊ณ ๋ฅด๋ flavor๋ฅผ ์ด๋ค
- Android๋
productFlavors(applicationIdSuffix๋ก ์ฑ ID๋ฅผ ๋ถ๋ฆฌ), iOS๋ ๋น๋ ์ค์ ๊ณผ ์คํด์ ๋ง๋ค๊ณ , ์คํด ์ด๋ฆ์--flavor์ด๋ฆ๊ณผ ๊ฐ์์ผ ํ๋ค flutter run --flavor dev์ฒ๋ผ ์คํํ๊ณ , ์ค์ ์ด ๋น ์ก์ ๋๋ ์๋ฌ ๋ฉ์์ง๊ฐ ์ด๋ ํ๋ซํผ์์ ๋น ์ก๋์ง ์๋ ค์ค๋ค- Dart์์ ์ฝ๋ ๊ฐ์
--dart-define์ด๋--dart-define-from-file๋ก ๋๊ธฐ๊ณString.fromEnvironment('FLAVOR', defaultValue: ...)๋ก ์ฝ์ผ๋ฉฐ, ๊ธฐ๋ณธ๊ฐ์ ์ด๋ ์ชฝ ์ฌ๊ณ ๊ฐ ๋ ๋ฌด์์ด์ง ๋ณด๊ณ ์ ํ๋ค - ์ฝ์ ์ค์ ์
AppConfig๋ก ๋ฌถ์ด์ provider๋ก ๋ด๋ ค์ฃผ๋ฉด, ํ๊ฒฝ์ด ๋ฐ๋์ด๋Dio๊ฐ์ ์ฝ๋๋ ๊ณ ์น์ง ์๋๋ค dart-define๊ฐ์ ์ฑ์ ๊ทธ๋๋ก ๋ค์ด๊ฐ๋ฏ๋ก ๋น๋ฐ ํค๋ฅผ ๋ฃ์ง ์๊ณ , Firebase ์ค์ ํ์ผ์ flavor ํด๋์ ๋ฐ๋ก ๋๋ค
๋ค์ ๊ธ์์๋ ๋ง๋ ์ฑ์ ์ค์ ๋ก ์ฌ์ฉ์์๊ฒ ๋ด๋ณด๋ด๋ ์คํ ์ด ๋ฐฐํฌ(Google Play, App Store)๋ฅผ ๋ค๋ค๋ณด๊ฒ ๋ค.
