[Android] 업데이트 안 해도 아이콘을 요리조리 바꾸는 법 (Dynamic App Icon / feat. 듀오링고)

2025. 12. 21. 17:45·Android

동기

듀오링고를 아시나요?

연속 학습일을 채우지 않으면 앱 아이콘이 바뀌며 링고가 화내고 얼어붙고 난리난리가 납니다.

지금은 울고 있네요, 조금 미안한 마음이 듭니다.

불쌍..

듀오링고 뿐만 아니라 무신사의 경우도 이벤트 기간에 무진장으로 앱 아이콘을 변경하는 등

업데이트 없이 특정 기간 동안 앱 아이콘을 변경하는 서비스가 많아요!

 

요거 어떻게 할까요?

따라해보고 싶어서 무작정 박치기 해봤어요 (^///^)

 

0. 아이콘 에셋 추가

🐱 res 우클릭 > new > image asset > png 파일 불러오기

 

위 방법으로 아이콘 파일 추가해, mipmap에 저장될 수 있게 해주세요!

 

1. activity-alias 설정

여러 개의 아이콘을 관리할 수 있는 핵심 기능입니다!

AndroidManifest.xml 에서 사용하는 태그예요.

 

해당 파일을 열어보면, 기본적으로 아래의 코드가 있을 거예요.

<application
    android:name=".presentation.DiveApplication"
    android:allowBackup="true"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.Dive"
    android:networkSecurityConfig="@xml/network_security_config"
    tools:targetApi="31">
    <activity
        android:name=".presentation.main.MainActivity"
        android:exported="true"
        android:theme="@style/Theme.Dive">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

사용자가 앱을 실행할 때 표시되는 첫 번째 화면을 설정하기 위해, 기본 액티비티를 <activity> 태그로 작성해요.

<activity>의 속성을 자세히 알아볼게요.

  • name : 액티비티 클래스 이름을 지정해요.
  • intent-filter : 본 액티비티가 어떤 인텐트를 받을 수 있는지 조건을 정의해요. 앱이나 시스템에서 intent를 특정 조건으로 호출할 때, 해당 조건을 intent-filter로 갖고 있는 액티비티가 열리게 돼요.
    • android.intent.action.MAIN → 앱 시작 시 본 액티비티로 시작한다.
    • android.intent.category.LAUNCHER → 이 액티비티의 아이콘이 앱 진입점이 된다.

우리의 앱은 시스템 런처로 실행돼요.

메인 액티비티가 MAIN+LAUNCHER 조건을 <intent-filter>로 정의해두면 앱의 진입점이 되고, 런처가 해당 인텐트를 보내 앱을 실행해요.

따라서 앱 아이콘이 시스템 홈 화면에 노출되고, 클릭 시 앱이 실행될 수 있어요.

 

이 아래에 <activity-alias>를 추가해줄게요.

<application
    android:name=".presentation.DiveApplication"
    android:allowBackup="true"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.Dive"
    android:networkSecurityConfig="@xml/network_security_config"
    tools:targetApi="31">
    <activity
        android:name=".presentation.main.MainActivity"
        android:exported="true"
        android:theme="@style/Theme.Dive">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    
    <!-- 여기 -->
    <activity-alias
	    android:name=".Kakashi"
	    android:targetActivity=".presentation.main.MainActivity"
        android:exported="true"
	    android:icon="@mipmap/ic_kakashi"
	    android:roundIcon="@mipmap/ic_kakashi"
	    android:enabled="false">
	    <intent-filter>
	        <action android:name="android.intent.action.MAIN" />
	        <category android:name="android.intent.category.LAUNCHER" />
	    </intent-filter>
	</activity-alias>
    <!-- 여기 -->    
</application>

 

각 속성을 알아볼게요. 

  • name* : 별명이에요! 맘대로 써줘요. 가능하면 아이콘에 대한 이름으로 써줘야 구분하기 편하겠죠?
  • targetActivity* : 실제로 실행할 액티비티예요! 꼭꼭 기본 activity의 name과 동일하게 써줘야, 앱이 정상적으로 실행돼요.
  • exported* : 외부에서 본 액티비티를 실행할 수 있는지를 의미해요. 앱 내부적으로 호출되는 액티비티라면 false도 괜찮겠지만, 본 alias를 통해 열리는 액티비티는 앱의 진입점으로 시스템에 의해 호출돼요. 즉, 외부에서 실행되므로 true여야 합니다.
  • icon, roundIcon* : 저장한 에셋 파일 넣어줘요.
  • enabled* : 본 alias로 타겟 액티비티를 인스턴스화할 수 있는지를 enabled 값으로 결정해요. 즉, 진입점, 앱 아이콘을 본 alias에서 설정한 것으로 할지를 enabled 값으로 조절해요.
체크리스트
✅ name은 자유롭게
✅ targetActivity, <intent-filter>는 MainActivity에서 복붙해오기
✅ exported=true, enabled=false
✅ icon, roundIcon은 저장한 에셋 불러오기

 

저는 총 4개의 alias를 만들어줬어요.

<activity-alias
    android:name=".Kakashi"
    android:targetActivity=".presentation.main.MainActivity"
    android:exported="true"
    android:icon="@mipmap/ic_kakashi"
    android:roundIcon="@mipmap/ic_kakashi"
    android:enabled="false">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity-alias>

<activity-alias
    android:name=".Naruto"
    android:targetActivity=".presentation.main.MainActivity"
    android:icon="@mipmap/ic_naruto"
    android:exported="true"
    android:roundIcon="@mipmap/ic_naruto"
    android:enabled="false">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity-alias>

<activity-alias
    android:name=".Sasuke"
    android:targetActivity=".presentation.main.MainActivity"
   android:exported="true"
    android:icon="@mipmap/ic_sasuke"
    android:roundIcon="@mipmap/ic_sasuke"
    android:enabled="false">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity-alias>

<activity-alias
    android:name=".Sakura"
    android:targetActivity=".presentation.main.MainActivity"
    android:exported="true"
    android:icon="@mipmap/ic_sakura"
    android:roundIcon="@mipmap/ic_sakura"
    android:enabled="false">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity-alias>

 

2. enabled 조절하기

이제 alias의 enabled를 조절하며 아이콘을 변경할 거예요.

다른 아티클에서 주운 코드를 제 스타일로 수정해보았어요.

 

우선 AppIcon이라는 enum class를 만들어 관리할 이름과 alias name을 저장해주었어요.

enum class AppIcon(val componentName: String) {
    DEFAULT("com.sopt.dive.presentation.main.MainActivity"),
    KAKASHI("com.sopt.dive.Kakashi"),
    NARUTO("com.sopt.dive.Naruto"),
    SASUKE("com.sopt.dive.Sasuke"),
    SAKURA("com.sopt.dive.Sakura")
}
🦉 DEFAULT MainActivity를 추가하지 않으면?
기본 진입점이 활성화된 상태로 alias를 활성화시키면 앱의 진입점이 두 개가 됩니다!
alias 뿐만 아니라 MainActivity의 활성 여부도 함께 관리하기 위해 enum class에 작성해요.
카게분신노쥬츠..

 

그리고 보여줄 앱 아이콘을 받아 해당 alias만 활성화하고, 나머지는 비활성화하는 메서드를 작성해줍니다!

fun Activity.changeIcon(
    targetIcon: AppIcon
) {
    AppIcon.entries.forEach { icon ->
        val state = if (icon == targetIcon) {
            PackageManager.COMPONENT_ENABLED_STATE_ENABLED
        } else {
            PackageManager.COMPONENT_ENABLED_STATE_DISABLED
        }

        packageManager.setComponentEnabledSetting(
            ComponentName(
                this,
                icon.componentName
            ),
            state,
            PackageManager.DONT_KILL_APP
        )
    }
}

 

본 메서드를 MainActivity에서 실행해요.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) { ... }

    override fun onDestroy() {
    	changeIcon(AppIcon.NARUTO)
		
    	super.onDestroy()
	}
}

 

