Flutter에서 다양한 릴리스 플레이버를 어떻게 관리하나요?

Flutter에서 릴리스 플레이버(또는 빌드 환경)를 관리하는 것은 다양한 환경(개발, 스테이징, 프로덕션 등)에 맞게 앱을 구성하고 배포하는 중요한 부분입니다. 릴리스 플레이버는 동일한 코드베이스를 사용하면서도 서로 다른 설정, API 엔드포인트, 앱 아이콘, 앱 이름 등을 가질 수 있도록 해줍니다. 아래에서 Flutter에서 릴리스 플레이버를 관리하는 다양한 방법을 자세히 설명하겠습니다.

1. Flutter에서의 기본 설정: --dart-define 활용

Flutter에서는 --dart-define 플래그를 사용하여 빌드 시간에 환경 변수를 정의할 수 있습니다.

1.1 다트 코드에서 환경 변수 사용

// 환경 변수 정의
const String environment = String.fromEnvironment('ENVIRONMENT', defaultValue: 'development');
const String apiBaseUrl = String.fromEnvironment('API_URL', defaultValue: 'https://dev-api.example.com');

// 사용 예
void main() {
  print('현재 환경: $environment');
  print('API URL: $apiBaseUrl');

  runApp(MyApp());
}

1.2 명령어 라인에서 값 전달

# 개발 환경 빌드
flutter run --dart-define=ENVIRONMENT=development --dart-define=API_URL=https://dev-api.example.com

# 스테이징 환경 빌드
flutter run --dart-define=ENVIRONMENT=staging --dart-define=API_URL=https://staging-api.example.com

# 프로덕션 환경 빌드
flutter run --dart-define=ENVIRONMENT=production --dart-define=API_URL=https://api.example.com

1.3 IDE에서 설정

VS Code의 launch.json 파일:

{
  "configurations": [
    {
      "name": "Flutter (Development)",
      "type": "dart",
      "request": "launch",
      "args": [
        "--dart-define=ENVIRONMENT=development",
        "--dart-define=API_URL=https://dev-api.example.com"
      ]
    },
    {
      "name": "Flutter (Production)",
      "type": "dart",
      "request": "launch",
      "args": [
        "--dart-define=ENVIRONMENT=production",
        "--dart-define=API_URL=https://api.example.com"
      ]
    }
  ]
}

2. 네이티브 플랫폼별 플레이버 설정

2.1 Android 플레이버 설정

Android에서는 build.gradle 파일을 수정하여 제품 플레이버를 정의할 수 있습니다.

android/app/build.gradle 파일:

android {
    // ... 기존 설정

    flavorDimensions "environment"

    productFlavors {
        dev {
            dimension "environment"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
            resValue "string", "app_name", "My App Dev"
        }

        staging {
            dimension "environment"
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
            resValue "string", "app_name", "My App Staging"
        }

        prod {
            dimension "environment"
            // 프로덕션은 기본 앱 ID 사용
            resValue "string", "app_name", "My App"
        }
    }

    // --dart-define 값을 BuildConfig 필드로 노출
    applicationVariants.all { variant ->
        variant.buildConfigField "String", "API_URL", "\"${getDefineValue("API_URL") ?: "https://default-api.example.com"}\""
    }
}

// --dart-define 값을 Gradle에서 가져오는 헬퍼 메서드
def getDefineValue(String name) {
    // Gradle 속성, 시스템 속성 또는 로컬 속성 파일에서 값 가져오기
    return project.findProperty(name) ?: System.getenv(name)
}

이렇게 설정하면 다음과 같이 다양한 플레이버로 앱을 빌드할 수 있습니다:

# 개발 환경 빌드
flutter run --flavor dev --dart-define=API_URL=https://dev-api.example.com

# 스테이징 환경 빌드
flutter run --flavor staging --dart-define=API_URL=https://staging-api.example.com

# 프로덕션 환경 빌드
flutter run --flavor prod --dart-define=API_URL=https://api.example.com

2.2 iOS 플레이버 설정

iOS에서는 Xcode 구성 및 스키마를 사용하여 다양한 환경을 설정할 수 있습니다.

  1. Xcode에서 Configuration 설정하기:

    • Runner.xcodeproj 파일을 열기
    • "Runner" 프로젝트 선택 > "Info" 탭 > "Configurations" 섹션
    • "+" 버튼으로 새 구성 추가 (예: "Debug-Development", "Release-Development" 등)
  2. xcconfig 파일 생성하기:

    • ios/Flutter/Development.xcconfig, ios/Flutter/Staging.xcconfig, ios/Flutter/Production.xcconfig 파일 생성
    • 각 파일에 환경별 설정 추가:
// Development.xcconfig
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp.dev
DISPLAY_NAME = My App Dev
FLUTTER_TARGET = lib/main_development.dart
// Staging.xcconfig
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp.staging
DISPLAY_NAME = My App Staging
FLUTTER_TARGET = lib/main_staging.dart
// Production.xcconfig
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp
DISPLAY_NAME = My App
FLUTTER_TARGET = lib/main_production.dart
  1. Info.plist 수정하기:
<key>CFBundleDisplayName</key>
<string>$(DISPLAY_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
  1. Dart에서 플랫폼별 네이티브 값 접근하기:
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

// MethodChannel을 통해 네이티브 값 가져오기
Future<String> getApiUrl() async {
  if (defaultTargetPlatform == TargetPlatform.android) {
    final String apiUrl = await const MethodChannel('channel_name')
        .invokeMethod<String>('getApiUrl') ?? 'https://default-api.example.com';
    return apiUrl;
  } else if (defaultTargetPlatform == TargetPlatform.iOS) {
    // iOS Info.plist에서 값 가져오기
    // 또는 --dart-define 값 사용
    return const String.fromEnvironment('API_URL',
        defaultValue: 'https://default-api.example.com');
  }
  return 'https://default-api.example.com';
}

3. 환경별 진입점 사용

각 환경별로 별도의 진입점(main 함수) 파일을 만들어서 관리할 수 있습니다.

3.1 환경별 main 파일 생성

lib/
  |- main_development.dart
  |- main_staging.dart
  |- main_production.dart
  |- main.dart (기본 진입점, 다른 파일로 리디렉션)
  |- app/
     |- my_app.dart (실제 앱 코드)
  |- config/
     |- environment_config.dart (환경 구성)

각 진입점 파일의 예시:

// main_development.dart
import 'package:flutter/material.dart';
import 'package:myapp/app/my_app.dart';
import 'package:myapp/config/environment_config.dart';

void main() {
  final config = EnvironmentConfig(
    environment: Environment.development,
    apiBaseUrl: 'https://dev-api.example.com',
    enableLogging: true,
  );

  runApp(MyApp(config: config));
}
// main_production.dart
import 'package:flutter/material.dart';
import 'package:myapp/app/my_app.dart';
import 'package:myapp/config/environment_config.dart';

void main() {
  final config = EnvironmentConfig(
    environment: Environment.production,
    apiBaseUrl: 'https://api.example.com',
    enableLogging: false,
  );

  runApp(MyApp(config: config));
}

환경 구성 클래스:

// environment_config.dart
enum Environment { development, staging, production }

class EnvironmentConfig {
  final Environment environment;
  final String apiBaseUrl;
  final bool enableLogging;

  const EnvironmentConfig({
    required this.environment,
    required this.apiBaseUrl,
    required this.enableLogging,
  });

  bool get isDevelopment => environment == Environment.development;
  bool get isStaging => environment == Environment.staging;
  bool get isProduction => environment == Environment.production;
}

앱 실행 방법:

# 개발 환경
flutter run -t lib/main_development.dart --flavor dev

# 스테이징 환경
flutter run -t lib/main_staging.dart --flavor staging

# 프로덕션 환경
flutter run -t lib/main_production.dart --flavor prod

4. 플레이버 관리를 위한 기타 접근법

4.1 환경 구성 파일 사용

각 환경에 대한 JSON 또는 YAML 구성 파일을 만들고 빌드 시 적절한 파일을 선택합니다.

assets/
  |- config/
     |- config_development.json
     |- config_staging.json
     |- config_production.json

config_development.json:

{
  "apiBaseUrl": "https://dev-api.example.com",
  "enableLogging": true,
  "timeout": 30000
}

이 파일들을 불러와서 사용하는 코드:

import 'dart:convert';
import 'package:flutter/services.dart';

class AppConfig {
  final String apiBaseUrl;
  final bool enableLogging;
  final int timeout;

  AppConfig({
    required this.apiBaseUrl,
    required this.enableLogging,
    required this.timeout,
  });

  static Future<AppConfig> load(String environment) async {
    final configString = await rootBundle.loadString(
      'assets/config/config_$environment.json',
    );
    final Map<String, dynamic> config = jsonDecode(configString);

    return AppConfig(
      apiBaseUrl: config['apiBaseUrl'],
      enableLogging: config['enableLogging'],
      timeout: config['timeout'],
    );
  }
}

// 사용 예
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final environment = const String.fromEnvironment('ENVIRONMENT',
      defaultValue: 'development');

  final config = await AppConfig.load(environment);
  runApp(MyApp(config: config));
}

