본문 바로가기

Android/Android Version

[Android] Android 15 (SDK 35) 버전 대응하기 - Compose UI

728x90

Android 15 버전인 SDK 35 버전은 아직 베타버전이지만, 개발자 톡방을 보면 종종 SDK 35를 대응했다. 대응하는데 어떻게 해야 하냐라는 식의 내용을 종종 확인할 수 있었다.

 

이 사람들 정말 빠르게 버전을 대응하는구나. 라고 생각을 하면서도, 나도 알고 있어야 하는 내용이다 싶어서 시간이 될 때 하나씩 대응을 해보려고 한다.

 

이번 게시글에서는 안드로이드 개발자 페이지의 내용을 전반적으로 살펴보고,

그 중, 필자가 적용한 UI에 관련된 부분을 조금 더 디테일하게 확인해보고자 한다.


처음으로,

모든 앱의 변경사항 문서에 가서 확인해 보자.

모든 앱에 관련하여 수정된 기능을 보는데, 딱히 실제로 컨트롤해야 하는 부분은 많이 보이지 않고, minSDK version에 관련한 내용이 눈에 들어왔다.

Android 15 builds on the the changes that were made in Android 14 and extends this security further. In Android 15, apps with a targetSdkVersion lower than 24 can't be installed. Requiring apps to meet modern API levels helps to ensure better security and privacy.
Malware often targets lower API levels in order to bypass security and privacy protections that have been introduced in higher Android versions. For example, some malware apps use a targetSdkVersion of 22 to avoid being subjected to the runtime permission model introduced in 2015 by Android 6.0 Marshmallow (API level 23). This Android 15 change makes it harder for malware to avoid security and privacy improvements. Attempting to install an app targeting a lower API level results in an installation failure, with a message like the following one appearing in Logcat:

INSTALL_FAILED_DEPRECATED_SDK_VERSION: App package must target at least SDK version 24, but found 7

On devices upgrading to Android 15, any apps with a targetSdkVersion lower than 24 remain installed.

 

대충 내용은 minSDK 버전을 23에서 24로 올려야 한다는 내용이다.

여기서 SDK 23는 마시멜로우(Mashmallow) 버전이고, SDK 24는 누가(Nougat)이다.

 

안드로이드 핸드폰을 사용해 봤으면 당연히 들어봤을 법한 버전들이고, 당연하다시피 아주 오래된 버전이다.

SDK 24는 16년도에 정식적으로 공개된 버전으로 약 10년 전 버전이다.

해당 SDK를 지원해 주는 기기는 갤럭시 시리즈에서는 S8 관련된 시리즈, LG 핸드폰 등 지금은 보기 힘든 기기들만 제공을 하지만, 패드류에서는 아직까지 사용하는 경우가 있을 수 있으므로, 실무에서 버전을 올릴 때는 Firebase로 활성화 기기의 OS 버전 및 모델 명을 확인해 보길 바란다.

 

다시 돌아와서,

굉장히 오래된 버전이지만 아무튼 minSDK 버전이 23에서 24로 올려주어야 한다는 점을 제외하곤, 공통적인 부분에서 필요한 경우가 아니라면 별도로 건들지 않아도 될 것으로 판단된다.

 

다음으로,

Android 15를 타겟팅 하는 앱에 대한 문서를 확인해 보자.

 

첫 번째로 보이는

데이터 동기화 포그라운드 서비스 제한 시간 동작

 

해당 내용은 dataSync 서비스가 24시간 중 6시간 동안만 실행되도록 허용한다는 것인데, 해당 서비스는 이름에서도 알 수 있다시피 데이터의 업로드/다운로드와 같이 데이터를 컨트롤할 때 사용하는 서비스이다.

뭔가 6시간 동안 작업이 가능하다고 나와있는데, 하단의 참고 부분을 보면 "사용자가 앱을 포그라운드로 가져오면 타이머가 재설정되고 앱을 사용할 수 있는 시간이 6시간으로 늘어납니다"라는 부분을 볼 수 있다.

