Flavor 빌드 변형 (Firebase, Naver, Kakao, Apple, Google, GCP, API , App Icon)

Create Flavors of a Flutter app
Google Cloud Platform
Naver Developers
Kakao Developers
Kakao Dev Talk

Singleton Pattern 이란 ?
Firebase 세팅하기 - Flutter 3.0 이후
Firebase 세팅하기 - Flutter 3.0 이전
Firebase Dynamic Links 란?
App Icon 만들기

이번에는 Flavor 빌드 변형에 대해서 자세히 다뤄보도록 하겠다.

Flavor는 안드로이드에서 사용하는 빌드 변형 방식인데, Flutter는 Android/IOS 플랫폼 모두를 사용하여야 해서 IOS에도 안드로이드의 빌드 변형을 사용해야 한다.

주로 Flutter의 빌드 변형을 구글링 하면 나오는 방식이 Flavor이고, Flutter 공식 문서에도 빌드 변형을 Flavor로 하라고 나와있다.

2년전에 Flavor를 한 번 작업해본 기억이 있는데, 지금까지 사용을 안하고 있다가 Flavor에 대해서 자세히 배워보고 싶어서 글을 작성하게 되었다.

Flavor 작업에 대해서 최대한 자세하게 다룰 예정이고, Flavor 환경 분리 뿐만 아니라 main() 함수 분리 및 Firebase, Social 로그인 등의 연동 방법에 대해서도 살펴볼 예정이다.

작성하는 글 내용 중 잘못된 내용이 있다면 댓글로 남겨주시길 바랍니다.

Flavor ?

먼저 빌드 변형을 왜 해야할까 ? 굳이 하지 않고도 충분히 배포하고 운영할 수 있는데, 왜 사용해야 하는지에 대해서 의문이 생길 수도 있다.

개발 초기 단계에서는 빌드 변형에 대해서 크게 중요하게 생각하지 않게 되는데, 나중에 프로젝트의 규모가 커지고 다양한 써드파티 및 SDK가 세팅되게 되면 그 때가서 작업을 하는게 더 힘들다....

프로젝트 생성시 CI/CD, Flavor 등은 미리 세팅하고 가는게 맘편한것 같다.

Android/IOS 두 플랫폼 모두 같은 앱을 설치하려고 하면 설치가 되지 않거나 덮어씌어서 설치를 진행하게 된다.
시스템이 appbundle/ipa 파일을 설치할 때에 고유 식별자를 사용하여 앱을 구분하기 때문에 이러한 현상이 나오게 된다.
Flavor는 이런 식별자를 각각의 환경 구성을 다시 해주는 방법으로 작동을 시키게 되는데, 보통은 고유 식별자 뒤에 suffix 또는 applicationId를 추가하여 고유 식별자를 다르게 빌드를 하는 방법을 사용하고 있다.

Flavor로 앱을 여러 개 설치하면 뭐가 좋을까 ? 게임 앱을 개발했다고 가정하고, 무료 버전인 앱과 유료 버전 앱을 따로 출시한다고 해보자. 그럼 프로젝트를 두 개 생성해서 관리하게 되는데, 이런 불필요한 작업을 하지 않도록 동일 코드 베이스를 사용해서 하나의 프로젝트를 2개의 앱으로 나누는 것이다.

앱은 2개로만 분리하는게 아닌 원하는 만큼 분리가 가능하다.

또 다른 경우는 QA 환경을 만들고 싶을 때에, 팀원들이 스토어에 배포된 운영 앱을 지우고 앱을 설치해서 QA를 진행하고 다시 스토어에 앱을 다운받는 과정을 없애기 위해 분리하여 배포를 해주면 운영 앱과 QA 앱 등을 동시에 설치할 수 있게 되고, fastlane을 구축할 때도 서버별 환경을 분리하여 운영할 수 있기 때문에 필요한 작업이라고 생각이 된다.

하지만 단순히 분리만 한다고 해도 제대로 작동이 되지 않는데, 문제는 SDK, 써드파티 등을 프로젝트에 연결할 때에 고유 식별자를 사용하는 경우에 문제가 된다.
예를 들어 "com.tyger.flavor"라는 식별자로 프로젝트를 생성해서 해당 식별자를 연결시켰는데, dev 앱을 분리 하였다고 가정하면 분리된 dev 앱의 식별자는 "com.tyger.flavor.dev"의 형태로 식별자가 다르게 되어서 연결이 되지 않기 때문에, 각각의 환경을 세팅해줘야 하는 번거로움도 있다.

여기서는 운영, QA, 개발 이렇게 3개의 환경으로 구성할 예정이다.

이제 천천히 Flavor 환경을 만들어 보도록 하자. 새로운 프로젝트를 생성해서 따라해보는 것을 추천한다.

Android

먼저 안드로이드의 환경을 분리하도록 하자. 안드로이드는 어렵지 않게 환경 분리를 할 수 있다.

build.gradle

android > app > build.gradle

gradle 중간에 보면 android 태그가 보일 것이다. android 태그 안에서 환경을 분리시켜 줘야 한다.

android 태그 안에서 flavor 환경을 생성해 주도록 하자.

먼저 dimension을 설정해야 하는데, dimension을 타겟으로 빌드를 하게 되어서, 상단에 flavorDimensions를 추가해 주고 dimension은 "build-type"으로 해주었다.

productFlavors 태그를 추가해서 분리하려고 하는 환경을 추가로 넣어주면 된다.

태그 안의 dimension과 flavorDimensions은 반드시 일치하여야 한다.

applicationId 부분에 값을 추가해 주는데, prod 환경은 실제로 스토어에 배포될 환경이기 때문에 추가하지 않아야 한다.
prod에 applicationId를 추가하게 되면 "com.tyger.test.prod" 형태로 패키지 네임이 변경되기 때문에, .prod가 없는 형태로 생성하여야 한다.

resValue 부분을 사용해서 앱의 네임을 지정하게 해줄 것인데, "string", "app_name", "{your_app_name}" 형태로 추가해주자.

android {
   	...
    
    flavorDimensions "build-type"

    productFlavors {
        dev {
            dimension "build-type"
            applicationIdSuffix ".dev"
            resValue "string", "app_name", "TYGER_DEV"
        }

        qa {
            dimension "build-type"
            applicationIdSuffix ".qa"
            resValue "string", "app_name", "TYGER_QA"
        }

        prod {
            dimension "build-type"
            resValue "string", "app_name", "TYGER"
        }
    }
}

