Flutter Flavor 사용해보기

Uno·2022년 12월 21일
1

flutter

목록 보기
7/16

이전 글 개발환경 분리는, 사실 이글 보여주려고..

개요

모바일 앱 개발 할 때, 개발환경과 운영환경을 사람이 하나하나 조작한다면, 매번, 해당 환경 변경되는 부분이 아는 사람이 있어야 합니다. 게다가, 실수로 잘못 배포라도 하게 되면... 쉽지 않은 경험을 할 수도 있습니다.

모바일 앱 개발을 하면서, 개발환경 / 운영환경 을 다르게 설정하여 앱을 다르게 제공해줘야할 때가 있습니다.

  • 테스터가 테스트 환경으로 설정된 앱을 요구할 때
  • 고객사가 자신의 앱을 확인해보고 싶을 때 (하지만 현재 서비스에는 영향 없도록)
  • 타 분야 개발자가 자신이 개발한 내용이 앱에 반영된 사항을 보고 싶을 때
  • 광고가 있는 버전의 앱 vs 광고가 없는 버전의 앱
  • ... etc

이런 경우, 매번 해당 서비스에 맞춰서 새롭게 빌드해서 앱을 건내주게 되면, 실수할 우려가 있을 뿐만 아니라, 어느 부분이 환경변경되어야 하는지 매번 암기하고 있어야 합니다.

이런 불편한 작업들을, 편리하게 해주는 도구가 Flutter 에서는 Flavor 라는 도구를 사용하여 해결합니다.

이전 방식

예를 들어 기존에는 아래와 같이 관리하고 있었다고 가정합시다.

const BAES_URL = "api.prod.com"
// const BAES_URL = "api.dev.com"
// const BAES_URL = "api.test.com"
  • 위 코드처럼, 서버 도메인을 구성하고, 필요할 때마다 값을 변경합니다.
main() {
	final repository = Repository(baseURL: BASE_URL);
	runApp(MyApp(repository));
	...
}
  • 예를들면, 위 코드처럼, BASE_URL 이란 값을 주입해서 사용하곤 했습니다.
  • 배포해야하는데, 만약 실수로 BASE_URL 을 안바꾸면, AppStore 에 잘못 올리게되고, 참사가 일어날 수도 있습니다...

그래서 애초에 빌드 환경을 나눠버리면, 주석을 잘못해서 실수하는 일은 없겠죠.

iOS 의 경우, Scheme 이라는 단위로 해당 설정을 컨트롤하고, 각각 다른 Config 파일을 지정하곤 했습니다. Flutter 에서는 그 Config 를 기반으로 하되,
개발환경을 분리하는 방법은 각 프레임워크 혹은 IDE 별로 제공하는 방법이 있습니다.
Flutter & Android 의 경우, "Flvaor" 를 사용하곤 합니다.

정리하면, Xcode 의 분리 방식을 Flavor 에 추가하여, iOS & Android 의 개발 환경을 동시에 관리한다고 보면 되겠습니다.

flavor 사용하기

Flavors helps us to create builds for different instances of our app. For example, we can create a flavor for development, a flavor for production and another flavor for a demo of the app. In this way we can create different flavors, and thus have different instances of our apps before publishing it on the App Store and Google Play.

Flavors 를 통해서 다른 환경의 앱 인스턴스를 편리하게 빌드할 수 있습니다.

만약 개발환경을 "prodction / test / dev" 3 단계로 구분을 했다면,
커멘드 라인 입력을 통해 손쉽게 구분해서 바로 빌드 할 수 있습니다.

flutter run --flavor {셋팅한 개발환경 이름}

ex)
flutter build --flavor development

추가로 관련된 상태값들도 일괄적으로 주입해주면, 정적 상태값들도 관리가 쉬워집니다.

Android Setting

1. build.gradle 를 설정한다.

  • flutter 폴더 네비게이터에서 아래 경로로 이동합니다.
현재FlutterProject폴더/android/app/src/build.gradle
  • 아래 코드를 작성합니다.
android {  
    ...(기타 설정 내용들)...
    
    // 모든 flvaor 는 반드시 이름이 정해진 아래와 같은 "Dimension" 이 있어야합니다  
    // 이 이름이 각각의 flavor 를 구분하는 ID 가 됩니다.  
    flavorDimensions "app"  

	// dev 와 product 로 환경을 구분했습니다.
    productFlavors {  
        dev {  
            dimensions "app"  
            applicationId "com.example.flavor_example"  
            resValue "string", "app_name", "DEV falvor example"  
        }  
        product {  
            dimensions "app"  
            applicationId "com.example.flavor_example"  
            resValue "string", "app_name", "falvor example"  
        }
    }  
}
  • flavorDimensions 는 "빌드" 의 구분의 기준을 정의한다고 생각하시면 됩니다.
    - "api" 라고 이름을 설정하고, API 서버에 따라서 구분하거나, "mode" 라고 이름을 설정하고, 앱의 유료 / 무료 에 따라서 지을 수도 있습니다.
  • applicationId 는 각각의 flavor 의 ID 입니다. 이 설정을 해주면, 동일 디바이스에 다른 앱 ID 로 함께 설치될 수 있습니다.

