Android 앱 아이콘이 PNG로 올바르게 표시되도록 수정
목차
Android에서 앱 아이콘이 생각한 대로 안 나오는 경험, 한 번쯤은 겪어봤을 거다. PNG로 아이콘을 준비해서 mipmap-* 디렉토리에 넣어뒀는데 런처에서 흰 배경에 짤린 이상한 원이 나온다거나, 아니면 시스템 기본 아이콘이 보인다거나. 이번 커밋이 딱 그 케이스였다.
mipmap-anydpi-v26이 뭔데 문제가 됐나
Android 8.0(API 26)부터 "어댑티브 아이콘(Adaptive Icon)"이라는 개념이 도입됐다. 배경 레이어(background)와 전경 레이어(foreground)를 XML로 정의해두면 런처가 디바이스 테마에 맞게 - 원형, 모서리 둥근 사각형, 스쿼클 등 - 자유롭게 잘라서 표시해주는 방식이다. 이걸 정의하는 디렉토리가 mipmap-anydpi-v26이고, 여기에 ic_launcher.xml, ic_launcher_round.xml 같은 벡터 기반 파일이 들어간다.
문제는 Android 리소스 해석 우선순위에 있다. 시스템은 API 26 이상 기기에서 mipmap-anydpi-v26을 다른 어떤 mipmap 디렉토리보다 먼저 고른다. anydpi qualifier는 dpi 무관하게 선택되는 특성이 있고 여기에 -v26까지 붙어 있어, 해당 XML이 PNG 리소스를 사실상 완전히 덮어쓴다. mipmap-hdpi, mipmap-xhdpi, mipmap-xxhdpi에 PNG를 열심히 넣어놔도 API 26 이상 기기에서는 무시된다.
어댑티브 아이콘 XML이 제대로 구성돼 있지 않으면 - foreground drawable이 빠졌다거나, 참조하는 리소스 경로가 틀렸다거나 - 런처에서 기본 아이콘이 나오거나 아예 깨져서 표시된다. mipmap-anydpi-v26 디렉토리를 만들어두고 XML 내부를 끝까지 구현하지 않으면, PNG가 멀쩡히 살아있어도 그게 쓰이지 않는 아이러니한 상황이 된다.
파손된 어댑티브 아이콘 XML이 있을 때 대략 이런 구조다:
<!-- mipmap-anydpi-v26/ic_launcher.xml -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<!-- ic_launcher_foreground 가 실제로 없으면 런처에서 깨짐 -->
</adaptive-icon>
해결은 단순했다. 어댑티브 아이콘을 쓸 계획이 없으니 디렉토리 자체를 제거했다. 그러면 시스템이 평범하게 mipmap-*dpi 디렉토리의 PNG를 해상도에 맞춰 선택한다.
res/
mipmap-hdpi/
ic_launcher.png
ic_launcher_round.png
mipmap-xhdpi/
ic_launcher.png
ic_launcher_round.png
mipmap-xxhdpi/
ic_launcher.png
ic_launcher_round.png
mipmap-xxxhdpi/
ic_launcher.png
ic_launcher_round.png
# mipmap-anydpi-v26/ <- 이게 있으면 위 PNG들이 전부 무시됨, 제거
런처 아이콘 PNG 해상도는 아래 기준을 따른다:
| 디렉토리 | 배율 | 권장 크기 |
|---|---|---|
| mipmap-mdpi | 1x | 48×48 |
| mipmap-hdpi | 1.5x | 72×72 |
| mipmap-xhdpi | 2x | 96×96 |
| mipmap-xxhdpi | 3x | 144×144 |
| mipmap-xxxhdpi | 4x | 192×192 |
Play Store 제출용 고해상도 아이콘(512×512)은 콘솔에서 별도로 올리는 거고, 런처에 쓰이는 건 위 사이즈들이다. 이걸 빼먹으면 특정 기기에서 흐릿하게 나오거나 특정 dpi 버킷에서 리소스를 못 찾는 상황이 생기기도 한다.
pay-monitor 앱이 뭘 하는지
이 앱은 금융 알림을 실시간으로 캡처해서 서버로 전달하는 역할을 한다. 입금 알림, 카카오페이 송금 같은 이벤트를 파싱해서 API로 쏴주는 구조고, 접근성 서비스를 활용한 자동수령 자동화도 포함돼 있다. 백그라운드 서비스가 핵심이라, 앱이 24시간 떠 있어야 한다.
여기서 생기는 주요 과제가 배터리 최적화에 의한 강제 종료다. Android 도즈 모드(Doze Mode)나 제조사별 절전 정책이 백그라운드 프로세스를 조용히 죽이는 경우가 많다. 삼성 One UI, 샤오미 MIUI 같은 커스텀 레이어는 특히 공격적으로 프로세스를 정리한다. startForeground()로 상태표시줄 알림을 유지하거나, 서비스가 죽었을 때 자동으로 재시작하는 로직을 넣어두지 않으면 아무도 모르게 꺼진 채로 방치되는 상황이 생긴다.
포어그라운드 서비스를 유지하려면 반드시 알림 채널을 만들고 알림을 연결해야 한다. API 31 이후로는 FOREGROUND_SERVICE_TYPE도 명시해야 하고, 이걸 빼면 크래시가 난다. WakeLock은 PARTIAL_WAKE_LOCK을 써서 CPU가 슬립 상태로 빠지는 걸 막는 방식을 주로 쓰는데, 이걸 제대로 해제하지 않으면 배터리를 계속 갈아먹는 반대 문제가 생기기도 한다. 획득/해제 쌍을 항상 맞춰야 한다.
이번 커밋은 그런 기능 로직과는 별개로 앱 아이콘 표시 문제를 다뤘다. 변경 파일 2개, 범위는 최소.
수정 범위를 최소화하는 이유
버그 픽스할 때 "이왕 하는 김에" 리팩토링이나 다른 개선까지 끼워 넣으면 나중에 고생한다. 코드 리뷰 범위가 커지고, 원래 버그 픽스와 다른 변경이 뒤섞여서 git blame이나 diff에서 추적하기 어려워진다. 의도치 않은 side effect가 섞이기 쉽고, 나중에 문제가 생겼을 때 어느 변경이 원인인지 찾는 데 시간이 두 배로 든다.
원칙은 단순하다. 버그 원인 부분만 건드린다. mipmap-anydpi-v26 제거가 이번에 해야 할 전부였으니 그것만 했다. 앱 아이콘 말고 다른 리소스 정리가 필요하다면 그건 별도 커밋으로.
이게 처음엔 답답하게 느껴지기도 한다. 보다 보면 다른 것도 손대고 싶어지는데, 그 욕구를 억제하고 커밋 단위를 좁게 유지하는 게 팀이 커지고 히스토리가 쌓일수록 훨씬 중요해진다는 걸 계속 실감한다. "이 커밋이 뭘 한 커밋이냐"에 한 줄로 답할 수 있는 상태, 그게 장기적으로 훨씬 편하다. 이번에도 그 원칙대로만 했다.
댓글 0
첫 댓글 달아줘.