AndroidManifest.xml

android > app > src > main > AndroidManifest.xml

Manifest 파일에서도 앱 네임이 지정될 수 있도록 수정해 주도록 하자.
application 태그 안에 android:label 태그 부분을 아래와 같이 수정해주면 된다.

android:label="@string/app_name"

Run

이제 실행을 해볼 것인데, 터미널 명령어로 실행을 해서 정상적으로 실행이 되는지 확인해보자.

flutter {mode} --flavor {build_tyge} -t {target_file}

위의 형태가 flavor로 실행할 수 있는 명령어 이다. {mode}는 flutter 실행 모드로 run 또는 build 모드가 있고, {build_type}은 우리가 추가해준 prod, qa, dev를 넣어주면 된다.
아직 우리는 main 함수를 분리하지 않았기에 {target_file} 명령어는 없어도 된다.

flutter run --flavor dev
flutter run --flavor qa
flutter run --flavor prod

명령어로 앱이 3개가 설치되는지 확인해 보자. 위의 명령어로 앱을 빌드하면 debug 모드로 빌드가 되고, 만일 release 모드로 디바이스에 바로 설치하고 싶다면 "flutter run --release --flavor prod"라고 입력하면 된다.


정상적으로 환경 분리가 끝이 났다.

이렇게 터미널 명령어가 아닌 IDE 환경에서도 실행할 수 있어야 하기 때문에, VS Code와 Android Studio에서 Run & Debug 환경을 설정하는 방법에 대해서 살펴보자.

VS Code

VS 코드에서 프로젝트에 생성된 .vscode 폴더를 보면 setting.json 파일이 보이게 되는데, 여기에 configurations를 사용하여 빌드를 할 수있는 launch.json 파일을 추가해주면 된다.

만일 launch.json 파일이 있다면 아래 단계는 진행하지 않아도 된다.

실행 및 디버그 탭을 클릭하여 launch.json 파일 만들기를 눌러주자.

Dart & Flutter를 클릭하면 launch.json 파일이 생성된 것을 확인할 수 있다.

configurations 안에 있는 json 구조는 전부 지워주고 아래와 같이 설정 해주면 된다.

name에는 빌드 환경의 이름을 넣어주고, args 배열 안에 --flavor, {build_type}을 추가해주면 해당 디버깅을 진행할 때 아래 환경을 구성하여 디버깅 빌드가 진행이 된다.

"configurations": [
        {
            "name": "dev",
            "request": "launch",
            "type" : "dart",
            "args" : [
                "--flavor",
                "dev",
            ],
        },
        {
            "name": "qa",
            "request": "launch",
            "type" : "dart",
            "args" : [
                "--flavor",
                "qa",
            ],
        },
        {
            "name": "prod",
            "request": "launch",
            "type" : "dart",
            "args" : [
                "--flavor",
                "prod",
            ],
        },
    ]

실행 및 디버그 탭으로 이동해 보면 각 분리 환경으로 디버깅을 진행할 수 있게 설정이 끝났다. 정상적으로 실행이 되는지 테스트 해보자.

만일 릴리즈 모드를 추가하고 싶다면 "flutterMode" : "release"를 추가하여 구성을 해주면 된다.

Android Studio Code

Android Studio를 사용하고 있다면 Edit Configurations...를 클릭해서 환경을 추가하여야 한다.

상단에 복사 버튼을 클릭해서 main.dart 파일을 3개로 늘려주자.

상단에 name 부분과 Build flavor 부분에 환경을 넣어주면 된다.

이제 정상적으로 각 환경에 따른 디버깅 빌드 환경이 구성이 되었다.

IOS

이번에는 IOS에도 Flavor를 세팅해주도록 하자.

XCode를 사용해서 설정을 해야 하는데, Xcode에 익숙치 않으신 분들은 천천히 잘 따라하시길 바란다. 안드로이드 보다 해줘야 하는 작업이 더 많다...

ios 폴더 구조를 보면 Flutter 폴더 아래 Debug와 Release 파일이 있는 것을 확인할 수 있다.

먼저 Project의 Runner 파일의 Configurations를 추가해주는 작업을 해주도록 하자. 기본은 아래와 같은 형태로 생성되어 있을 것이다.

Debug 부분을 클릭하고 "+" 추가 버튼을 클릭하면 아래와 같은 Duplicate를 할 수 있게 해준다. 이렇게 2개를 복제해 주고 dev, qa를 추가해주면 된다.

아래에 이미지 처럼 복제를해서 환경을 추가해주면 된다. 나머지 Release, Profile도 복제를 진행해서 똑같이 만들어 주면 된다.

복제를 진행했으면, 이제 원래 Debug, Release, Profile에 prod로 네임을 수정해 주면된다.

아래와 같이 추가가 되어있다면 정상적으로 추가를 진행한 것이다.

이제 Configurations를 만들었으니, Xcode의 Scheme을 추가해주면 된다.

상단의 Scheme을 보면 Runner로 되어있을 것이다. 여기 부분에 Configurations을 추가하여 Scheme을 추가하고 싶은 환경만큼 추가해주면 된다.

Runner를 클릭하여 Manage Schemes... 클릭해서 Scheme을 추가해보자.

Runner를 클릭한 상태에서 하단의 버튼을 클릭하면 Duplicate를 진행할 수 있다.

Copy of Runner 부분을 지우고, dev로 넣어주도록 하자.

좌측 탭 부분을 보면 Run, Test, Profile, Analyze, Archive 부분에 현재 prod로 되어있는 것을 확인할 수 있는데, Runner를 복제해서 prod로 되어있는 것이다.

이 부분을 전부 dev로 변경해주면 된다.

이렇게 dev로 변경해주고, 같은 방법으로 qa, prod도 추가해주도록 하자.

정상적으로 추가를 했으면 이제 Shared 부분을 체크해 주어야 한다. Runner 파일은 삭제해도 되고, Show, Shared 부분을 비활성화 해서 사용하지 않도록 해줘도 된다.

이제 이어서 Android에서 Package Name의 applicationIdSuffix를 추가해서 Package Name을 변경한 것처럼 Bundle ID도 같은 작업을 진행해주어야 한다.

TARGETS의 Runner로 이동하여 Build Settings 탭을 눌러주자. Basic, Combined를 선택하자.