4.2 코드 생성 사용

build_runnersource_gen 패키지를 사용하여 설정 코드를 생성할 수 있습니다.

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

dev_dependencies:
  build_runner: ^2.4.6
  source_gen: ^1.4.0
  # 커스텀 코드 제너레이터

5. 스크립트 및 자동화 도구

5.1 Flutter 플레이버를 위한 도우미 스크립트

tool/build.dart 파일 생성:

import 'dart:io';

// 다양한 플레이버와 대상 플랫폼으로 빌드하는 스크립트
void main(List<String> args) async {
  // 기본값
  String flavor = 'dev';
  String platform = 'android';

  // 인수 파싱
  for (var i = 0; i < args.length; i++) {
    if (args[i] == '--flavor' && i + 1 < args.length) {
      flavor = args[i + 1];
    } else if (args[i] == '--platform' && i + 1 < args.length) {
      platform = args[i + 1];
    }
  }

  // 환경 변수 설정
  final environmentVars = {
    'dev': [
      'ENVIRONMENT=development',
      'API_URL=https://dev-api.example.com',
    ],
    'staging': [
      'ENVIRONMENT=staging',
      'API_URL=https://staging-api.example.com',
    ],
    'prod': [
      'ENVIRONMENT=production',
      'API_URL=https://api.example.com',
    ],
  };

  // 환경 변수 가져오기
  final envVars = environmentVars[flavor] ?? environmentVars['dev']!;

  // 빌드 명령 생성
  final dartDefines = envVars.map((e) => '--dart-define=$e').join(' ');

  String buildCommand;
  if (platform == 'android') {
    buildCommand = 'flutter build apk --flavor $flavor $dartDefines';
  } else if (platform == 'ios') {
    buildCommand = 'flutter build ios --flavor $flavor $dartDefines --no-codesign';
  } else {
    print('지원되지 않는 플랫폼: $platform');
    exit(1);
  }

  print('실행 명령어: $buildCommand');

  // 명령 실행
  final result = await Process.run('sh', ['-c', buildCommand]);
  stdout.write(result.stdout);
  stderr.write(result.stderr);

  exit(result.exitCode);
}

스크립트 사용:

# 개발 환경 Android 빌드
dart tool/build.dart --flavor dev --platform android

# 프로덕션 환경 iOS 빌드
dart tool/build.dart --flavor prod --platform ios

5.2 빌드 자동화 도구 통합

CI/CD 파이프라인에 플레이버 설정을 통합할 수 있습니다. 예를 들어, GitHub Actions:

# .github/workflows/build.yml
name: Build and Release

on:
  push:
    branches: [main, develop]

jobs:
  build-android:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        flavor: [dev, staging, prod]

    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.10.0"
          channel: "stable"

      - name: Install dependencies
        run: flutter pub get

      - name: Build APK
        run: |
          if [ "${{ matrix.flavor }}" == "dev" ]; then
            DART_DEFINES="--dart-define=ENVIRONMENT=development --dart-define=API_URL=https://dev-api.example.com"
          elif [ "${{ matrix.flavor }}" == "staging" ]; then
            DART_DEFINES="--dart-define=ENVIRONMENT=staging --dart-define=API_URL=https://staging-api.example.com"
          else
            DART_DEFINES="--dart-define=ENVIRONMENT=production --dart-define=API_URL=https://api.example.com"
          fi

          flutter build apk --flavor ${{ matrix.flavor }} $DART_DEFINES

요약: 플레이버 관리 모범 사례

  1. 환경별 설정 분리: 코드와 구성 분리를 유지하여 환경 간 전환을 쉽게 합니다.
  2. 일관된 네이밍 규칙: 모든 환경에서 일관된 이름 지정 규칙을 사용합니다.
  3. 단일 진실 소스(SSOT): 환경 설정에 대한 단일 소스를 유지합니다.
  4. 자동화: 다양한 환경에 대한 빌드 및 배포 프로세스를 자동화합니다.
  5. 환경별 아이콘 및 스플래시 화면: 각 환경을 시각적으로 구별할 수 있도록 다른 앱 아이콘과 색상을 사용합니다.
  6. 플레이버별 앱 ID 사용: 같은 장치에 여러 버전을 설치할 수 있도록 다른 앱 ID를 사용합니다.
  7. 환경별 분석 및 모니터링: 각 환경에 대해 별도의 분석 및 오류 보고 프로젝트를 설정합니다.

Flutter에서 릴리스 플레이버를 효과적으로 관리하면 개발 주기가 더 원활해지고 각 환경에 맞게 앱을 쉽게 구성하고 배포할 수 있습니다.

results matching ""

    No results matching ""