Android 16 타겟 SDK 대응은 트랙 하나만 올려선 끝나지 않는다

Google Play Console에 “8월 31일까지 API 36을 타겟팅하지 않으면 앱을 업데이트할 수 없다”는 정책 알림이 떴다. 처음엔 계정에 있는 다른 앱 얘기인 줄 알고 엉뚱한 프로젝트를 열었고, 나중엔 비공개 테스트 트랙에만 새 빌드를 올려놓고 다 됐다고 착각했다. 두 번 다 헛다리였다

이 글은 개인이 만든 Flutter 단위 변환 앱의 targetSdk를 35에서 36으로 올리고 스토어 게시까지 마친 기록이다. 결론부터 말하면 이번 작업에서 가장 값진 교훈은 코드가 아니라 게시에 있었다. Play Console의 정책 준수 판정은 트랙 하나가 아니라 계정에 활성화된 모든 트랙을 기준으로 본다. targetSdk를 36으로 올린 빌드를 아무리 잘 만들어도, 쓰고 있는 트랙 중 하나라도 구버전에 머물러 있으면 경고는 사라지지 않는다

갑자기 온 이메일과 알림은 내가 뭘 잘못해서 온 게 아니었다

그리고 정책 센터에 뜬 문구는 이랬다.

문제: 앱이 Android 16(API 수준 36) 이상을 타겟팅해야 함
2026년 8월 31일부터 최신 Android 출시로부터 1년 이내의 대상 API 수준을
타겟팅하지 않는 경우 앱을 업데이트할 수 없습니다.
규정을 준수하지 않는 가장 높은 대상 API 수준은 Android 15(API 수준 35)입니다.

당시 앱의 targetSdk는 35였고 기한은 약 40일 남아 있었다. 가장 먼저 든 생각은 “내가 최근에 뭘 잘못 건드려서 이게 뜬 건가”였다. 결론은 아니었다

공식 문서를 확인해 보니 이건 Google Play가 2018년경부터 매년 반복해 온 강제 정책이었다. 최신 Android 버전이 나오면 대략 1년 뒤를 기한으로 잡아 “신규 앱과 업데이트는 그 버전을 타겟팅하라”는 조건을 건다. 2026년 기준으로는 8월 31일부터 신규 앱·업데이트가 API 36 이상이어야 한다. 왜 강제하냐면, 매 버전의 보안·성능 개선은 앱이 targetSdkVersion으로 그 버전을 명시적으로 선언해야 실제로 적용되기 때문이다. 오래된 targetSdk를 방치한 앱이 새 OS의 보호 기능을 못 받는 상황을 막으려는 조치다

이 알림이 개발자의 행위와 무관하게 자동 발송된다는 것도 이때 확인했다. 같은 계정의 다른 앱(뭐 먹지?)도 동시에 같은 알림을 받았기 때문이다. Play Console이 계정 내 모든 앱의 targetSdk를 주기적으로 스캔해서 기준 미달인 앱에 일괄 경고를 보내는 구조였다. 앱 단위가 아니라 계정 전체를 훑는 스윕이라는 사실이, 나중에 겪을 트랙 문제의 복선이었다.

여기서 첫 삽질도 나왔다. 알림이 다른 앱 얘기인 줄 알고 그쪽 프로젝트에서 처리하려다, 실제로는 지금 작업 중이던 앱 얘기라는 걸 뒤늦게 확인했다. 계정에 앱이 여러 개라면 알림 화면 우측 상단에 어떤 앱이 선택돼 있는지부터 보는 게 먼저다

코드 변경은 세 줄이었지만 브랜치는 나눴다

실제 코드 변경은 단순했다. android/app/build.gradle에서 SDK 버전 두 개를 올렸다

 android {
     namespace = "com.hyeok.easy_unit_conversion"
-    compileSdk = 35
+    compileSdk = 36
     defaultConfig {
         minSdkVersion 23
-        targetSdk = 35
+        targetSdk = 36
     }
 }

빌드 설정 변경은 실패했을 때 파장이 큰 축이라, main에 바로 커밋하지 않고 짧은 브랜치(chore/target-sdk-36)를 따서 작업하고 로컬 검증 후 병합했다.