Packaging 부분을 보면 Product Bundle Identifier 부분에서 id 마지막 부분을 아래와 같이 수정해주면 된다.

마지막으로 앱 네임을 변경해주도록 하자.

추가를 클릭해서 User-Defined를 추가해주면 된다.

APP_NAME으로 해주고 각각의 환경에 따른 앱 네임을 등록해주자.

Info.plist 파일로 이동해서 Bundle display name을 우리가 지정한 $(APP_NAME)이라고 변경해주자.

Xcode에서 실행을 해보면 정상적으로 실행이 되는 것을 확인할 수 있다. VS Code에서도 실행을 해보자.

터미널 명령어로도 정상적으로 실행이 되는지 확인해봐야 한다.

flutter run --flavor dev


Android와 IOS에 Flavor가 정상적으로 적용되었다. 이번에는 main 함수 분리를 진행하는데, 만일 main 함수 분리가 필요하지 않다면 진행하지 않으셔도 됩니다.

Flutter는 앱 첫 시작시 main() 함수를 찾아서 앱을 실행하게 된다. 그런데 우리가 Flavor로 환경은 나눴지만 결국 동일 코드베이스가 되어있는 상태인 것이다.

main 함수를 하나만 사용하고 싶다면 Platform Channel로 연결해서 현재 빌드 환경을 수신받아서 처리해도 되는데, 여기서는 main 함수를 분리해서 환경에 맞는 다른 main에서 앱이 실행되게 해주는 작업을 진행할 것이다.
실제 테스트 앱이라 서버 전송 API는 없지만 있다는 가정하에 진행해 보겠다. prod 앱에는 운영서버를 사용하고, dev와 qa 앱에는 개발서버를 사용하는 방법으로 main 함수를 분리하도록 하겠다.

main 분리

main 함수는 굳이 분리하지 않고 사용하여도 된다. 하지만 각 환경에 따라 서버 환경이 다르게 적용이되고, Firebase 또는 SDK & 써드파티 서비스를 사용하는 경우 고유 식별자가 다르게 되어 분리해서 관리를 하여야 하는 경우가 있어 분리하여 사용하는 것을 추천한다.

main을 분리하여 실행하는데, 해당 main 함수를 실행하는 세팅이 필요하다.

Enviroment

Singleton Pattern 이란 ?

Enviroment 클래스를 싱글톤 패턴을 사용하여 초기 인스턴스 생성 후 앱의 전역에서 호출 되더라도 기존 인스턴스를 리턴하여 사용할 수 있도록 하여야 한다.

먼저 enum을 사용하여 각 빌드 환경에 대한 서버 환경을 구성하도록 하겠다.

flutter 3.0인 경우에는 아래 코드가 문제가 없지만 아직 flutter 2를 사용 중이라면 해당 코드가 적용이 안될 것이다.

dev, qa 환경에서와 prod 환경에서 서버 url을 다르게 적용해 주었다.

lib > _env > enviroment_type.dart

enum EnviromentType {
  dev(type: "DEV", url: "https://tyger_dev.com"),
  qa(type: "QA", url: "https://tyger_dev.com"),
  prod(type: "PROD", url: "https://tyger.com");

  final String type;
  final String url;

  const EnviromentType({
    required this.type,
    required this.url,
  });
}

아래 코드로 Enviroment 클래스를 생성하면 된다.

lib > _env > enviroment.dart

class Enviroment {
  static late Enviroment _instance;
  static late EnviromentType _type;

  factory Enviroment.init(EnviromentType type) {
    _type = type;
    _instance = const Enviroment._internal();
    return _instance;
  }

  const Enviroment._internal();

  void run() => runApp(const MyApp());

  static Enviroment get instance => _instance;
  static EnviromentType get enviroment => _type;
}

위의 코드에 대해서 잠깐 살펴보자면 Singleton Pattern의 기본 사용 코드 구조이다.

class Enviroment {
  static Enviroment instance = Enviroment._internal();
  factory Enviroment() => instance;
}

기본 싱글톤 패턴 구조에 초기 인스턴스시 EnviromentType을 필수로 받아와야 해서 코드를 수정해준 것이다.

또 하나 주목할 점이 해당 클래스 안에서 runApp을 호출하고 있다는 것이다.

void run() => runApp(const MyApp());

이제 빌드 환경에 맞는 main 함수를 생성해 주도록 하겠다.

main 함수가 실행될 때에 우리가 생성해준 Enviroment 클래스를 호출해주면 된다.

main_dev.dart

lib > main_dev.dart

main() => Enviroment.init(EnviromentType.dev).run();

main_qa.dat

lib > main_qa.dart

main() => Enviroment.init(EnviromentType.qa).run();

main_prod.dart

lib > main_prod.dart

main() => Enviroment.init(EnviromentType.prod).run();

간단하게 main 함수를 생성해주고 이제 실행해 보도록 하자.

이제 부터는 터미널 명령어에 -t 타겟 파일을 넣어서 실행해주어야 한다.

flutter run --flavor dev -t lib/main_dev.dart

IDE에서 실행을 해보면 실행이 되지 않을 것이다. IDE 환경에 맞는 configuration을 수정해 주도록 하겠다.

VS CODE

program 부분에 타겟이 되는 main 파일의 경로를 넣어주면 된다.

.vscode > launch.json

"configurations": [
        {
            "name": "dev",
            "request": "launch",
            "type" : "dart",
            "args" : [
                "--flavor",
                "dev",
            ],
            "program": "lib/main_dev.dart",
        },
        ...
   ],    

Android Studio Code

이번에는 안드로이드 스튜디오를 사용하는 경우에 Configurations를 생성한 것과 동일하게 수정을 진행해주면 된다.

위에서 Configuration 생성시 main.dart 함수 경로가 entrypoint로 설정된 것을 확인할 수 있는데, 여기 부분의 파일을 환경에 맞는 파일로 변경해주면 된다.

XCode

XCode를 열고, TARGETS Runner 부분에서 앱 네임을 설정한 것과 같이 User-Defined를 추가해주면 된다.

FLUTTER_TARGET으로 생성하고 각 환경에 맞는 main 파일의 경로를 지정해 주자.

정상적으로 실행이 된것을 확인할 수 있다.


Multiple Firebase Enviroments

이번에는 Flavor 환경에서 Firebase 사용 방법에 대해서 알아보도록 하자.

