플러터에서 앱 이름과 번들 ID 쉽게 바꿔서 개발용 배포용 앱 나누기 (with rename)

thumbnail

스토어에 등록 후…

안드로이드 마켓에 정상 등록을 한 이후 스토어용과 개발용 (웹에서는 개발과 운영섭 나누는 느낌)으로 나눠서 해야 할 필요가 생겼다.
당연한 이야기겠지만 공기계가 있다면 이런 짓을 안해도 된다.

하지만 기기 해상도 체크 및 현실적인 부분으로 인해 보통은 개발/운영을 나눠서 작업한다.
(공폰을 4개 이상 들고 있는 사람이 있을까?)

일단 안드로이드 기준으로는 몇 가지 내용을 바꿔야 한다.

  1. AndroidManifest.xml
  2. bundle.gradle
  3. MainActivity & Path
  4. 기타 서비스 패키지 (Firebase, etc..)

이런 항목을 일일이 하나씩 다 바꾸는 방법은 무식했다.
그리고 스토어 배포하고 개발용 돌리다가 실수를 할 수도 있다.
그래서 몇 가지 좀 찾아보던 중 괜찮은 패치키를 찾게 되었다.

changeapppackage_name (함정카드였던것)

img01

changeapppackage_name는 Likes도 많고, 괜찮은 것 같아서 한번 받아서 설치하였다.
괜찮은 라이브러리인 줄 알았으나…

몇 가지 찾아보니 이게 단순하게 이름만 바꿔주는 형태였다.
그래서 내가 원하는 형태로 쓰기엔 문제가 있었다.

그리고 깃헙 이슈를 보는데 아래와 같은 이슈가 있었다.

img02

그렇다.
Only Android 용이었다.
동작도 안하는데 Android Only라니… 그리고 개발자 댓글이 2020년인데 아직도 반영이 안된 상태다.

img03

22년에는 위와 같이 댓글을 달았는데 아직도 업데이트가 안된 것 같았다.
그런데 재미있는게 아까 이슈에 누가 댓글을 달았다.

img04

그래서 난 댓글에 있는 라이브러리를 확인해봤다.

Rename

img05

rename은 아까랑 비슷한 역할을 하는데 iOS도 지원한다.
그리고 기능도 아까 찾은 라이브러리보다 나은듯 해서 이 라이브러리를 사용해보기로 했다.

설치법

먼저 cli 형태로 사용하기 위해서는 아래 명령어를 이용한다.

dart pub global activate rename

flutter 내부에서 사용할 수도 있지만 나는 배포 전, 그리고 개발환경 스왑용으로만 해서 cli 형태로만 설치했다.
그리고 도움말을 참고하면 다음과 같은 기능이 있다.
주로 사용할 것은 set 기능이다.

getAppName : Get app names for the targeted platforms

getBundleId : Get bundleId identifiers for the targeted platforms

setAppName : Set app name for the targeted platforms

setBundleId : Set bundleId identifier for the targeted platforms

여기서 AppName은 설치 될 때 표시될 이름을 의미한다.

img06

그리고 BundelId의 경우 스토어나 기기에서 사용되는 일종의 고유 값이다.

img07

사용법은 매우 간단하다.

사용법

터미널에서 적용할 프로젝트로 이동해서 아래의 명령어를 수행한다.

// 앱 이름 바꾸기
>rename setAppName --targets ios,android --value [적용할 이름]

// 앱 번들 Id 바꾸기
>rename setBundleId --targets ios,android --value [적용할 번들id]

이렇게 하면 일괄로 변경되는 것을 알 수 있다.
근데 이렇게 해도 사실 개발용/운영용으로 나눌 수 없었다.

왜냐하면 이게 전부 바뀌는게 아니었기 때문이다.

진짜로 나누려면…

사실 rename으로 해도 나눠지지 않는다.
몇 가지 파일을 바꿔주지 않기 때문이다.

일단 안드로이드 기준이다.
사실 iOS는 배포 준비중이라서 다음 주 쯔음 다시 포스팅 할 예정이다.