포그라운드 서비스를 실행했는데, 다시 포그라운드로 앱을 가져오는 게 무슨 말이지? 싶을 수 있지만,

필자가 이해한 바로는 타이머가 재설정되는 케이스는 "사용자가 앱과 상호작용을 했을 때"로 이해하였다.

 

서비스를 실행시키고 6시간 동안만 동작을 하되, 사용자가 해당 앱과 계속해서 상호작용을 하고 있으면 해당 시간은 상관없게 되는 것이다.

즉, 앱과 상호작용을 하고 있지 않을 때, 불필요한 리소스 낭비가 발생할 수 있으니 이를 제한하겠다는 말이 된다.

 

해당 동작 제한과 연결돼서 나오는 mediaProcessing, broadcast receiver에 대한 설명은 읽어보면 쉽게 이해가 갈 것이니 넘어가도록 하겠다.

 

관련 내용 마지막에 나오는

앱이 SYSTEM_ALERT_WINDOW 권한을 보유하는 동안 포그라운드 서비스 시작에... 

 

관련된 부분은 첫 번째 내용에서 포그라운드 서비스가 어떻게 동작 없이 6시간 동안 지속될 수 있는지에 대한 이해를 도와줄뿐더러, 어떻게 변경되는지 잘 나와있다.

지금까지는 SYSTEM_ALERT_WINDOW 권한을 보유한 경우, 앱이 화면에 보이지 않더라도(백그라운드 상태) 포그라운드 서비스를 이용하여 작업 처리가 가능했던 반면,

Android 15(SDK 35)부터는 권한뿐 아니라 별도 Overlay 되는 창이 존재해야 한다고 한다.

오버레이 되는 창이란, 앱 자체는 보이지 않더라도 해당 서비스가 동작하고 있음을 알려주는 UI를 사용자가 확인할 수 있어야 한다는 부분이다.

 

해당 부분은 별도로 확인해 보긴 해야겠지만, 설명만 봐서는 비교적 간단(?)하게 UI를 그릴 수 있다고 나온다.

 

첫 번째 문단만 읽었을 땐 뭐 별거 없구나 싶었는데, 마지막 Overlay 되는 UI가 필요하다는 부분에서 관련 기능을 사용하는 경우 생각보다 좀 확인할 것들이 많겠다.라는 생각이 든다.

 

다음에 보이는 부분은 

앱이 방해 금지 모드의 전역 상태를 수정할 수 있는 시점 변경

 

해당 부분은 통칭 DND(Do Not Disturb)라고 불리는 기능에 대한 변경 사항을 나타낸다.

 

Android 15부터는 앱이 더 이상 기기의 전체적인 DND 상태나 정책을 직접 변경할 수 없다고 하며,

AutomaticZenRule이라는 것을 필수적으로 사용해야 한다고 한다.

 

방해금지 모드에 대해서는 한 번도 컨트롤해 본 적이 없기 때문에 자세한 사용 방법은 알 수 없지만,

이전 PandingIntent를 수정했던 것처럼 기존에 사용하고 있던 관련 함수들을 찾아 일부 수정을 해야 하는 변경사항으로 보인다.

 

OpenJDK API 변경 사항

 

필자는 이름만 보고 "아.. 변경할 것 많겠는데..?" 싶었지만, 내용을 보면 생각보다 수정할 부분이 많지 않아 보인다.

 

내용을 확인해 보면 Android 15부터 변경된 사항들과 kotlin-stdlib와 Java 메서드 간 충돌이 발생하여 기존에 사용하던 몇몇 메서드들을 수정해야 한다는 것이다.

String.format(...)
Formatter.format(...)
Arrays.asList.(...).toArray(...)
Random.ints()
Randomw.nextInt()
...

 

이것들 외에 추가적으로 변경되어야 할 부분이 있지만, 컴파일 자체에서 에러가 발생하므로 컴파일 에러가 발생하는 부분을 찾아서 버전에 맞춰 변경해 주면 되는 단순 노가다 작업들이 되겠다.

 