Flavor를 사용해서 식별자가 변경되어 빌드가 되기에, Firebase가 정상적으로 작동되지 않을 것이다.
Firebase 프로젝트에 Flaovr 환경과 동일한 식별자로 앱을 생성해야 사용이 가능하다.

Firebase 프로젝트 생성하는 부분은 설명하지 않도록 하겠다.

Firebase 세팅이 필요하신 분들은 아래 공유한 글을 참고하시면 됩니다.
Firebase 세팅하기 - Flutter 3.0 이후
Firebase 세팅하기 - Flutter 3.0 이전

Firebase CLI를 사용해서 세팅을 진행할 예정이고, CLI 환경을 사용하지 않는 경우에는 대시보드에서 각각 생성해주면 된다.

Create Apps

먼저 프로젝트에서 환경에 맞는 앱을 등록하도록 하겠다. Bundle Id와 Package Name을 넣어줄 때에 Flavor 환경으로 변환된 값을 넣어주어야 한다.

flutterfire config \
--project={your_firebase_project_name} \
--ios-bundle-id={your_bundle_id} \
--android-app-id={your_package_name} 

먼저 dev 환경 앱을 Firebase에 등록하도록 하겠다.

flutterfire config \
--project=flutter-flavor-app-test \
--ios-bundle-id=com.example.flutterFlavorTest.dev \
--android-app-id=com.example.flutter_flavor_test.dev 

이렇게 앱이 정상적으로 실행이되고, GoogleService 파일도 플랫폼에 맞게 등록된 것을 확인할 수 있다. 여기서 중요한 부분이 있는데, dev, qa, prod 3개의 앱을 Firebase에 등록하여야 하기에 firebase_options.dart 파일의 이름을 변경해 주도록 하겠다.

이제 qa 환경도 동일한 방법으로 등록해주면 되고, 지금 상태로 앱을 추가할 때애 GoogleService 파일을 덮어쓰기 하는지 물어보는데, 대시보드에서 언제든지 다운 가능하기에 편하신대로 하시면 된다.

prod 환경에서는 식별자 뒤에 .prod 붙이면 안되고 그냥 고유 식별자 그대로 생성해야 한다 !

각 환경에 맞는 앱이 정상적으로 등록이 된것을 확인할 수 있다.

DefaultFirebaseOptions 파일도 각각 아래와 같이 넣어주었다.

추가적으로 firebase_app_id_file.json은 환경에 무관하고 프로젝트로 생성이 되는거라 그냥 생성되는 그대로 사용해도 됩니다.

Firebase 프로젝트에 3개의 앱을 등록하였다.

Firebase는 프로젝트를 연결할 때에 google-services.json, GoogleService-Info.plist 파일을 찾게 되는데, 환경이 다르게 되면 하나의 파일로 사용할 수가 없게된다.

Google Service 파일도 환경에 맞게 나눠줘야 하는데, Firebase의 모든 프로젝트에 꼭 Google Service 파일이 사용되는 것은 아니기에 필요에 따라 사용하지 않아도 된다.

Android

Android의 경우 google-services.json 파일의 태그를 확인해 보면 하나의 파일안에 3개의 앱 환경이 구성되어 있는 것을 확인할 수 있다.

Firebase 대시보드에서 파일을 우선 다운받자. Android 앱 3개 중 아무거나 다운받아도 된다. 어짜피 똑같은 파일이다.

google-service.json 파일을 확인해 보면 파일 안에 태그가 생성되있는 것을 확인할 수 있다.



아래 경로에 json 파일을 넣어주면 끝이다.

android > app > google-services.json

만약에 Flutter로 앱을 등록하지 않고 Android를 개별적으로 등록하였다면, json 파일이 다를 수 있다.

그럴 경우에는 각각의 json 파일을 다운 받아서 넣어주는데, dimension과 동일한 이름으로 폴더를 생성해야 한다.

IOS

이번에는 IOS에도 GoogleService-Info.plist 파일을 넣어주도록 하겠다. google-services.json 파일과는 다르게 GoogleService-Info.plist 파일은 우리가 생성한 환경마다 각각 태그가 다르게 구성되어 있어서, 분리를 해줘야 한다.

ios 폴더안에 firebase 폴더를 생성하고 dev, prod, qa 폴더를 만들었다.

ios > firebase > {enviroment}

Firebase 설정에서 GoogleService-Info.plist 파일을 각각 환경에 맞게 다운받아 생성한 폴더에 넣어주자.

Xcode를 열고 TARGET Runner의 Build Phases에서 추가 버튼을 클릭하여 Run Script를 눌러서 bash scrpit를 생성해 주도록 하곘다.

Scrpit의 이름은 자유롭게 넣어줘도 된다. 저는 "GoogleService-Info Enviroments"라고 해주었다.

Shell 안에 내용을 넣어주면 된다.

Shell안에 들어갈 bash 내용인데, 여기서 directory 경로가 저와 다르다면 수정은 하셔야 한다. 저랑 동일하게 폴더 구조를 생성했다면 아래 내용 복붙해서 사용하시면 된다.

environment="default"

echo "Configuration: ${CONFIGURATION}"
if [[ $CONFIGURATION =~ \-([^-]*)$ ]]; then
  flavor=${BASH_REMATCH[1]}
fi

echo "Flavor: $flavor"

GOOGLESERVICE_INFO_PLIST=GoogleService-Info.plist
GOOGLESERVICE_INFO_FILE=${PROJECT_DIR}/firebase/${flavor}/${GOOGLESERVICE_INFO_PLIST}

echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_FILE}"
if [ ! -f $GOOGLESERVICE_INFO_FILE ]
then
  echo "No GoogleService-Info.plist found. Please ensure it's in the proper directory."
  exit 1
fi

PLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app
echo "Will copy ${GOOGLESERVICE_INFO_PLIST} to final destination: ${PLIST_DESTINATION}"

cp "${GOOGLESERVICE_INFO_FILE}" "${PLIST_DESTINATION}"

기존 Runner에 자동으로 생성된 GoogleService-Info.plist 파일은 삭제해 주도록 하자.

Flutter

firebase_options.dart 파일을 분리해 주어야 하는데, 이때 현재 빌드된 앱의 PackageName이 필요하게 된다.

dependencies

package_info_plus는 여러 프로젝트의 정보를 가져와야 할 때에 유용한 라이브러리이다.

dependencies:
	package_info_plus: ^4.0.2

Environment 클래스의 runApp이 작동 전 Firebase를 초기화 해주도록 하자.