여기서 예상과 달랐던 지점이 하나 있다. Android Gradle Plugin은 그대로 8.6.0을 쓰고 있었는데, compileSdk = 36으로 올려도 별문제 없이 빌드됐다. “Flutter가 곧 AGP 8.11.1 이상을 요구할 거다”라는 경고는 떴지만 이번 이슈와는 무관한 별개 경고였다. compileSdk를 올린다고 AGP·Gradle·Kotlin 버전까지 항상 같이 올려야 하는 건 아니라는 걸 이때 확인했다. 미리 겁먹고 전부 올렸다면 오히려 다른 호환성 문제를 만들었을 것이다

Android 16의 동작 변경 두 가지는 상황에 따라 대응이 갈렸다

targetSdk를 36으로 올리면 Android 16의 동작 변경 두 가지가 따라온다. 둘 다 점검했는데, 하나는 손이 필요했고 하나는 아니었다

첫째는 predictive back이다. API 36부터 뒤로가기 프리뷰 애니메이션이 기본 활성화된다. 마침 직전에 “변환기 화면에서 시스템 뒤로가기를 누르면 앱이 바로 꺼지는” 버그를 PopScope로 고쳐 둔 상태였는데, Flutter 공식 마이그레이션 가이드에 따르면 이게 제대로 동작하려면 매니페스트 플래그가 하나 더 필요했다

     <application
         android:label="다잰다"
         android:name="${applicationName}"
-        android:icon="@mipmap/launcher_icon">
+        android:icon="@mipmap/launcher_icon"
+        android:enableOnBackInvokedCallback="true">

이 한 줄이 없어도 뒤로가기 자체는 레거시 방식으로 동작한다. 다만 OS 차원의 predictive back 제스처 프리뷰는 이 플래그가 있어야 정식 지원된다. PopScope만 넣고 끝냈다면 절반만 대응한 셈이었다

둘째는 edge-to-edge 강제화다. API 36부터 앱이 상태바·내비게이션바 뒤까지 그려야 하고 opt-out이 안 된다. 이건 별도 대응이 필요 없었다. 앱의 모든 화면을 이미 Flutter의 SafeArea로 감싸고 있었기 때문이다. 같은 동작 변경이라도, 원래 시스템 바 영역을 처리해 두었는지에 따라 대응이 필요할 수도, 없을 수도 있다는 점이 갈렸다

마지막으로 게시를 위해 버전 코드를 올렸다. Play Console에 이미 versionCode 1이 올라가 있어서, 새 빌드를 게시하려면 더 높은 값이 필요했다

-version: 1.0.0+1
+version: 1.0.1+2

“빌드된다”와 “Android 16에서 확인했다”는 다르다

flutter analyze만 돌리고 끝낼 수도 있었다. 하지만 빌드가 통과한다는 것과 그 OS 버전에서 실제로 문제없이 돈다는 것은 다른 얘기라, API 36 환경을 직접 띄워서 확인하기로 했다.

로컬 SDK에 API 36 시스템 이미지가 없어서 새로 받았다.

sdkmanager --install "system-images;android-36;google_apis_playstore;arm64-v8a"
avdmanager create avd -n Pixel_API36 \
  -k "system-images;android-36;google_apis_playstore;arm64-v8a" -d "pixel_8"

에뮬레이터를 부팅해 ro.build.version.sdk가 36인 걸 확인하고, 실제로 손으로 눌러 본 것들은 이랬다. 홈 화면이 edge-to-edge 강제화 환경에서도 상태바·내비게이션바에 콘텐츠가 겹치지 않고 렌더링되는지, 변환기 화면에 들어갔다가 시스템 뒤로가기로 홈에 정상 복귀하는지(predictive back이 기본 켜진 환경에서도), 그리고 adb logcat에 크래시나 FATAL EXCEPTION이 없고 광고가 Ad loaded 로그를 남기는지였다.

서명된 release AAB도 따로 빌드해서, 디버그 서명이 아니라 업로드 키로 정상 서명됐는지 확인했다.