보안 관련된 사항에서는 TLS 버전 1.0과 1.1 버전 사용이 제한된다는 것이 있으며, TLS 1.1 버전은 2006년도에 공개된 버전으로 현재 1.3 버전(2018)까지 나와있다.

 

이것들 외에도 몇 가지가 더 있는데, 내용만 한번 확인해 보고 해당 사항이 있으면 일부 수정을 해주면 된다.

내용 자체가 깊게 들어가야 하지만 많은 부분 적용이 안될 것 같아서 넘어가도록 하겠다.

 

Android 15를 타겟팅하는 앱 외에도 3가지 내용이 더 있는데 해당 내용들은 상당히 짧게 서술되어 있으므로, 서술된 내용을 그대로 가져오는 것은 의미가 없어 보여 넘어가도록 하겠다.


그렇다면 이제,

UI에 관련된 부분을 확인해 보도록 하자.

 

여기서 가장 중요한 부분은 다음과 같다.

Android 15(SDK 35)를 타게팅하는 앱은 edge-to-edge기본적으로 사용된다.

 

edge-to-edge 란 아래의 두장의 이미지를 보면 한 번에 이해할 수 있을 것이다.

기존 UI
edge to edge 적용 UI

 

이와 같이 Status Bar 영역과 Navigation Bar 영역까지 Contents가 포함된다는 것이다.

즉, System Bar 영역까지 UI가 보이게 되고, 해당 영역에 위치하던 아이콘들은 overlay 돼서 보이게 된다.

 

이것들을 실제 필자의 앱을 통해 비교해 보도록 하자.

 

간단하게 기존의 앱에 SDK 버전만 35로 올린 다음, 34, 35 버전의 기기에 각각 빌드하면 위와 같이 나오게 된다.

기존에는 systemBar 영역에는 별 다른 작업을 하지 않아도 알아서 해당 영역은 차지가 되어있고, 필자가 색상을 넣어뒀는데 그것이 그대로 나오는 것을 볼 수 있다.

Android 15 기기에서는 systemBar 영역이 완전히 무시된 체 아이콘이 오버레이 되고 있음을 볼 수 있다.

당연하다시피, systemBar 영역의 이벤트가 우선시되기 때문에 오른편의 SDK 35 버전의 경우 뒤로 가기 버튼이 클릭되지 않는다.

 

이렇게 오른편 UI처럼 나오게 되는 것이 edge-to-edge 기능이 자동으로 활성화되어있는 것이다.

물론 이 자동으로 활성화된 부분을 제거할 수 있는데,

<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>

 

res/values/themes.xml 파일에서 사용 중인 테마에 해당 속성을 추가해 주면 된다.

하지만, 해당 속성을 추가하는 것은 임시적인 해결책일 뿐이고, 추후에 해당 속성을 deprecated 처리하고 비활성화할 계획이라고 하니 되도록이면 해당 속성을 추가하지 말고 직접 개선하는 것을 추천한다.

이에 관련된 글은 해당 포스팅을 확인하였다.

 

우선 해당 부분은 수정해야 한다는 것을 알았으니, 다시 돌아와서 문서를 이어서 확인하도록 하자.

 

Navigation Bar, Status Bar는 기본적으로 투명한 색을 보이게 되며, 3-button Navigation은 80%의 투명도를 갖게 된다고 나와있다.

뿐 아니라, 기존에 사용하던 systemUI에 대한 색상 지정 함수가 deprecated 되었으며 적용되지 않는다고 나와있다.

 

그렇기 때문에 위의 실제 이미지에서 볼 수 있듯이, status bar의 검은색, navigation bar의 노란색이 적용이 안되었던 것이다.

 

이어서 문서를 확인해 보면,

기존 edge-to-edge 기능을 사용하고 있던 앱의 경우, 영향을 받지 않을 것이지만 혹시 모르니 확인해 달라는 내용이 있으며,

edge-to-edge 기능을 사용하고 있지 않더라도, Material 3의 일부 요소를 사용하고 있다면 해당 부분을 자동으로 처리하므로 영향을 받지 않을 가능성이 크다고 한다.