🦉 onDestroy에서 실행하는 이유?
처음에는 onCreate에서 실행했는데요, 메인이 비활성화되자마자 앱이 꺼지더라구요..!
저도 정확한 이유를 찾지는 못했는데, ENABLED 상태인 alias/component가 DISABLED로 변경되면 안드로이드 시스템이 앱을 닫는다는 글을 블로그에서 봤어요.

해결법이 여러 가지 있는데, 가장 쉬운 방법은 앱을 닫을 때 코드를 실행해요!
어차피 닫을 거니까 종료돼도 상관없음 ㅎㅎ

 

메인을 비활성화할 때의 또 다른 문제점이 있는데욥,,

에뮬레이터에서 아이콘을 클릭해 앱을 실행하는 건 가능하지만 안드로이드 스튜디오의 RUN 기능으로는 메인을 찾을 수 없다는 오류가 뜨며 실행이 안 돼요.

Activity class {com.sopt.dive/com.sopt.dive.presentation.main.MainActivity} does not exist

 

이것도 정확한 이유를 못찾아서 유추해보았는데요, 아래 내용은 제 뇌피셜입니다!!

  • 에뮬레이터에서 아이콘 클릭 : 시스템을 통해 실행되는 것으로, 활성화된 alias가 intent-filter로 앱 실행 intent를 받아주기 때문에 정상 동작.
  • 안드로이드 스튜디오의 RUN : 지정된 액티비티를 직접 실행. 기본 액티비티를 비활성화 했기 때문에 앱 실행이 불가.

