모바일 앱 개발시 개발 환경과 운영 환경의 세팅을 일일이 수동으로 변경한다면 매우 번거로운 일이다. 또한 실수로 운영 빌드시 개발 환경 세팅으로 앱이 배포된다면 정말 아찔한 경험을 할 수 있다.
Flutter 의 flavor 를 이용한다면 개발/운영 환경 설정을 깔끔하게 분리할 수 있다. 덤으로 한 디바이스에서 개발/운영 앱을 동시에 설치 할 수 있어 테스트 시에 앱을 지우고 재설치하는 번거로움을 줄일 수 있다.
Flavor 란?
사실 flavor 는 Android 에서 동일한 소스로 다른 버전의 앱을 빌드 할 수 있도록 해주는 개념이다.
- 개발/운영 에 따른 접속 서버 url 구분 및 다양한 key, token 값 구분
- 유료/무료 앱 설정
- 광고 유무 설정
위와 같은 값들을 하드코딩 하는게 아닌 build 시 설정에 따라 적용하게 된다.
iOS 의 경우 build scheme 을 이용해서 구현할 수 있다. Flutter 의 경우 구글의 플랫폼이다 보니 좀 더 Android 의 향기가 짙게 나는게 사실이다.
Android Native 설정
build.gradle 설정
android/app/src/build.gradle 에서 android.productFlavors 와 android.flavorDimensions 에서 설정할 수 있다.
- flavorDimensions : 빌드의 구분. 기본적으로 하나 이상의 dimension 이 존재해야 함.
- applicationId : 각 flavor 를 유일하게 만들어주는 id 값. 이 값을 설정 해주면 동일한 디바이스에 동시에 앱을 설치 할 수 있다.
android {
// ... all existing things like `sourceSets`, ...
flavorDimensions "app"
productFlavors {
dev {
dimension "app"
applicationId "com.example.flavor_test.dev"
resValue "string", "app_name", "DEV flavor test"
}
product {
dimension "app"
applicationId "com.example.flavor_test"
resValue "string", "app_name", "flavor test"
}
}
}
Android Manifest 설정
flavor 에 맞게 app 이름을 변경 될 수 있도록 label 값을 변경하자
android:label="@string/app_name"
MainActivity 변경
build.gradle 에 의해 설정된 값을 method channels 를 통해 Flutter 에서 사용할 수 있도록 전달 하자.
package com.example.flavor_test
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "flavor").setMethodCallHandler {
call, result -> result.success(BuildConfig.FLAVOR)
}
}
}
iOS Native 설정
custom build Configurations 생성
iOS 의 설정은 Xcode 에서 하는것이 편하다.
iOS workspace 를 Xcode 에서 오픈하고 아래와 같은 설정을 해주자
- 좌측 Navigation tab 에서 Runner Project 선택
- 메인 창에서 Runner PRJECT 선택 (TARGETS 아님)
- info 탭 선택
- Configurations 영역에서 Debug, Release, Profie 을 복사 후 이름 변경.
ex) Debug-dev, Release-dev, Profile-dev, Debug-product, Release-product, Profile-product
custom build schemes 설정
이제 위에서 설정한 configuration 을 build scheme 에 연결 시켜 보자
- main toolbar 에서 Product > Scheme > Manage Schemes 를 선택
- Manage Schemes 창에서 Runner 를 선택후 복사하자. 하단에 + 버튼으로 할 수 있고, + 버튼 옆에 더보기 버튼 클릭시 Duplicate 가 존재한다.
- 복사한 scheme 은 dev 라는 이름으로 변경하고 Edit 버튼을 눌러 Edit 창에서 각 세션 (Run, Text, Profile, Analyze, Archive) 에 선택된 build configuration 이 dev 임을 확인하자.
- 똑같은 방법으로 scheme 을 하나 더 만들어서 product 라고 이름을 변경하고, Edit 창의 각 세션이 이번엔 -product 임을 확인 하자
이제 scheme 설정이 끝났다. 설정이 잘 마무리 되었다면, Xcode 상단에 아래와 같이 선택이 가능할 것이다. 사실 여기서 run 버튼 (플레이 화살표) 을 통해서도 해당 scheme 으로 실행이 가능하다. 하지만 CLI 형태로 설정한다면 CI/CD 설정을 간편히 할 수 있어 CLI 를 사용하는것을 추천한다.
![](https://blog.kakaocdn.net/dn/cGHD0G/btssHy26GAi/UC9tRASWelql3TGKxH09h1/img.png)
Build Setting 에 설정 추가
이제 각 빌드 scheme 별로 다른 설정 값을 추가 해주자
- Identifier 수정
Android 에 applicationId 을 변경해주었다면, iOS 에서는 Identifier 값을 수정해주면 된다. build Setting 창에서 identifier 라고 검색하면 Product Bundle identifier 내용이 나올 것이다. 이 내용을 dev, product 환경에 맞게 변경 해주자
(dev 빌드시 identifier 뒤에 .dev 를 붙여 줄 것이다.) - app name 추가
이제 각 빌드마다 앱 이름을 다르게 설정하기 위해 App_Name 값을 추가 해 준다.
좌측 상단에 + 버튼을 눌러 Add User-defined setting 을 선택하면 사용자가 custom 하게 설정을 추가 할 수 있다.
APP_NAME 이라고 추가하고 각 빌드 scheme 별 이름을 설정하자.
이제 info.plist 에 Bundle display name 을 $(APP_NAME) 로 변경 시켜주면 된다.
![](https://blog.kakaocdn.net/dn/H02Bs/btssGufNCLP/ya6Wdk5QKrOk5ZYGglwU5K/img.png)
- app flavor 추가
flavor 의 경우 Android 에서의 개념이고 iOS 는 build scheme 이라고 했다. 그런데 왜 flavor 값을 추가 해 주어야 할까?
아래에서 설명하겠지만 Flutter 소스에서 Method Channel 을 이용해서 native 의 build 값을 flavor 로 가지고 올 수 있도록 할 계획이다. 이 때를 위해 각 빌드 scheme 에 맞는 flavor 값을 추가 설정 해주어야 한다.
APP_NAME 을 추가했던거 처럼 User-defined setting 을 추가해서 APP_FLAVOR 을 추가하자. 그리고 각 scheme 에 맞게 flavor 값을 설정 해 주자
AppDelegate.swift 수정
Android 마찬가지로 build 설정을 flutter 에 전달하기 위해서 AppDelefate.swift 에서 Method channel 을 이용하여 flavor 를 전달하자
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller = window.rootViewController as! FlutterViewController
let flavorChannel = FlutterMethodChannel(
name: "flavor",
binaryMessenger: controller.binaryMessenger)
flavorChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
// Note: this method is invoked on the UI thread
let flavor = Bundle.main.infoDictionary?["App-Flavor"]
result(flavor)
})
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Flutter
flavor 에 따른 설정 세팅
flutter code 에서는 flavor 에 따른 설정을 가지고 오는 부분이 구현 되어야 한다.
main.dart 에서 실행시 아래와 같이 flavor 값을 전달 받은 다음 설정을 해주자
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// App flavor 값 조회
// flavor = 'dev' | 'product'
String? flavor = await const MethodChannel('flavor')
.invokeMethod<String>('getFlavor');
Config(flavor);
runApp(MyApp());
}
class Config{
final String baseUrl;
final String token;
Config._dev():
baseUrl = '', // dev url
token = ''; // dev token
Config._product():
baseUrl = '', // product url
token = ''; // product token
factory Config (String? _flavor) {
if (_flavor == 'dev'){
instance = Config._dev();
(_flavor == 'product'){
instance = Config._product();
}else {
throw Exception("Unknown flaver : $_flavor}");
}
return instance;
}
static late final Config instance;
}
실행
위에서 잠깐 말했듯이 Android Studio 나 xCode 에서 flavor 를 선택후 실행을 할 수 있다. CLI 를 이용한다면 CI/CD 를 활용할 수 있고, 무엇보다 터미널 창에서 command 로 실행하는것이 간지(?) 가 난다. 쉽다
flutter run --flavor [flavor 명]
위 명령어를 실행하면 아래와 같이 다른 이름의 앱이 설치 되는것을 볼 수 있다. flavor 에 따라 앱 아이콘도 변경 가능하지만 이는 이번 글에서 소개하지 않겠다
![](https://blog.kakaocdn.net/dn/dgFXx2/btssLGzIZtc/jh7LGT1heK3uUSOLWkJtgK/img.png)
![](https://blog.kakaocdn.net/dn/FZOpj/btssPv45JfX/khh85KD1vLWhqtTMo3kDo0/img.png)