초기화시에 FirebasOptions는 패키지 네임을 사용해 일치하는 패키지 네임의 환경과 동일한 FirebaseOptions 클래스로 초기화 해주면 된다.


import 'firebase/firebase_options.dart' as prod;
import 'firebase/firebase_options_dev.dart' as dev;
import 'firebase/firebase_options_qa.dart' as qa;


class Enviroment {
  ...
  
   Future<FirebaseOptions> _initializeFirebaseOption() async {
    PackageInfo package = await PackageInfo.fromPlatform();
    const String name = "com.example.flutter_flavor_test";
    return switch (package.packageName) {
      name => prod.DefaultFirebaseOptions.currentPlatform,
      "$name.qa" => qa.DefaultFirebaseOptions.currentPlatform,
      "$name.dev" => dev.DefaultFirebaseOptions.currentPlatform,
      _ => throw UnimplementedError(),
    };
  }
  
  void run() {
  	WidgetsFlutterBinding.ensureInitialized();
    FirebaseOptions currentPlatform = await _initializeFirebaseOption();
    await Firebase.initializeApp(options: currentPlatform);
  	runApp(const MyApp());
  } 
  
  ...
}

IOS Reverse Id

구글 로그인을 사용하는 경우에 구글 로그인 Reverse id를 URL Types에 등록해 두어야 한다. 각각의 GoogleService-Info.plist 파일을 보면 REVERSED_CLIENT_ID 태그가 보일 것이다 해당 값을 추가해 주면 된다.

아래 경로로 들어와서 User-Defined를 추가해 주도록 하자.

GOOGLE_SCHEME이라 생성하면 각 환경에 맞는 값들을 등록할 수 있게된다. 여기서 등록을 해주자.

Info 탭으로 와서 하단부에 URL Types를 추가해주도록 하자.

아래 이미지와 같이 추가해주면 된다.


Firebase 다중 환경 구성을 테스트 해보도록 하자. 아래 코드를 실행해서 보자.

코드 내용은 구글 로그인을 진행하고 Authentication에 연결을 한 뒤, Firestore에 빌드 환경을 컬렉션으로 uid 값을 저장해주는 코드이다.

Android, IOS 둘다 테스트 해보고, 각 환경에서 잘 작동하는지도 테스트 해보시길 바란다.

FirebaseAuth.instance.signOut();
          GoogleSignIn _googleSignIn = GoogleSignIn();
          GoogleSignInAccount? _account = await _googleSignIn.signIn();
          if (_account != null) {
            GoogleSignInAuthentication _authentication =
                await _account.authentication;
            OAuthCredential _googleCredential = GoogleAuthProvider.credential(
              idToken: _authentication.idToken,
              accessToken: _authentication.accessToken,
            );
            UserCredential _credential = await FirebaseAuth.instance
                .signInWithCredential(_googleCredential);
            print(_credential.user?.displayName);
            print(_credential.user?.email);
            FirebaseFirestore _firestore = FirebaseFirestore.instance;
            await _firestore
                .collection("${Enviroment.enviroment.type}_flavor_test")
                .doc(_credential.user?.uid)
                .set(
              {
                "uid": _credential.user?.uid,
              },
            );
          }

정상적으로 작동이 되는 것을 확인했다. 만일 에러가 발생한 경우 해결이 어려우시면 댓글 남겨주세요.

Google 로그인 자체가 에러가 발생 했다면, SHA-1 키 등록을 Firebase에 추가했는지 확인해 보시고 그래도 안되신다면 아래 GCP 환경 구성 부분을 보시길 바란다.

그 외 Firebase 궁금하신 부분있거나, 제가 잘 못 공유한 내용이 있다면 댓글 남겨주세요. 확인해 보고 개선하도록 하겠습니다.

Multiple GCP(Google Cloud Platform) Enviroments

Google Cloud Platform

이어서 Google API와 관련된 다중 환경을 구성하도록 하겠다. 우리가 통상적으로 사용하는 GCP 구성에는 OAuth2를 사용하는 Google 로그인일 것이다.

GCP의 사용자 인증 구성 부분에도 환경별로 등록을 해주어야 한다. Firebase로 Google 로그인을 사용하는 경우에는 자동으로 생성이 되지만, 자동으로 등록이 되지 않은 경우에는 추가하면 된다.

GCP

GCP 대시보드로 이동해서 프로젝트를 선택해 주자.

프로젝트로 진입하면 아래 API 및 서비스 탭이 보이는데, 보이지 않은 경우에는 모든 제품 보기에서 보실 수 있습니다. 클릭해서 이동을 하도록 하자.

사용자 인증 정보 탭으로 이동해보면 OAuth 2.0 클라이언트 ID가 있다. 저는 Firebase로 구글 로그인을 연동 해서 Android, IOS 각각 3개의 환경의 클라이언트 ID가 생성이 되어있다.

여기에 3개의 환경이 구성되어 있지 않다면 구성되지 않은 환경을 추가해주면 된다.

새롭게 추가를 해야하는 경우에 사용자 인증 정보 만들기를 클릭하여 생성해주면 된다.

아래와 같이 패키지 이름에 suffix를 추가해서 생성하면 되고, 구글 로그인 사용하는 경우 SHA-1 인증서는 필수로 넣어주어야 정상 작동이 된다.

[Hash Key] SHA

IOS에서도 번들 ID 부분을 확인해서 추가해주면 된다.

구글 서비스의 모든 제품은 GCP를 통해서 사용할 수 있으니, 추가적으로 필요한 환경 분리나 설정은 GCP에서 하시면 된다.

여기 부분에서도 궁금하신 점은 댓글 남겨주세요.

다음은 Naver SDK를 사용하여 네이버 로그인 또는 네이버 지도를 사용해야 하는 경우에 대해서 살펴보도록 하겠다.

네이버는 애플리케이션을 생성하면 Client ID, Client Secret을 발급 받아 사용할 수 있다. 환경 분리가 되었어도 이 부분은 문제 없이 사용해도 되지만, 패키지 네임은 추가로 등록을 해야한다.

제가 네이버에서 Android PackageName 관련해서 이것저것 테스트를 해보다가 패키지 네임을 잘 못 등록했는데도 정상적으로 실행이 되버린 것이다. 계속 테스트하다가 PackageName이 어떻게 사용되는지 너무 궁금해서 결국 네이버 개발자 센터에 문의를 했다. 원래 PackageName은 "com.example.flutter_flavorApp_test" 인데, 모르고 "com.example"만 넣어져 있었다.
결국 Flavor로 패키지네임의 suffix가 추가되도 문제 없이 로그인을 사용할 수 있다는게 된다.