나는 현재 구글 파이어베이스를 이용 중이다.
이걸 쓰려면 아래의 경로 파일이 추가되어 하고…

//android/app/google-services.json

"client_info": {
                "mobilesdk_app_id": "-",
                "android_client_info": {
                    "package_name": "-"
                }
            },

이렇게 package_name이 필요하다.
그래서 이 부분도 같이 변경을 해야 하는데 이 부분은 rename이 처리해주지 않는다.

게다가 위에서 언급한 바와 같이 build.gradle 파일이나 MainActivity.kt등 내용이 바뀌지 않는다.
그래서 난 dart로 코드를 하나 짜서 실행하면 모드에 맞게 각 구성이 바뀌게 처리해줬다.

일단 코드를 먼저 보자.

import 'dart:convert';
import 'dart:io';

const devAppId = "개발용 Bundle ID";
const prodAppId = "운영용 Bundle ID";

const devAppName = "개발용 앱이름";
const prodAppName = "운영용 앱이름";

void main(List<String> args) {
  bool isProdMode = false;

  if (args.isNotEmpty) {
    ///전달인자로 온 값을 확인해본다.
    for (var arg in args) {
      //제대로 들어온 경우
      if (arg.startsWith('-run=')) {
        var parts = arg.split('=');
        if (parts.length == 2) {
          var runMode = parts[1];
          isProdMode = runMode == "prod";
        }
      }
    }

    runProcess(isProdMode);
  } else {
    print("전달인자 -run가 빠졌습니다. (dev/prod)");
  }
}

///작업 처리
void runProcess(bool isProdMode) async {
  //Activity
  await updateMainActivity(isProdMode);

  // build.gradle
  updateGradleFile(isProdMode);

  //update android manifest
  updatePackageInManifest(isProdMode);

  // JSON 파일 경로
  const filePath = '안드로이드용 google-services.json 파일 경로';

  // JSON 파일 읽기
  final file = File(filePath);
  final jsonString = file.readAsStringSync();

  // JSON 파싱
  final json = jsonDecode(jsonString);

  // 원하는 변경 수행
  json['client'][0]['client_info']['android_client_info']['package_name'] =
      isProdMode ? prodAppId : devAppId;

  // 변경된 JSON 데이터 다시 문자열로 직렬화
  final modifiedJsonString = jsonEncode(json);

  // 수정된 데이터를 파일에 다시 쓰기 (기존 파일 덮어쓰기)
  file.writeAsStringSync(modifiedJsonString);

  //명령어 수행
  // 3. 외부 명령 실행
  await Process.run('rename', [
    'setAppName',
    '--targets',
    'ios,android',
    '--value',
    isProdMode ? prodAppName : devAppName
  ]);
  await Process.run('rename', [
    'setBundleId',
    '--targets',
    'ios,android',
    '--value',
    isProdMode ? prodAppId : devAppId
  ]);

  print("\nRun end = mode = ${isProdMode ? "운영-Release" : "개발-Develop"}");
}

///MainActivity.kt 처리
Future<void> updateMainActivity(bool isProdMode) async {
  //디렉토리 수정
  const activityPath = 'main activity 경로';

  final oldDir = Directory(
      "$activityPath/${isProdMode ? 'dev' : 'prod'}");
  final newDir = Directory(
      "$activityPath/${isProdMode ? 'prod' : 'dev'}");

  if (oldDir.existsSync()) {
    oldDir.renameSync(newDir.path);
    print('디렉토리 이름을 변경했습니다: $oldDir -> $newDir');
  } else {
    print('디렉토리가 존재하지 않습니다: $oldDir');
  }

  //Activity 파일 수정
  try {
    final activityFile = File("${newDir.path}/MainActivity.kt");
    final lines = await activityFile.readAsLines();

    // 변경할 패키지 문자열을 찾아서 교체합니다.
    for (var i = 0; i < lines.length; i++) {
      if (lines[i].contains(
          'package ${isProdMode ? "개발용 패키지" : "운영용 패키지"}')) {
        lines[i] =
            'package ${isProdMode ? "운영용 패키지" : "개발용 패키지"}';
        break;
      }
    }

    // 변경된 내용을 파일에 다시 씁니다.
    await activityFile.writeAsString(lines.join('\n'));
    print(
        'MainActivity Package를 변경하였습니다. = ${isProdMode ? "운영용 패키지" : "개발용 패키지"}');
  } catch (e) {
    print('파일 업데이트 중 오류 발생: $e');
  }
}

