Play Feature Delivery 사용
Qt에서 Google Play 기능 제공을 활용하는 사례를 소개합니다.

이 문서는 Feature Delivery 예제의 기능을 설명합니다. 이 예제는 Qt 6.11부터 지원되는 기능을 사용합니다.
Play Feature Delivery는 이전 버전의 Qt에서도 사용할 수 있지만, 애플리케이션을 생성하려면 Android 프로젝트를 수동으로 추가하고 Qt 바이너리를 복사해야 합니다. 이에 대한 지침은 ‘Qt 6.11 이전 버전에서 Feature Delivery 사용하기’ 장에서 확인할 수 있습니다.
피처 딜리버리란 무엇인가요?
Play Feature Delivery는 Google이 제공하는 기능으로, 개발자가 프로젝트를 구성하면 Google Play 스토어가 앱 콘텐츠를 여러 개의 다운로드 가능한 패키지로 분할할 수 있게 해줍니다. 또한 개발자는 콘텐츠가 사용자에게 전달되는 방식을 제어할 수 있습니다. 이렇게 분할된 소프트웨어 및 콘텐츠 패키지는 Android App Bundles(AAB)를 사용하여 Google Play 스토어로 전달됩니다. Google의 개발자 문서에서 이 기능에 대해 자세히 설명하고 있습니다.
예제 프로젝트: Feature Delivery Map Loader
이 간단한 앱은 Play Feature Delivery를 사용하여 사용자가 요청할 때 이미지를 제공합니다. 이 앱은 쉽게 수정하여 200MB가 넘는 앱을 만들고, 앱 크기 제한을 테스트하며, 대용량 Feature Delivery 모듈을 사용하여 다운로드할 수 있습니다.
앱
이 애플리케이션은 드래그 가능한 뷰와 네 개의 버튼으로 구성되어 있습니다.
- 시작 시 ‘ Load Map ’ 및 ‘ Show Map Info ’ 버튼이 활성화됩니다.
- Show Map Info 버튼을 클릭하면 정보가 없다는 메시지가 표시됩니다.
- Load Map 기능 모듈 로딩을 시작합니다:
- 다운로드 팝업이 표시되며, 이 팝업에서 다운로드를 취소할 수 있습니다.
- 다운로드가 완료되면 팝업이 사라지고
- Change Map Remove Map 버튼이 활성화됩니다.
- Remove Map 버튼을 클릭하면 피처 모듈의 제거가 요청되며, 활성화되어 있던 버튼들이 비활성화됩니다.
- ' Change Map '을 클릭하면 표시된 지도를 변경할 수 있는 화면이 열립니다. 이 예시에서 피처 모듈은 겨울을 주제로 한 지도 이미지 하나로만 구성되어 있습니다.
소스 폴더 설정
- fdwintermapmodule: 피처 모듈
- fdmaploader: 메인 앱
- fdmaploader/storeloader: 피처 전달 JNI 인터페이스
기능 전달 인터페이스
fdmaploader/storeloader 폴더에는 기능 전달 API용 인터페이스 클래스 PlayStoreLoader 가 포함되어 있습니다. 이 API는 아직 완성되지는 않았으나, 기능 모듈을 로드하고 제거하는 데 필요한 관련 함수들을 포함하고 있습니다. 모듈 로드는 PlayStoreLoader::loadModule 호출로 시작됩니다. 프로세스 상태는 PlayStoreLoaderHandler 가 제공하는 신호를 통해 모니터링할 수 있습니다. 콜백 핸들은 PlayStoreLoader::getHandler 함수를 통해 얻을 수 있습니다. 이 예제 API는 또한 PlayStoreLoader::getInstalledModules 함수를 통해 이미 설치된 모듈을 확인하는 방법과, PlayStoreLoader::uninstallModules 함수를 통해 설치된 모듈을 제거하는 옵션을 제공합니다.
이 예제는 개발자가 기능 배포(Feature Delivery)를 테스트하기 위해 자신만의 콘텐츠를 쉽게 추가할 수 있도록 설계되었습니다. Play 스토어의 최대 패키지 크기를 초과할 경우, 지도 이미지(반드시 지도 이미지일 필요는 없으나 예제의 주제에 부합합니다)를 fdmaploader 및 fdwintermapmodule 내의 images 폴더에 추가할 수 있으며, 이미지 이름은 images.qrc 파일에 반드시 기재해야 합니다.
내부 구조
API 모듈은 두 부분으로 구성됩니다. Qt 인터페이스(PlayStoreLoader 및 PlayStoreLoaderHandler)와 Android: Google Split Install 인터페이스 호출을 처리하는 Java 클래스입니다. Qt 인터페이스는 주로 Java 클래스를 위한 패스스루 역할을 합니다. Qt 인터페이스는 API를 단순화하여, 기능 모듈이 로드될 때 Google의 SplitCompat 및 SplitInstall 클래스와 리스너가 자동으로 생성 및 해제되도록 합니다. 이 예제에서는 deferredInstall 및 언어 지원과 같은 API의 일부 기능이 제외되었습니다.
Qt creates and builds package suitable for Google Play deployment using qt6_add_android_dynamic_features when it is defined CMakeLists.
qt6_add_android_dynamic_features(${target_name}
FEATURE_TARGETS fdwintermapmodule)CMake 함수 ` qt6_add_android_dynamic_features `는 특정 동적 라이브러리를 Android 애플리케이션 타깃의 동적 기능으로 추가합니다. 이를 사용하려면 ` QT_ANDROID_GRADLE_MULTI_MODULE `이 활성화되어 있어야 합니다. 이는 컴파일 시간 플래그로 지정하거나 CMakeLists에서 설정할 수 있습니다. 인터페이스의 Java 부분이 위치한 폴더는 ` qt_add_android_dynamic_feature_java_source_dir`를 사용하여 빌드에 추가됩니다.
예제 앱에서는 storeloader 인터페이스를 사용하여 모듈을 로드합니다. Qt의 PlayStoreLoader 함수와 PlayStoreLoaderHandler 클래스는 예제 코드와 Java 간의 중개자 역할을 합니다.
void PlayStoreLoader::loadModule(const QString& callId,
const QString&moduleName)
{
if (callId.isEmpty()|| moduleName.isEmpty())
return;
if (!loaderInstance->registerNatives())
return;
if (!loaderInstance->loader().isValid()) {
qCritical("StoreLoader not constructed");
return;
}
loaderInstance->loader().callMethod<void>("installModuleFromStore", moduleName, callId);
}Java 클래스는 Split Install API에 대한 호출을 처리합니다.
m_splitInstallManager.startInstall(request)
.addOnSuccessListener(sessionId -> {
PlayStoreLoaderListener listener = m_listeners.get(callId);
if (listener != null)
listener.setSessionId(sessionId);
})바이너리 생성
명령줄을 사용하여 로컬에서 테스트할 수 있는 AAB 패키지를 빌드할 수 있습니다.
빌드 디렉터리를 생성하고, 소스 디렉터리와 동일한 수준에 위치하도록 설정합니다:
mkdir build-feature-delivery/ ; cd build-feature-delivery/구성:
path-to-qt-version/path-to-abi/bin/qt-cmake -GNinja -B . -S ../feature-delivery/ -DQT_USE_TARGET_ANDROID_BUILD_DIR=ON -DCMAKE_BUILD_TYPE=Debug빌드:
ninja aab테스트
빌드된 AAB는 ` bundletool ` 명령어에 ` --local-testing ` 매개변수를 지정하여 로컬에서 테스트할 수 있습니다. Android: BundleTool 문서 `bundletool build-apks ` 명령어는 APK 파일을 생성하며, 이 파일은 ` install-apks ` 명령어를 통해 기기나 에뮬레이터에 설치할 수 있습니다.
사용된 Bundletool 명령어
번들에서 APK 생성:
bundletool build-apks --bundle=/path/to/bundle.aab --output=/path/to/apk/package.apks --local-testing기기에 앱 설치:
bundletool install-apks --apks=/path/to/apk/package.apksPlay 스토어에 배포하기
생성된 AAB 패키지를 Google Play 스토어에 업로드하려면 패키지에 서명해야 합니다. 이를 위해 jarsigner 를 사용할 수 있습니다. 다음은 AAB 패키지에 서명하는 jarsigner 명령어의 예시입니다. Android: Jarsigner 문서를 참조하세요.
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore [path-to-keystore-file].keystore [path-to-aab-file].aab [alias]Qt 6.11 이전 버전의 기능 배포
Qt 6.11 이전 버전은 Google Play 스토어 호환 패키지 생성을 지원하지 않습니다. Qt 6.11 이상 버전을 사용할 수 없는 경우 Google Play 기능 제공(Feature Delivery)을 사용할 수 있는 방법이 있지만, 이를 위해서는 Android 프로젝트를 수동으로 생성하고 Qt 바이너리를 해당 프로젝트로 복사해야 합니다. 이 문서의 나머지 부분에는 이를 수행하는 방법에 대한 지침이 포함되어 있습니다. 이 지침이 모든 환경에서 100% 호환되지는 않을 수 있지만, Feature Delivery를 성공적으로 구현하는 데 유용한 팁을 제공해 줄 것입니다. 예제를 바탕으로 진행하는 것이 좋습니다.
기능 모듈
기능 모듈은 일반 라이브러리와 동일한 방식으로 빌드됩니다.
- Qt Creator 를 사용하여 C++ 공유 라이브러리를 생성하십시오.
- 기능을 구현하고 리소스를 추가합니다.
- 빌드하여 .so 바이너리를 생성합니다.
Feature Delivery는 C++ 라이브러리를 런타임 시점에 사용 가능할 수도 있고 아닐 수도 있는 일반 공유 라이브러리처럼 처리합니다. 라이브러리를 호출하기 전에 해당 라이브러리의 사용 가능 여부를 반드시 확인해야 합니다.
메인 앱 (Qt)
- Qt Creator 를 사용하여 앱을 생성합니다(여기서는Qt Quick 프로젝트 템플릿을 사용했습니다).
- Feature Delivery 라이브러리에 대한 액세스를 구현합니다. Google Play Feature Delivery Java 라이브러리의 핵심 클래스는 Android: SplitInstallManager입니다.
- QtCreator의 'Create Templates' 버튼 Projects -> Build&Run -> [target ABI] -> Build Steps -> Build Android APK 을 사용하여 Android 템플릿 파일을 생성할 수 있습니다. 템플릿은 프로젝트의 'android' 폴더에 생성됩니다.
-
폴더
.../android/src/java/[package...]에 Java 파일을 추가하고,CMakeLists.txt의 파일 경로:qt_add_executable... ...[path]/[java-filename.java] ... - 이 예제에서는 호출과 콜백을 처리하기 위한 Java 클래스를 생성했습니다. 이 Java 클래스는 JNI를 사용하여 Qt에서 호출됩니다. Android: Android 문서의 ‘on demand 모듈 요청’ 섹션에는 모듈을 요청하는 방법에 대한 간단한 설명이 있습니다.
- 프로젝트의 android 폴더 아래에 Java 파일을 추가할 때,
CMakeLists.txt에 QT_ANDROID_PACKAGE_SOURCE_DIR 속성을 추가해야 합니다:... set_property(TARGET appFDMainApp APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android) ... - 또한, 메인 앱
build.gradle에는 기능 API에 대한 의존성이 포함되어야 합니다. dependencies 블록에서implementation 'androidx.core:core:1.13.1'을
implementation("com.google.android.play:feature-delivery:2.1.0")으로
대체하십시오. - 기능 모듈이 제공하는 라이브러리에 대한 액세스 기능을 구현하십시오. 기능 모듈이 메인 앱에서 사용 가능할 수도 있고 아닐 수도 있으므로, 모듈은 빌드 시점에 링크되지 않으며 모듈에 대한 호출은 런타임에 해결되어야 합니다. 예시:
QString MapLoader::loadMapInfo() { QScopedPointer<QString> resultStr; typedef void* (*LoadMapInfoFunc)(); // wintermap 라이브러리가 있는지 확인 mWintermapLibrary.setFileName("fdwintermapmodule"); if (!mWintermapLibrary.load()) { qWarning() << Q_FUNC_INFO << "Failed to load library"; return QString(); } LoadMapInfoFunc loadMapInfo = ( LoadMapInfoFunc) mWintermapLibrary.resolve("loadMapInfo"); if (loadMapInfo) { void* result = loadMapInfo(); resultStr.reset(static_cast<QString*>(result)); } else qWarning() << Q_FUNC_INFO << "Function loadMapInfo not loaded"; return *resultStr.data(); } - 메인 앱의 사용자 인터페이스 및 기타 필수 부분을 구현합니다.
기능 모듈 (Qt)
- Qt Creator 를 사용하여 앱을 생성합니다(Qt C++ 라이브러리 프로젝트 템플릿을 사용했습니다).
- 모듈이 제공하는 기능을 구현합니다.
Android 프로젝트 (Android)
Feature Delivery용 Android 앱 번들을 빌드하기 위한 프로젝트 생성은 Android 문서를 기반으로 하며, 주로 다음을 참조합니다:
수동으로 또는 Android Studio를 사용하여(‘No Activity’ 템플릿 사용) Android 프로젝트를 생성합니다. 이 프로젝트는 최상위 프로젝트와 두 개의 하위 프로젝트( app 및 feature-module)를 포함하도록 수정됩니다. Android Studio 템플릿은 app 하위 프로젝트를 생성하며, feature-module 는 ‘ File -> New -> New Module ’ 템플릿을 사용하여 추가할 수 있습니다.
이 템플릿 프로젝트에는 몇 가지 수정이 필요합니다:
- 메인 레벨
build.gradle에 Feature Delivery 플러그인을 추가합니다:plugins { id 'com.android.application' version '8.5.2' apply false id 'com.android.dynamic-feature' version '8.5.2' apply false id 'com.android.library' version '8.5.2' apply false } settings.gradle에 기능 모듈을 추가하고, 필요한 경우rootProject.name를 수정하십시오:... rootProject.name = "name-of-the-root-project" include(:app) include(:name-of-the-feature-module)
app - 하위 프로젝트
- Android 프로젝트에는 메인 앱 프로젝트의 Qt 바이너리가 필요합니다:
- Qt 빌드 폴더(
[build directory]/android-build/libs/[target ABI])에 있는 네이티브 라이브러리를app/src/main/jniLibs/[target ABI] [build directory]/android-build/libs/에 있는 JAR 파일을 다음 위치로 복사하십시오:app/libs/
- Qt 빌드 폴더(
- Qt 빌드에서
res폴더의 내용과AndroidManifest.xml,local.properties파일도 Android 프로젝트의 해당 위치로 복사됩니다. - 기능 모듈에 대한 문자열이 포함된
feature_names.xml파일을app/src/main/res/values폴더에 추가합니다:<?xml version="1.0" encoding="utf-8"?> <resources> <string name="feature_module_name">name-of-the-feature-module-here</string> </resources> app/src/main/res/raw폴더에keep.xml파일을 추가하고 다음 내용을 포함시킵니다:<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@string/feature_module_winter_map" tools:discard="" />
앱 하위 프로젝트 빌드 파일 수정 사항
Android 프로젝트로 복사된 빌드 파일에는 일부 수정이 필요합니다.
app - 하위 프로젝트
buildScript및repositories블록을 제거하십시오.- 메인 앱의 `
build.gradle` 내 `Android` 블록은 일부 수정이 필요합니다:defaultConfigpackagingOptionsdynamicFeaturessourceSetsaaptOptionsdependencies
android {
...
defaultConfig {
...
applicationId "your-project-name-here"
...
}
packagingOptions.jniLibs.useLegacyPackaging true
dynamicFeatures = [":your-dynamic-feature-name-here"]
sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qtAndroidDir + '/res', 'res']
resources.srcDirs = ['resources']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['src/main/jniLibs/']
}
}
// Do not compress Qt binary resources file
aaptOptions {
noCompress 'rcc'
}
...
}
dependencies {
...
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'com.google.android.play:feature-delivery:2.1.0'
implementation libs.material
...
}또한 Android 블록에 서명 구성을 추가하십시오:
android {
...
signingConfigs {
release {
storeFile file("/absolute/path/to/the/keystore.jks")
storePassword "myStorePassword"
keyAlias "myKeyAlias"
keyPassword "myKeyPassword"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
...
}
}
...
}Qt에서는 gradle.properties 에 프로젝트 변수를 추가했습니다. 필요한 경우 androidPackageName 의 값을 변경하십시오.
package을 제거하십시오:... <manifest ... android:package... <--remove ... > ...- 필요한 경우
label및android.app.lib_name을 수정하십시오:... <application ... android:label=" ... <activity ... > <meta-data android:name="android.app.lib_name" android:value=" ... /> ...
기능 모듈 - 하위 프로젝트
앱 모듈과 기능 모듈은 최상위 Android 프로젝트의 하위 프로젝트로 생성됩니다. 폴더 및 파일 구조는 앱 하위 프로젝트와 유사합니다.
- Qt 빌드에서 생성된 기능 모듈의 바이너리 파일은
[name-of-feature-module]/src/main/jniLibs/ - 메인 앱과 마찬가지로, `
src/main/res/` 폴더에는 각각 `qtprovider_paths.xml`과 `libs.xml`을 포함하는 `xml` 및 `values` 폴더가 있어야 합니다. 두 파일 모두 앱 프로젝트에서 복사할 수 있습니다. src/main/res/폴더에 drawable 또는 mipmap 폴더가 포함되어 있지만 해당 기능에 필요하지 않은 경우, 이를 제거할 수 있습니다.- 피처 모듈에서
src/main/res/values에는app_name필드가 포함되어서는 안 됩니다. 다른 용도로 strings.xml이 필요하지 않은 간단한 프로젝트의 경우, 이 파일을 제거할 수 있습니다. libs.xml다음은 기능 모듈의 이름만 포함합니다:... <array name="load_local_libs"> <item>name-of-the-feature-module-here</item> </array> <string name="static_init_classes"></string> <string name="use_local_qt_libs">0</string> <string name="bundle_local_qt_libs">0</string> ...AndroidManifest.xmlsrc/main/디렉터리에 다음이 추가됩니다:<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:dist="http://schemas.android.com/apk/distribution"> <dist:module dist:instant="false" dist:title="@string/feature_module_title_string"> <dist:delivery> <dist:on-demand /> </dist:delivery> <dist:fusing dist:include="false" /> </dist:module> <!-- This feature module does contain code. --> <application android:hasCode="true"/> </manifest>- 기능 모듈의 `
build.gradle`는 앱 프로젝트의 것과 상당히 유사하지만, 몇 가지 변경 사항이 있습니다. 다음은 예시입니다:plugins { id 'com.android.dynamic-feature' } dependencies { implementation project(':app') implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation 'com.google.android.play:feature-delivery:2.1.0' } android { namespace = androidPackageName compileSdk = androidCompileSdkVersion ndkVersion androidNdkVersion // Extract native libraries from the APK packagingOptions.jniLibs.useLegacyPackaging true defaultConfig { minSdkVersion qtMinSdkVersion targetSdkVersion qtTargetSdkVersion } sourceSets { main { manifest.srcFile 'src/main/AndroidManifest.xml' resources.srcDirs = ['resources'] renderscript.srcDirs = ['src'] assets.srcDirs = ['assets'] jniLibs.srcDirs = ['src/main/jniLibs/'] } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } // Do not compress Qt binary resources file aaptOptions { noCompress 'rcc' } } gradle.properties파일은 앱 하위 프로젝트에서 복사해 올 수 있으며, ‘androidPackageName’을 기능 모듈의 패키지로 변경하면 됩니다.
빌드 및 배포
AAB 번들은 Gradle 래퍼를 사용하여 명령줄에서 빌드할 수 있습니다: ` ./gradlew bundle`. 생성된 AAB는 ` build/outputs/bundle/release `(또는 ` debug`) 폴더에 저장됩니다. 이후 AAB를 Google Play 스토어로 복사하여 테스트용으로 출시할 수 있습니다. 또한 ` bundletool `에 ` --local-testing ` 매개변수를 지정하여 로컬에서 테스트할 수도 있습니다.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.