즉, 그냥 UI는 전부 다 확인해 보고 영향 가는 부분이 있으면 확인해서 수정해라.라는 말인 것 같다.

 

이처럼 deprecated 된 함수들도 많은데, 보면 알 수 있다시피 systemBar 관련되어 컨트롤 하는 함수들이 deprecated된 것을 확인할 수 있다.

 

그리고 다음에는 영향이 가는 필드를 말해주는데, 결국 화면 크기를 계산하는 부분에서 systemBar 영역이 제거되었으니 계산 결과가 달라질 것이다.라는 내용이다.

 

여기서 되게 뜬금없지만,

필자는 "Displazy.getMetrics()는 API 수준 33부터 이미 이렇게 작동했습니다."라는 부분이 어이가 없어서 웃음이 나왔다.

영향 없으면 안 쓰면 되는 거 아닌가? 싶었지만, "어? 이건 왜 영향 없어?"라고 파고들 사람들이 있기 때문에 작성해두지 않았을까.. 싶다.


아무튼 여기까지 보면 대충 다 훑어본 것 같으니 실제로 코드를 변경하여 SDK 35를 대응하도록 하자.

 

문서의 내용을 정독했으면 알 수 있다시피, 다음과 같은 내용이 있다.

For custom composables, apply the insets manually as padding. If your content is within a Scaffold, you can consume insets using the Scaffold padding values. Otherwise, apply padding using one of the WindowInsets.

 

아주 친절하게 나와있다.

Scaffold를 사용하는 경우, Scaffold 내부의 패딩 값을 사용하면 되고 그렇지 않으면 WindowInsts 중 하나를 적용하면 된다고 한다.

 

그렇다면 아주 간단하게 수정이 되지 않을까?라는 생각으로, 필자의 프로젝트의 BlogExampleActivity의 UI에 가볍게 Scaffold로 감싸줘서 확인해 보았다.

Scaffold { paddingValues ->
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues)
            .background(color = Color.White)
    ) {
        BlogExampleScreen(
            launcher = launcher,
            blogExampleViewModel = blogExampleViewModel
        )

        Toast(stream = blogExampleViewModel.toast)
    }
}

 

정말 간단하게 Scaffold로 선언하고 내부 paddingValues를 사용하여 Padding을 추가해 주었다.

 

그리고 빌드를 해보면, 정말 간단하게 해결이 된 것 같다.

Box에 Padding을 추가했으므로 Padding 영역에는 색상이 들어가지 않기 때문에 systemBar 영역에 색상이 들어가지 않는다.

따라서 이 또한 간단하게 containerColor를 추가해 주도록 하자.

Scaffold(
    containerColor = Color.LightGray
) { ... }

 

이렇게 추가해 주면,

 

이렇게 설정되게 된다.

이렇게 설정하고 보니, Scaffold를 사용하지 않고 단순히 Box로 선언하고 WindowInsets를 사용하여 padding을 주면 되지 않을까? 란 생각이 들었다.

 

그래서 WindowInsets를 확인해 보니 이와 같이 사용할 수 있는 값이 나왔다.

 

status, navigation, system. ime 등등 상당히 많은 값을 사용할 수 있는데, 간단하게 systemBar를 사용하면 되지 않을까 생각했다.

 

단순하게 padding에 값을 넣으니 타입이 맞지 않는다고 해서 확인해 보니,

 

해당 값은 WindowInsets 값이었다. 너무 날로 먹으려고 생각해서 그런지 타입조차 확인을 안 했었다.

그런데 당연히 내장 함수로 해당 값을 PaddingValues로 변경하는 함수가 있을 것이라고 생각하고 .을 찍어보니, 너무나도 다행히도 변환하는 함수를 제공해 주었다.

Box(
    modifier = Modifier
        .fillMaxSize()
        .padding(WindowInsets.systemBars.asPaddingValues())
        .background(color = Color.White)
) {...}

 