네이버 개발자 센터 문의

답변으로는 현재는 PackageName의 앞부분만 일치해도 검증이 통과되는데, 앞으로 검증 로직을 강화할 예정이니, Flavor 도입에 따른 PackageName을 추가해서 사용해야 한다고 한다.

우리도 추가를 해주도록 하자.

Android

Naver Developers에 방문하여 Application -> API 설정으로 이동하자.

로그인 오픈 API 환경 설정에 보면 안드로이드 부분이 있는데, 추가 버튼을 눌러 PackageName을 다중으로 등록할 수 있도록 지원해주고 있다.

IOS

아래로 이동해 보면, IOS 관련된 환경이 있다. IOS는 로그인 시도시 네이버 앱으로 이동했다가 다시 앱으로 리다이렉션이 되어야 한다. 이때 앱간 통신이 가능하게 하는것이 scheme을 통해서 구현되어 있다.

만약에 앱이 3개가 있는데, 스킴이 동일한 3개의 앱을 다 설치한 상태에서 네이버 로그인을 진행하면 어떤 앱으로 리다이렉션이 될지 모른다. 스킴이 동일하기 때문이어서 스킴을 각각 등록해서 사용해야 한다.

XCode를 열고 TARGETS Runner > Build Settings 탭으로 이동해서 User-Defiend을 추가해주자.

저는 이름을 "NAVER_SCHEME"이라고 등록하였다. 디벨로퍼에서 등록한 스킴을 각각의 환경에 맞게 넣어주도록 하자.

Info.plist에 등록한 네이버 스킴에 추가한 스킴으로 변경되서 적용될 수 있게 해주자.

네이버 로그인 사용시 "naverServiceAppUrlScheme"을 info에 등록해야 하는데, 여기 부분도 동일하게 스킴이 환경에 따라 변경될 수 있도록 해주자.

네이버는 다중 환경을 지원하고 있어서 어렵지 않게 환경 분리가 가능하다.

Kakao

이번에는 카카오 SDK 환경 분리를 해보자. 카카오 SDK가 가장 문제다....

카카오 로그인은 다중 환경을 지원하지 않고 있지만, 카카오 싱크를 사용한 로그인을 사용 중이라면 다중 환경이 가능하다.

실제 운영중인 서비스는 "제 3자 마케팅 동의"를 얻어 채널 구독을 자동으로 유도할 수 있는 싱크 로그인을 사용중이라 문제가 없지만, 그냥 카카오 로그인을 사용하고 있는 서비스가 문제가 된다.

왜 싱크 로그인만 다중 환경을 지원하고 있을까 ? 이유는 검수 때문에 그렇다고 한다.

네이버는 로그인 기능을 릴리즈로 사용하려면 검수를 받아야 하지만, 카카오는 검수를 하지 않고도 사용할 수 있다. 하지만 카카오 싱크는 검수를 통해서 사용이 가능하기에, 다중 환경을 지원하고 있다는 것이다.


Kakao Sync

카카오싱크 | Kakao Developers 시작하기

카카오 싱크 사용 중이라면, 카카오 데브톡 방문하여 관리자에게 앱 ID를 보내주면, 멀티 플랫폼을 등록할 수 있도록 승인을 받을 수 있다.

승인을 받으면, 자동으로 플랫폼 수정에서 멀티 패키지명 / 멀티 번들 ID 영역이 추가된 것을 확인할 수 있다. 여기에 환경을 추가한 식별자를 넣어주면 된다.

Multi Package Name

Multi Bundle ID


환경을 추가했으면, 아래에 Custom Scheme 이라는 부분이 생성되있는 것을 확인할 수 있다.

해당 커스텀 스킴을 Android, IOS 각각 추가해 주어야만 정상적으로 앱으로 리다이렉션 될 수 있다. 만일 추가안하게 되면, 어느 앱으로 리다이렉션 될지 모른다.

Android

커스텀 스킴을 안드로이드는 등록하지 않아도 정상적으로 작동이 되기는 한다. 하지만 웹 로그인을 진행하는 경우에는 작동이 되지않아 우선 추가해서 사용하려고 한다.

AndroidManifest.xml에 기존 카카오 로그인 관련 태그가 있는 곳에서 scheme 부분을 변경하도록 하자. 없는 경우에는 추가해주면 된다.

build.gradle에 분리한 환경에 맞는 커스텀 스킴을 넣어주도록 하자.

웹 로그인을 하는 경우에는 아래 태그를 AndroidManifest.xml에 추가해 주시면 된다.

IOS

TARGETS > Runner > Build Settings > Add User-Defined Setting 으로 위에서 FLUTTER_TARGET을 추가한 것과 같이 카카오 커스텀 스킴을 추가하도록 하자.

"KAKAO_SCHEME"이라고 만들어주고, 카카오 디벨로퍼에 있는 스킴을 아래와 같은 형태로 추가해주면 된다. 참고로 아무렇게나 적은 스킴이기 때문에, 각자의 스킴을 넣어야 한다.

Info 탭에서 URL Types를 추가해 주는데, 중요한 부분은 URL Schemes 부분에 "$(KAKAO_SCHEME)" 이라고 해주어야 각 환경별 스킴을 독립적으로 가지게 되어 정확하게 카카오 로그인을 요청한 앱으로 돌아올 수 있다.

Flutter

Flutter에서도 추가적인 작업을 해주어야 하는데, Kakao SDK 버전이 낮으면 지원되지 않아 버전을 올려야 적용이 된다.

Kakao SDK가 초기화될 때에 현재 앱 환경에 맞는 커스텀 스킴을 알지 못하면, 정상적으로 앱으로 리다이렉션을 못한다고 한다.

KakaoSdk.init(
	nativeAppKey: {your_native_key},
	customScheme: {your_custom_scheme},
);

환경에 스킴들을 등록해주자.

Kakao SDK 초기화시 customScheme을 전달해 주면 된다.

Future<void> run() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(options: _options);
    KakaoSdk.init(
      nativeAppKey: _type.kakaoKey,
      customScheme: _type.customScheme,
    );
    runApp(const MyApp());
}

Kakao Login

