Flavor와 환경 분리 — dev, staging, prod 빌드 관리

실무에서는 개발, 스테이징, 프로덕션 환경을 분리해야 합니다. 각 환경마다 다른 API 서버, 앱 이름, 아이콘을 사용하는 것이 일반적입니다.


왜 환경 분리가 필요한가?

환경API 서버앱 이름용도
devlocalhost:3000[DEV] MyApp개발
stagingstaging-api.example.com[STG] MyAppQA 테스트
prodapi.example.comMyApp실제 배포

방법 1: Dart define (간단한 방법)

BASH
# 빌드 시 환경 변수 전달
flutter run --dart-define=ENV=dev --dart-define=API_URL=http://localhost:3000
flutter run --dart-define=ENV=prod --dart-define=API_URL=https://api.example.com

# .env 파일로 관리
flutter run --dart-define-from-file=.env.dev
DART
class AppConfig {
  static const String env = String.fromEnvironment('ENV', defaultValue: 'dev');
  static const String apiUrl = String.fromEnvironment(
    'API_URL',
    defaultValue: 'http://localhost:3000',
  );

  static bool get isDev => env == 'dev';
  static bool get isProd => env == 'prod';
}

// 사용
final baseUrl = AppConfig.apiUrl;
if (AppConfig.isDev) {
  // 개발 전용 로직
}

방법 2: Flutter Flavor (완전한 방법)

Flavor는 네이티브 빌드 시스템을 활용하여 앱 이름, 아이콘, 번들 ID까지 환경별로 분리합니다.

flutter_flavorizr로 자동 설정

YAML
# pubspec.yaml
dev_dependencies:
  flutter_flavorizr: ^2.2.0

flavorizr:
  flavors:
    dev:
      app:
        name: "[DEV] MyApp"
      android:
        applicationId: "com.example.app.dev"
      ios:
        bundleId: "com.example.app.dev"
    staging:
      app:
        name: "[STG] MyApp"
      android:
        applicationId: "com.example.app.staging"
      ios:
        bundleId: "com.example.app.staging"
    prod:
      app:
        name: "MyApp"
      android:
        applicationId: "com.example.app"
      ios:
        bundleId: "com.example.app"
BASH
# 자동 설정 실행
dart run flutter_flavorizr

빌드 및 실행

BASH
# 개발 환경으로 실행
flutter run --flavor dev -t lib/main_dev.dart

# 스테이징 환경으로 빌드
flutter build apk --flavor staging -t lib/main_staging.dart

# 프로덕션 빌드
flutter build appbundle --flavor prod -t lib/main_prod.dart

환경별 진입점

DART
// lib/config/app_config.dart
enum Environment { dev, staging, prod }

class AppConfig {
  final Environment environment;
  final String apiBaseUrl;
  final String appTitle;
  final bool enableLogging;

  const AppConfig({
    required this.environment,
    required this.apiBaseUrl,
    required this.appTitle,
    this.enableLogging = false,
  });

  bool get isDev => environment == Environment.dev;
  bool get isProd => environment == Environment.prod;
}

// lib/main_dev.dart
void main() {
  const config = AppConfig(
    environment: Environment.dev,
    apiBaseUrl: 'http://localhost:3000',
    appTitle: '[DEV] MyApp',
    enableLogging: true,
  );
  runApp(MyApp(config: config));
}

// lib/main_staging.dart
void main() {
  const config = AppConfig(
    environment: Environment.staging,
    apiBaseUrl: 'https://staging-api.example.com',
    appTitle: '[STG] MyApp',
    enableLogging: true,
  );
  runApp(MyApp(config: config));
}

// lib/main_prod.dart
void main() {
  const config = AppConfig(
    environment: Environment.prod,
    apiBaseUrl: 'https://api.example.com',
    appTitle: 'MyApp',
    enableLogging: false,
  );
  runApp(MyApp(config: config));
}

앱에서 config 접근

DART
class MyApp extends StatelessWidget {
  final AppConfig config;
  const MyApp({super.key, required this.config});

  @override
  Widget build(BuildContext context) {
    return Provider.value(
      value: config,
      child: MaterialApp(
        title: config.appTitle,
        home: const HomeScreen(),
      ),
    );
  }
}

// 하위 위젯에서 접근
final config = context.read<AppConfig>();
print(config.apiBaseUrl);

환경별 Firebase 설정

DART
// 환경별 다른 Firebase 프로젝트 연결
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Flavor에 따라 다른 Firebase 옵션
  await Firebase.initializeApp(
    options: _getFirebaseOptions(),
  );

  runApp(const MyApp());
}

FirebaseOptions _getFirebaseOptions() {
  const flavor = String.fromEnvironment('FLAVOR');
  switch (flavor) {
    case 'dev':
      return DevFirebaseOptions.currentPlatform;
    case 'staging':
      return StagingFirebaseOptions.currentPlatform;
    default:
      return ProdFirebaseOptions.currentPlatform;
  }
}

환경별 앱 아이콘

YAML
# pubspec.yaml
dev_dependencies:
  flutter_launcher_icons: ^0.14.0

# flutter_launcher_icons-dev.yaml
flutter_launcher_icons:
  android: true
  ios: true
  image_path: "assets/icons/icon_dev.png"

# flutter_launcher_icons-prod.yaml
flutter_launcher_icons:
  android: true
  ios: true
  image_path: "assets/icons/icon_prod.png"
BASH
dart run flutter_launcher_icons -f flutter_launcher_icons-dev.yaml
dart run flutter_launcher_icons -f flutter_launcher_icons-prod.yaml

VS Code 런 설정

JSON
// .vscode/launch.json
{
  "configurations": [
    {
      "name": "Dev",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_dev.dart",
      "args": ["--flavor", "dev"]
    },
    {
      "name": "Staging",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_staging.dart",
      "args": ["--flavor", "staging"]
    },
    {
      "name": "Production",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_prod.dart",
      "args": ["--flavor", "prod"]
    }
  ]
}

정리

  • dart-define: 간단한 환경 변수 전달에 적합합니다
  • Flavor: 앱 이름, 번들 ID, 아이콘까지 완전히 분리할 때 사용합니다
  • flutter_flavorizr로 네이티브 설정을 자동 생성할 수 있습니다
  • 환경별 진입점(main_dev.dart, main_prod.dart)으로 config를 분리합니다
  • 같은 기기에 dev와 prod 앱을 동시에 설치하려면 번들 ID를 다르게 해야 합니다
  • Firebase도 환경별 다른 프로젝트를 연결하는 것이 좋습니다
댓글 로딩 중...