이와 같이 선언하고 확인을 해보면, 맨 처음 Scaffold만 사용했을 때처럼 systemBar 영역이 흰색으로 되어있다.

여기서, background의 color 값 때문에 그런 게 아니냐? 할 수 있지만, 이를 Black으로 변경하면 다음과 같이 나온다.

 

 

당연하다시피, padding값을 주었기 때문에 systemBar 영역은 background Color가 들어가지 않는다.

 

하지만 해당 내용은 Modifier의 적용되는 방식에 대해 알고 있다면 쉽게 해결 가능하다.

많이들 알고 있겠지만, Modifier의 속성 값의 적용되는 방식은 체이닝 방식을 사용하게 되는데 이는 순서에 따라 속성 값이 다르게 적용된다는 점을 인지하고 있어야 한다.

 

간단하게 예로 들자면,

Box(
    Modifier
        .border(1.dp, Color.Red)
        .padding(10.dp)
        .size(40.dp)
        .background(Color.Green)
)

Box(
    Modifier
        .padding(10.dp)
        .border(1.dp, Color.Red)
        .size(40.dp)
        .background(Color.Green)
)

 

이 두 가지의 Box는 선언한 것들은 모두 동일하지만 border와 padding의 선언 순서가 서로 다르다.

이 차이로 인해서

 

이와 같이 다른 UI가 그려지게 되는 것이다.

 

이것을 왜 설명했는가?

당연히 해당 체이닝을 사용해서 위의 systembar 영역에 색상을 줄 수 있기 때문이다.

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(color = Color.Yellow)
        .padding(WindowInsets.systemBars.asPaddingValues())
        .background(color = Color.Black)
) {...}

 

이와 같이 선언하게 되면,

 

이렇게 색상이 들어가게 된다.

체이닝으로 modifier 속성을 적용하면서, padding이 발생하기 전에 노란색을 도화지에 칠하게 된다. 그다음에 padding을 추가하는데 이는 노란색 도화지 위에 padding이 추가된 영역을 만들고, 그 위를 검은색으로 칠하겠다는 의미가 된다.

 

이와 같은 방식으로 이런 UI도 그릴 수 있다.

 

코드는 다음과 같다.

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(color = Color.Yellow)
        .padding(WindowInsets.statusBars.asPaddingValues())
        .background(color = Color.Blue)
        .padding(WindowInsets.navigationBars.asPaddingValues())
        .background(color = Color.Black)
) {...}

 

이런 식으로 색상을 추가할 수 있다.

 

이처럼 systemBar에 대한 padding을 추가해서 작업을 해봤는데,

이와 같은 케이스는 별도로 Scaffold를 사용하고 있지 않거나, Scaffold를 사용하되 Contents 영역만 사용하여 UI를 그리는 경우이다.

즉,

Scaffold를 사용하면서 TopBar와 같이 별도의 UI 요소를 사용하게 되면, 내부의 padding이 아닌 scaffold 자체의 padding이 필요하게 된다.

 

이게 무슨 말인지는 다음 코드와 UI를 보면 이해할 수 있을 것이다.

Scaffold(
    modifier = Modifier
        .fillMaxSize(),
    topBar = {
        Text(
            modifier = Modifier.height(20.dp),
            text = "Test"
        )
    }
) { paddingValues ->
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.Yellow)
            .padding(paddingValues)
            .background(color = Color.Black)
    ) {
        MainActivityScreen()
    }
}

 

Scaffold를 사용하고 추가적으로 UI 요소 중 topBar를 사용하고 있으며, contents 영역에 UI를 그리고 있다.

여기서는 일부러 TopAppBar Component를 사용하지 않았는데, Material 3의 TopAppBar를 사용하면 자동으로 해당 padding 값이 추가되기 때문에 확인을 위해 사용하지 않았다.

 

위와 같이 코드를 추가하게 되면,

 

이와 같이 Scffold에서 사용할 수 있는 UI Component에서는 동작하지 않음을 확인할 수 있다.

 

따라서 Scaffold에도 적용하기 위해 Scaffold의 Modifier에 padding을 추가하는 형식으로 간단하게 수정이 가능하다.