///Gradle 파일 수정 처리
void updateGradleFile(bool isProdMode) {
  const buildGradlePath = 'build.gradle 경로';
  final file = File(buildGradlePath);

  // 1. 파일을 읽어옵니다.
  final lines = file.readAsLinesSync();

  // 2. 'namespace'를 찾아서 변경합니다.
  final updatedLines = <String>[];
  bool updated = false;

  for (final line in lines) {
    if (line.trimLeft().startsWith('namespace')) {
      // 찾은 줄이 'namespace'로 시작하는 경우 값을 변경합니다.
      updatedLines.add('namespace "${isProdMode ? prodAppId : devAppId}"');
      updated = true;
    } else {
      updatedLines.add(line);
    }
  }

  // 3. 변경된 내용을 파일에 다시 씁니다.
  if (updated) {
    file.writeAsStringSync(updatedLines.join('\n'));
    print('build.gradle 파일이 업데이트되었습니다.');
  } else {
    print('namespace 키워드를 찾지 못했습니다.');
  }
}

///Android Manifest 수정 처리
void updatePackageInManifest(bool isProdMode) {
  File file = File('AndroidManifest.xml 경로');

  // Read the file contents
  String manifestContent = file.readAsStringSync();

  // Replace the package name with the new one
  manifestContent = manifestContent.replaceAllMapped(
    RegExp(r'package="([^"]*)"'),
    (match) => 'package="${isProdMode ? prodAppId : devAppId}"',
  );

  // Write the modified contents back to the file
  file.writeAsStringSync(manifestContent);
  print('AndroidManifest 파일이 업데이트되었습니다.');
}

실행법은 그냥 프로젝트 최상단에 코드를 배치하고 아래처럼 수행한다.

> dart change_mode.dart -run=dev
> dart change_mode.dart -run=prod

수행하고 나면 각 환경에 맞게끔 변경 처리가 된다.

코드에 대한 전반적인 설명은 생략한다.
하지만 몇 가지 내용을 훝고 가자면…

updateMainActivity 함수에서는 아래의 코드가 약간 의아할 수 있다.

final oldDir = Directory(
      "$activityPath/${isProdMode ? 'dev' : 'prod'}");
  final newDir = Directory(
      "$activityPath/${isProdMode ? 'prod' : 'dev'}");

기존의 oldDir의 경우 반대의 상황이 된다.
즉 운영모드로 바꿀 때는 기존의 경우 개발 모드이고, 개발모드로 바꿀 때는 기존이 운영으로 되어 있을 것이다.

그리고 MainActivity의 경우 디렉토리랑 파일 내에 있는 package를 같이 변경해줘야 한다.
프로젝트마다 다르겠지만 코드가 아래처럼 되어 있을 것이다.

package -

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
    ///....
}

저 위 package도 같이 바꿔줘야 한다.
안그러면 앱이 구동이 안되는 문제가 생긴다.

사실 코드를 쭉 훝어보면 어려운 부분은 없을 것이다.
급조한 코드라서 부족한 부분과 엉성한 부분이 있다.
근데 그냥 임시로 쓰는거라 노력을 더 기울이진 않았다.

조만간 iOS 하면 iOS에 맞게끔 내용을 추가해봐야겠다.
이 부분은 포스팅을 업데이트 하거나 추가 포스팅을 해보겠다.

참고


Written by@MHLab
로또는 흑우집합소 🎲
와인관리, 시음노트, 셀러관리는 마와셀 🥂

🫥 My Service|  📜 Contact|  💻 GitHub