요새는 거의 대부분의 서비스가 싱크를 사용하고 있지만, 카카오 로그인을 계속 사용해야 될 때에 환경을 어떻게 분리할 수 있는지 살펴보도록 하자.

방법은 간단한데, application을 여러 개 생성하여 각각의 네이티브 키를 사용하는 방법으로 구현해야 한다.
하지만 카카오 공식 답변으로는 멀티 앱 사용을 위한 싱크 도입을 권장하고 있으니, 싱크 사용을 검토해 보시는 걸 추천한다.

EnviromentType

kakaoKey를 추가해주자.

enum EnviromentType {
  dev(
    type: "DEV",
    url: "https://tyger_dev.com",
    kakaoKey: "{dev_native_app_key}",
  ),
  qa(
    type: "QA",
    url: "https://tyger_dev.com",
    kakaoKey: "{qa_native_app_key}",
  ),
  prod(
    type: "PROD",
    url: "https://tyger.com",
    kakaoKey: "{native_app_key}",
  );

  final String type;
  final String url;
  final String kakaoKey;

  const EnviromentType({
    required this.type,
    required this.url,
    required this.kakaoKey,
  });
}

Kakao SDK 초기화시 네이티브 키를 전달해 주면 된다.

class Enviroment {	
	...
 Future<void> run() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(options: _options);
    KakaoSdk.init(nativeAppKey: _type.kakaoKey);

    runApp(const MyApp());
  }
  ...
}

Apple

Apple은 디벨로퍼에 접속해보면, 추가된 환경이 자동적으로 Identifier에 등록되있는 것을 확인할 수 있다.

Apple 로그인은 추가적인 작업은 필요 없으며, 푸시(APNS)는 환경에 새로운 인증키를 추가해서 발급 받아 사용하면 된다.

참고로 인증키 방식은 2개만 생성이 가능하기에, 더 많은 앱에 푸시를 작동시키길 원한다면 인증서 방식으로 변경해야 한다.

인증키, 인증서 관련된 부분은 내용이 많아 여기서 다루지는 않도록 하겠다.

App Icon

App Icon 만들기

앱 아이콘에도 Flavor 적용을 하도록 하자.

앱 아이콘 생성시 라이브러리를 사용하는 경우에는 다른 글을 참고하시고 여기서는 네이티브에서 직접 생성해 주도록 하겠다.

만일 아이콘을 동일하게 사용하고 싶다면 해당 작업은 진행하지 않아도 됩니다.

Android

앱 아이콘을 네이티브에서 직접 생성해주기 위해 Android Studio를 열어주자.

해당 프로젝트를 Android로 변경하고 File > Image Asset을 열어주자. 여기서 안드로이드 앱 아이콘을 생성할 수 있다.

앱 아이콘을 만드는 부분인데, 텍스트로 생성할 수도 있고, 이미지를 넣어줘도 된다. 저는 그냥 텍스트로 생성해 주었다.

해당 이미지 만드는 법에 대해서 자세히 알고 싶다면 아래 글을 참고하길 바란다.

App Icon 만들기

생성한 아이콘을 넣어줄건데, 우리가 이전에 google-services.json 파일을 넣어주기 위해 생성한 폴더를 선택해 주면 된다.

만일 해당 폴더가 없다면 dimension과 동일하게 폴더명을 생성해 주면된다.

다시 VS Code로 돌아와 보면 아래와 같이 이미지 파일이 생성된 것을 확인할 수 있다. 각 환경에 맞게 같은 방법으로 생성해 주시면 된다.

기존 main > res에 있던 이미지 폴더는 제거해준다.

IOS

이번에는 IOS에도 똑같이 적용하도록 하자.

IOS에서 사용할 이미지를 사이즈 별로 변환한 파일들이 필요하기에 아래 사이트에서 이미지를 생성해주도록 하겠다.

App Icon Generator

원하는 이미지를 업로드하고, iphone, ipad 부분을 체크하고 Generate 해주면 된다.

생성한 이미지의 압축을 풀고, 아래 경로를 따라 들어오면 AppIcon.appiconset 폴더가 있다. 해당 폴더명을 AppIcon-dev.appiconset 으로 변경해 주었다.

dev외에도 각 환경에 맞는 폴더명을 정해주시면 된다. 폴더명은 아무렇게나 만들어도 상관없다.

이제 X Code를 열어주도록 하자.

Runner > Assets 안에 보면 AppIcon이라는 기본적으로 생성이된 appiconset 폴더가 있다. 이 폴더가 IOS에서 사용하는 아이콘 이미지 셋이다.

해당 Assets안에 생성한 이미지의 appiconset 폴더를 넣어주자. AppIcon-dev.appiconset 이라고 이름을 변경해준 폴더를 드래그 & 드롭을 해주면 자동으로 넣어진다.

동일한 방법으로 dev, qa, prod 환경에 맞는 앱 아이콘을 넣어주고 기준에 사용하던 AppIcon 셋은 삭제해 주었다. 이렇게만 한다고 앱 아이콘이 적용이 되지 않는다. 이제 AppIcon에 경로를 지정해 주도록 하자.

TARGETS Runner > Build Settings > Basic & Combined 탭으로 이동을 해주자.

Filter 부분에 Primary라고 검색해 주면, 아래에 Primary App Icon Set Name 필드가 보일 것이다. 여기서 각 환경에 맞는 아이콘 셋의 폴더를 지정해 주면 된다.

AppIcon 부분을 더블클릭해서 환경에 맞게 변경해 주도록 하자.

환경에 맞게 AppIcon 셋을 지정해 주었다. 기존에 설치된 앱은 삭제하고 다시 빌드하셔야 제대로 적용할 수 있습니다.

Result

각 환경에 맞게 이미지가 정상적으로 적용이 된것을 볼 수 있다.

이번엔 Flavor 환경에 따른 딥링크를 적용해 보도록 하겠다. 딥링크는 앱 간에 통신에 사용되어, 앱을 오픈할 수 있도록 도와주고, 컨텐츠 공유를 하는데 필요한 기능이다.

URL Scheme도 아래의 방식과 동일하게 작업할 수 있기 때문에 크게 Android에서 사용하는 App Links와 IOS Universal Links를 환경 분리하는 방법에 대해서 살펴보도록 하겠다.

App Links & Universal Links를 애플과 구글의 서버를 통해 직접 생성하는 경우에도 아래와 같이 사용하시면 되고, Firebase Dynamic Links를 사용하는 경우에도 방법은 동일하다.