Scaffold(
    modifier = Modifier
        .fillMaxSize()
        .background(color = Color.LightGray)
        .windowInsetsPadding(WindowInsets.systemBars),
) { _ ->
    MainActivityScreen()
}

 

여기서 조금 다른 부분을 확인할 수 있을 것인데,

.padding을 사용한 것이 아니라 windowInsetsPadding이라는 Modifier 함수를 사용하였다.

필자는 해당 함수가 없는 줄 알고 padding을 사용하고, WindowInsets 값을 PaddingValues로 변경해서 사용했는데, 그대로 사용할 수 있는 함수가 존재했다.

해당 프로젝트는 공부를 위한 프로젝트이기 때문에, 사용할 수 있는 두 가지 방법을 모두 사용하여 padding 값을 추가하도록 하였다.

 

위의 코드와 같이 Scaffold의 Modifier에 추가하게 되면,

 

이와 같이 targetSDK 35가 대응된 UI가 나오게 된다.


이것으로 Android 15를 타겟했을 때의 변경점과 UI 부분에 대해 대응하는 방법을 알아보았다. 

작성하다 보니 생각보다 많이 길어진 감이 없진 않지만, 그래도 최대한 디테일하게 작성하려고 노력했다.

 

필자는 샘플 앱이기 때문에 비교적 간단하게 UI를 대응했지만,

실 서비스 중인 앱에서 Material3를 사용하고 있지 않다면 생각보다 대규모로 변경을 해야 하지 않을까 싶다.

뭐, 필자처럼 Scaffold로 한번 감싸고 그곳에다가 padding을 주는 정도로 해결이 가능하다면 좋겠지만 말이다.

 

이런 UI부분 외에도, 위에 언급했다시피 실 서비스에 적용되는 기능들 중 몇 가지들은 별도로 문서를 확인하면서 수정해야 하는 부분이 존재한다.

하지만 이는 아직까지는 그렇게 급하게 변경하지 않아도 되는 것으로 보이며, 비교적 시간이 많이 걸릴 것으로 예상되는 UI 적인 부분부터 하나씩 차례대로 마이그레이션을 하면 되지 않을까 생각한다.

 

그리고 아주 놀랍게도,

해당 글을 작성하기 위해 찾아보던 도중, 안드로이드 개발자 페이지에서 다음과 같은 문구를 확인하였다.

The Android 16 Beta is now available.
Try it out today and let us know what you think!

 

Android 15에 대한 부분을 적용하려고 하니까 Android 16 Beta 버전에 대한 내용을 확인할 수 있다니..

힘이 쭉 빠지는 기분이기는 했지만, 빨리 발견해서 다행이다(?)라는 생각도 들었다.

 

필자와 같은 기분을 주고 싶지 않아 본 글에서는 해당 문서에 대한 언급은 없었지만,

Android 15 (API 수준 35)를 타겟팅하는 앱에 Android 15에서 전체 화면이 적용되었지만 앱은 R.attr#windowOptOutEdgeToEdgeEnforcement를 true로 설정하여 선택 해제할 수 있습니다. Android 16을 타겟팅하는 앱의 경우 R.attr#windowOptOutEdgeToEdgeEnforcement가 지원 중단되고 사용 중지되며 앱에서 전체 화면을 선택 해제할 수 없습니다.

 

테마에 옵션을 추가하여 edge-to-edge 강제 설정을 해제할 수 있는 옵션이 해당 버전에서는 제거되었다.

물론, Android 16 (SDK36) 버전을 강제하기까지는 몇 년의 시간이 더 남았겠지만, 그래도 미리미리 대응하는 게 좋을 것 같다.

 

해당 게시글에 사용한 예제는 Github에 올려두었다.

https://github.com/HeeGyeong/ComposeSample

 

GitHub - HeeGyeong/ComposeSample: This project provides various examples needed to actually use Jetpack Compose.

This project provides various examples needed to actually use Jetpack Compose. - HeeGyeong/ComposeSample

github.com

728x90