그럼 어떻게 하느냐? 개발 중에는 메인 액티비티 안 닫게 하면 될 것 같아요!

일단 돌아가면 되는 거 아닐까요?

저는 이런 식으로 시간 활용해서 자동으로 돌아가게 테스트했어요..ㅎㅎ

override fun onDestroy() {
    val currentMinute = Calendar.getInstance().get(Calendar.MINUTE)

    val target = if (currentMinute % 2 == 0) {
        AppIcon.NARUTO
    } else {
        AppIcon.DEFAULT
    }

    changeIcon(target)

    super.onDestroy()
}

눈물나는 화질..

 

3. AppIcon 불러오는 로직 작성

그렇담 상용 서비스처럼 원격으로 앱 아이콘을 어떻게 조절하려면 어떻게 해야할까요?

override fun onDestroy() {
    val target = // ⭐ 여기!! //

    changeIcon(target)

    super.onDestroy()
}

 

target 값을 원격으로 불러오면 돼요!

 

자체 서버가 있다면 이를 위한 API를 하나 만들어 사용할 수도 있고,

firebase 연동 중이라면 remote config를 사용하는 것도 좋은 방법이예요.

 

결론

간단히 설명하면 아이콘이 혼자 바뀌는 앱은요

  1. 앱 아이콘 소스를 여러 개 넣어둔다.
  2. alias로 별명-아이콘을 설정한다.
  3. MainActivity에서 remote config를 읽고, 아이콘 변경 코드를 실행한다.

이 플로우로 구현할 수 있어요.

 

주의할 점은 업데이트 없이 아이콘을 바꿀 수 있다고 말은 하지만요

  1. 미리 아이콘 에셋 및 변경 코드가 심어져 있어야 하고
  2. 사용자가 앱을 실행해야 아이콘이 바뀝니다!

이벤트가 계획된 시점부터 미리미리 업데이트 해서 아이콘 심어둬야 이벤트 기간 됐을 때 아이콘 바꿔줄 수 있을 것 같아요.

 

참고자료

🔗 https://oguzhanaslann.medium.com/dynamic-app-icon-in-android-a61f8570ab9f

🔗 https://blog.stackademic.com/change-app-icon-f4394eb73fe1

🔗 https://developer.android.com/guide/components/activities/intro-activities?hl=ko

 

활동 소개  |  App architecture  |  Android Developers

활동은 사용자가 전화 걸기, 사진 찍기, 이메일 보내기 또는 지도 보기와 같은 작업을 하기 위해 상호작용할 수 있는 화면을 제공하는 애플리케이션 구성요소입니다. 각 활동에는 사용자 인터페

developer.android.com

🔗  https://developer.android.com/guide/topics/manifest/activity-alias-element?hl=ko

 

<activity-alias>  |  App architecture  |  Android Developers

활동의 별칭으로, targetActivity 속성에서 이름이 지정됩니다. 타겟은 별칭과 동일한 애플리케이션에 있어야 하며 manifest에서 별칭보다 먼저 선언해야 합니다. 별칭은 타겟 활동을 독립된 항목으로

developer.android.com

🔗 https://blog.famapp.in/blog/change-app-icon-dynamically-in-android/

'Android' 카테고리의 다른 글

[Android][Jetpack Compose] 커스텀 드롭다운 메뉴를 구현해보자  (0) 2026.01.28
[Kotlin] Kotlin in Action 3장. 함수 정의와 호출  (0) 2025.11.12
[Kotlin] 클래스를 알아보자 (클래스와 프로퍼티 심화편)  (0) 2025.11.06
'Android' 카테고리의 다른 글
  • [Android][Jetpack Compose] 이미지를 불러와, 최적화하고, 서버에 전송해보자 (feat. presigned-url)
  • [Android][Jetpack Compose] 커스텀 드롭다운 메뉴를 구현해보자
  • [Kotlin] Kotlin in Action 3장. 함수 정의와 호출
  • [Kotlin] 클래스를 알아보자 (클래스와 프로퍼티 심화편)
오카룽
오카룽
Dart/Flutter, Kotlin, Android Jetpack, Jetpack Compose
  • 오카룽
    Okarun.dev
    오카룽
  • 전체
    오늘
    어제
    • 분류 전체보기 (20)
      • Architecture (1)
      • Android (14)
      • Flutter (0)
      • Collaboration (2)
      • Launching (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • Github
  • 공지사항

  • 인기 글

  • 태그

    list state
    jetpack compose
    dynamic icon
    안드로이드
    slack
    Actions
    sticky header
    스크롤 방향
    android studio
    이미지 해상도 관리
    스크롤
    Kotlin
    플로팅
    lazy column
    github
    android
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
오카룽
[Android] 업데이트 안 해도 아이콘을 요리조리 바꾸는 법 (Dynamic App Icon / feat. 듀오링고)
상단으로

티스토리툴바