먼저 IOS Universal Links를 살펴보도록 하자. IOS는 Apple Developer에서 Associated Domains 를 추가하여 등록 후 사용하게 된다.

Identifier 탭으로 이동하면 각자의 추가된 환경에서 환경별 Domains를 추가/제거할 수 있다. 만일 운영 앱인 prod 앱에만 Universal Links를 작동되게 하려면 그냥 나머지 환경의 Associated Domains를 Off해주면 된다.

하지만 환경별 다른 Domains를 사용하게 하고 싶다면, 분리해주는 작업이 필요하다.

먼저 XCode를 열고 Signing & Capabilities 탭으로 이동해 보면, 각자의 등록된 Domains 영역이 보일 것이다.

해당 도메인을 사용자 정의에서 생성해서 넣어주면 빌드 환경에 따라 적용되게 된다.

Build Settings 탭으로 이동해서 "Add User-Defined Setting"를 클릭해 주자.

이름은 자유롭게 지워주면 되고, 각 환경에 맞는 도메인을 추가해주면 된다. 만약에 어떤 환경에서는 도메인 등록을 하지 않도록 하려면 빈 값으로 넣어주면 된다.

Associated Domains 탭으로 이동해서 정의한 이름을 넣어주면 된다. 이렇게 되면 앱간에 도메인이 충돌하지 않아 원하는 버전의 앱에서만 작동되도록 할 수 있다.

이번엔 안드로이드에서 사용하는 App Links를 환경 분리해보자.

build.gradle 환경 분리 태그안에 dynamic_link를 추가했다.

AndroidManifest.xml 파일에 등록한 스킴부분의 호스트를 변경해주면 된다.

마무리

Flavor 환경 분리에 대해서 모든 작업이 끝이 났다. Flutter를 사용하여 Android와 IOS의 환경 분리를 살펴봤고, VS Code, Android Studio Code, X Code에서도 분리된 환경으로 실행을 할 수 있도록 추가해 주었다.
Flutter에서 main 함수를 분리하고, Firebase, Social Login(Google, Naver, Kakao..) 등에서 환경을 분리하여 생성하고 등록해주는 방법도 살펴보았다.

보는 것보다 같이 따라해 보면서 직접 해보시는 것을 추천한다. 직접 구현하다 보면 더 좋은 방법이 있을 수 있고, 저와는 다른 상황이 나올 수도 있다.

Flavor 환경 분리에 대해서 궁금하시거나, 제가 잘 못 사용하고 있는 부분이 있다면 댓글 남겨주세요

추가로 Flavor 환경 분리가 되지 않는 경우에 대해서도 댓글 남겨주시면 제가 환경 분리해서 공유하도록 하겠습니다.

profile
Flutter Developer

21개의 댓글

comment-user-thumbnail
2023년 7월 28일

좋은 정보 감사합니다

1개의 답글
comment-user-thumbnail
2023년 8월 22일

와 좋은 정보 감사합니다

1개의 답글
comment-user-thumbnail
2023년 11월 6일

flutter web..bb

답글 달기
comment-user-thumbnail
2023년 12월 10일

안녕하세요. 궁금한게 있어서 질문 드립니다.
한 firebase 에 3개의 환경을 넣으신거 맞나요?
그렇게 되면 3개의 환경이 전부 "같은서버", "같은 디비" 를 바라본다고 생각하는데
저는 현재 3개의 환경 모두 "다른서버", "다른디비"를 바라보기 위해 세팅중인데 쉽지가 않네요..
firebase_app_id_file.json
firebase_options.dart
google-service.json
이 3개의 파일을 모두 나눠야 한다고 생각하는데, 문제는 나눈다고 알아서 찾아갈지도 모르겠고
처음이라 많이 헤매고 있습니다. 도움좀 주실 수 있나요?

2개의 답글
comment-user-thumbnail
2024년 1월 4일

안녕하세요.
글 올려주셔서 많은 도움 받았습니다.

빌드까지는 나뉘어서 잘 됐는데, 파이어베이스를 연동할때 잘 안되고 있어서 도움 요청 드립니다.
저희쪽 파이어베이스 구성을 하나로만 하지 않고 dev, qa, prod로 나누어 둔 상태입니다.

현재까지 진행 한 부분은
안드로이드 google-services.json까지 나눠서 android/dev, qa, prod로 나눠두었고,
sha-1 key를 추출해서 우선 파이어베이스-dev 프로젝트에 핑거프린트를 등록해서 dev의 google-service.json을 변경해둔 상태입니다.

sha-1 key의 문제인지 FirebaseAuth 와 관련되어
GoogleSignIn googleSignIn = GoogleSignIn();
GoogleSignInAccount? account = await googleSignIn.signIn();
을 호출하면 설정된 키로 로그인이 되지 않고 앱 초기화면에서 구글 로그인 화면이 뜹니다.

추가로 sha-1키는 어떻게 추출해도 dev, qa, prod 모두 같은 값이 나오는데 이 값을 그냥 3개의 파이어베이스 프로젝트에 등록해도 되는건지 모르겠네요.

이부분의 설정에서 애를 좀 먹고 있는데 도움 주시면 감사하겠습니다.

1개의 답글
comment-user-thumbnail
2024년 1월 11일

감사합니다. 정리가 잘되어 있어서 큰 도움이 되었습니다!!!!

답글 달기
comment-user-thumbnail
2024년 9월 10일

Flutter 의 flavor 기능을 사용해서 앱을 만들고 있는데
도움이 많이 되었습니다.
말씀하신 것을 토대로 FCM 부분을 추가하고 있습니다.

iOS의 경우 (안드로이드는 잘 작동함)
FCM notification 메시지를 보내면 정상적으로 Flutter 앱에서 잘 받아지는데
only data 메시지를 보내는 경우는 포그라운드, 백그라운드 모두 메시지가 받아지지 않아요
only data 메시지를 보낼때 당연히 전송 성공이라고 나옵니다.

(참고로 Flavor 를 사용하지 않으면 정상적으로 only data 메시지도 정상적으로 잘 받아 집니다.)

혹시 원인이 될만한 부분을 알 수 있을까요?

1개의 답글
comment-user-thumbnail
2024년 11월 21일

상세하게 써주셔서 감사합니다!! 정말 많은 도움이 되었네요 ㅎㅎ

답글 달기