2. Android Manaifest 를 설정한다.

경로는 다음과 같습니다.

android/app/src/main/AndroidManifest.xml
  • 바로 이전 스탭에서 정의한
    resValue "string", "app_name", "flavor example" 을 이곳에서 사용합니다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"  
    package="com.example.flavor_example">  
   <application  
        android:label="@string/app_name" 
        ...
    </application>  
</manifest>

3. MainActivity 를 변경한다.

경로

android/app/src/main/kotlin/프로젝트명/MainActivity.kt
package com.example.flavor_example  
  
  
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)  
        }  
    }  
}
  • 위 코드를 작성하는 이유는, 이전 스탭에서 build.gradle 에서 설정한 값을 사용하기 위함입니다.

iOS Setting

1. Configuration 을 생성한다.

Flutter 폴더에서, iOS 를 우측클릭하여, Flutter > "open xcode" 를 통해 Xcode 를 켭니다.
(그냥 폴더에 가셔서 직접 .proj 파일을 실행해도 됩니다.)

최상단에 있는 "Runner" 파일을 누르고 프로젝트 클릭 후, "Configurations" 를 세팅합니다.

저는 Debug, Release, Profile 을 각각 Duplicate 해서 구성했습니다.

2. Configration 에 맞게 Scheme 을 설정한다.

위 이미지와 같이, "Edit Scheme" 을 클릭해서 설정합니다.

이후 "Duplicate Scheme" 을 눌러서, 현재 있는 Scheme 을 복제합니다.

우측에 "Build, "Run", "Test", "Profile", "Analyze", "Archive" 가 있는데, 해당 Scheme 의 이름에 맞게 dev 인지 product 인지 설정합니다.

그러면 Xcode 의 탭바영역의 Scheme 설정 화면에 다음과 같이 표시됩니다.

Build Setting 수정

1. Target 의 Building Setting - Packaging

  • 최초에는 위 화면처럼 구성되어 있습니다. 여기서 그대로 실행하게 되면 모두 같은 ID 를 사용하게 되어, 다른 환경의 Scheme 을 다운로드 하게 되더라도, 하나의 앱만 설치됩니다.
  • 그러므로, 이 부분을 다르게 수정해주어야 합니다.

저는 dev 환경에서는 뒤에 .dev 를 추가했습니다.

2.Bundle display name 을 수정한다.

Bundle display name 은 앱에 보이는 이름입니다. 환경에 따라서 .dev 가 붙을지 말지가 결정됩니다. 해당 값은 변수 값이므로, 변수로 설정해줘야 합니다.

  • 아래 이미지는 최초의 이미지 입니다.
  • 추가하는 곳은 "Info.plist" 입니다.
  • 변경된 이미지 입니다.

3. 사용자 정의 값에 "APP_FLAVOR" 와 "APP_NAME" 을 추가한다.

Info.plist 에서 사용한 값을 정의해주어야 하는데, 다시 이전에 갔던 곳으로 이동합니다.

경로

Runner/Target/Build Settings

위 경로로 이동한 이후, User-Defind 을 2 개 추가합니다.
각각의 이름은 "APP_FLAVOR" 와 "APP_NAME" 입니다.

추가한 모습입니다.

flavor 는 Android 에서 사용하는 개념입니다. iOS 의 경우 scheme 을 통해서 환경을 분리하고 관리합니다. 그러므로, Flutter 에서 iOS 를 지정하기 위해서는 XCode 에서 설정한 값을 flavor 에서 알 수 있도록 해야합니다. 그래야 Flutter Project 에서 설정한 값이 iOS 도 컨트롤이 가능하니까요.

이렇게 iOS 에서 설정한 값을 Dart 로 가져오는 방법이 "Platform Channel" 입니다. 그 중 "MethodChannel" 을 사용할 예정입니다.

4. iOS 에서 FlutterViewController 실행 시점에, flavor 관련 값을 전달한다.(in AppDelegate.swift)

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)  
            }  
}
  • 위 코드 중에서 FlutterMethodChannel 이란 부분이 Swift 코드와 dart 코드가 커뮤니케이션 하도록 도와주는 객체 입니다.
  • MethodChannel 의 구조는 아래 이미지와 같습니다.
  • 이전에 MainActivity.kt 에서 처리해준 것도 동일한 로직입니다.

위 설명과 위 코드의 예시 및 자세한 설명이 궁금하시면 아래 공식문서를 참고하시면 됩니다.
(참고로, MethodChannel 은 비동기이지만, MainThread 에서만 동작합니다.)
공식문서 링크

  • 환경설정 값 변경에 따른 앱 이름 변경
    - 실제로 ID 도 다르기 때문에, 덮어쓰기 될 우려는 없습니다.

촤종 결과물

  • 하나의 코드에 두 개의 빌드

참고자료

profile
iOS & Flutter

0개의 댓글