unzip -l app-release.aab | grep META-INF
# UPLOAD.SF / UPLOAD.RSA 존재 → 업로드 키로 서명됨

이 검증 단계에서 문제가 나오진 않았다. 하지만 만약 edge-to-edge나 predictive back에서 화면이 깨졌다면, 에뮬레이터 없이 스토어에 올린 뒤에야 사용자 리뷰로 알게 됐을 것이다. 로컬에 해당 API 이미지가 없으면 새로 받아서라도 띄워 보는 편이 안전하다

진짜 함정은 게시에 있었다 – 트랙 하나만 올려선 끝나지 않는다

코드와 검증은 오히려 순탄했다. 발이 걸린 건 게시였다. 이 앱은 비공개 테스트(Closed Testing – Alpha)와 내부 테스트(Internal Testing) 두 트랙을 동시에 쓰고 있었다. 처음엔 비공개 테스트에만 새 AAB(targetSdk 36, versionCode 2)를 올렸다.

비공개 테스트: 업로드 → 검토 제출 → 자동 사전 검사(약 14분) → 정식 심사 → 출시됨

출시까지 정상적으로 끝났다. 그런데 정책 상태 페이지를 다시 보니 경고가 그대로 남아 있었다. 원인은 내부 테스트 트랙이 여전히 구버전(targetSdk 35, versionCode 1)에 머물러 있었기 때문이다. 정책 준수 판정은 내가 올린 트랙 하나가 아니라, 계정에 활성화된 모든 트랙을 기준으로 보고 있었다. 내부 테스트에도 같은 AAB를 올리고 나서야(내부 테스트는 정식 심사 없이 거의 즉시 반영된다) 상태가 움직였다.

두 트랙을 모두 맞추자 정책 상태 메시지가 단계적으로 바뀌었다.

이전: "규정을 준수하지 않는 가장 높은 대상 API 수준은 Android 15입니다."
      + 해결: 1. API 36 타겟팅  2. 프로덕션에 게시

이후: 위 문장 사라짐
      + 해결: "2. 앱의 새 버전을 프로덕션에 게시합니다" 만 남음

즉 targetSdk 자체는 인식됐고(1번 완료), 남은 조건은 프로덕션 트랙에 API 36 버전이 실제로 떠 있어야 한다는 것이다. 이 앱은 아직 프로덕션에 아무 버전도 올린 적 없는 신규 개발자 계정이라, 비공개 테스트에서 테스터 12명이 14일 이상 유지돼야 프로덕션 게시 권한이 열린다. 그 조건을 채운 뒤 지금 만들어 둔 1.0.1(targetSdk 36) 빌드를 프로덕션으로 승격하는 순간 완전히 해소될 것으로 보고 있다.

이 대목은 아직 진행 중이라 “완전히 끝났다”고 단언하진 못한다. 다만 트랙 하나만 올려선 경고가 사라지지 않는다는 것, 그리고 최종 해소에는 프로덕션 게시가 필요하다는 것까지는 직접 확인했다

정리

정리하면, Android 16 타겟 SDK 대응에서 코드 변경은 시작일 뿐이고 게시가 절반이다. targetSdk 36으로 올리는 것 자체는 build.gradle 두 줄과 predictive back 매니페스트 한 줄, 그리고 edge-to-edge 확인으로 끝난다. 어려운 건 그다음, 쓰고 있는 트랙을 빠짐없이 맞추는 일이다

바로 적용할 수 있는 판단 기준을 남긴다. 정책 경고가 안 사라지면 코드부터 다시 보지 말고, Play Console에서 활성화된 트랙 목록을 먼저 펼쳐라. 비공개·내부·공개 테스트 중 하나라도 구버전 AAB가 남아 있으면 그게 원인이다. 그리고 최종 “해소됨” 판정은 프로덕션 트랙에 새 targetSdk 버전이 실제로 게시돼야 떨어진다. 프로덕션 이력이 없는 신규 앱이라면 게시 권한 조건(테스터 수·기간)을 먼저 채워야 한다는 점까지 계산에 넣어야 한다

참고 자료