안드로이드 15 대응기: contentView Insets 충돌과 SDK 개선 방법

H.Zoon·2025년 6월 20일
0
post-thumbnail

1. Edge to Edge UI란 무엇인가?

최근 안드로이드 디자인 가이드라인에서는 컨텐츠를 화면의 경계까지 확장하여 몰입감을 높이는 Edge to Edge UI 구현을 적극 권장하고 있습니다.
하지만 실제 프로젝트에 적용해보면 단순히 fitsSystemWindows=false로는 해결되지 않는 다양한 문제가 발생합니다.

이 글에서는 제가 실제로 겪은 문제를 기반으로, Edge to Edge UI를 구현하는 과정에서 마주친 트러블과 그 해결 과정을 공유하고자 합니다.


2. Edge to Edge의 동작 원리와 구성 요소

✔ Edge to Edge UI란?

안드로이드의 Edge to Edge는 시스템 바(Status bar, Navigation bar)를 포함한 전체 화면을 앱 컨텐츠가 차지하게 하는 디자인 방식입니다.
기존의 fitsSystemWindows=true 설정은 시스템 바 아래에 컨텐츠를 배치하지 않도록 처리했지만, 이제는 이 경계를 넘어서 더 자연스럽고 몰입감 있는 UI를 만드는 것이 권장됩니다.

✔ 주요 구성 요소

  • WindowInsets
    시스템 UI(상태바, 네비게이션 바 등)의 영역 정보를 포함한 클래스입니다. 각 View 또는 Compose Modifier에서 이 값을 활용해 패딩/마진 등을 조절합니다.

  • setDecorFitsSystemWindows(false)
    이 설정을 통해 시스템 바와 컨텐츠 간의 기본 마진 적용을 막고, 앱이 직접 Insets 처리를 하도록 만듭니다.

  • ViewCompat.setOnApplyWindowInsetsListener()
    뷰 계층에서 Insets를 수신하고 커스터마이징할 수 있게 도와주는 리스너입니다.

  • Jetpack Compose의 WindowInsets API
    Compose에서도 WindowInsets.systemBarsModifier.systemBarsPadding() 등을 통해 동일한 처리가 가능합니다.


3. SDK에서 발생한 문제 상황

✔ SDK 구조와 동작 방식

저희 SDK는 연동된 앱의 Activity 컨텍스트를 받아, 해당 액티비티의 android.R.id.content 뷰에 커스텀 UI 컴포넌트인 PointHomeSlider를 동적으로 삽입하여 서비스를 노출합니다. SDK 내 삽입 로직은 다음과 같이 구성되어 있습니다:

val parentViewGroup = activity.findViewById<ViewGroup>(android.R.id.content)
val customSlider = CustomSlider(...)

val container = FrameLayout(activity).apply {
    id = R.id.container
    layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
    fitsSystemWindows = true
}

container.addView(customSlider)
parentViewGroup.addView(container)

PointHomeSlider는 바텀시트 형태의 UI로, 부모 뷰의 높이에 비례한 0.98f 비율로 자체 높이를 설정하여 자연스러운 여백을 확보하는 구조입니다.

✔ 문제 발생 배경: 인셋 중복 적용 이슈

이 구조는 안드로이드 14 이하 환경에서는 fitsSystemWindows 속성을 통해 시스템 바 영역을 회피하는 방식으로 안정적으로 동작했습니다.

하지만 Android 15 대응을 위해 연동 앱 측에서 다양한 방식으로 WindowInsets 처리를 시작하면서 문제가 발생했습니다. 특히 다음과 같은 상황이 문제의 원인이 되었습니다:

  • 외부 앱이 android.R.id.content에 직접 인셋을 적용하는 경우
  • SDK는 이미 해당 영역에 fitsSystemWindows 기반 처리를 하고 있음

이로 인해 인셋이 중복 적용되어 컨텐츠 여백이 이중으로 생기고, 바텀시트 위치가 어색하게 올라가거나 내려가는 현상이 발생했습니다.

정상적인 상황 정상적인 상황 문제가 되는 레이아웃 문제가 되는 레이아웃

✔ 연동 앱의 Insets 적용 사례 분석

✅ 정상적으로 동작하는 경우

  1. 루트 뷰(R.id.main)에 인셋을 적용하는 코드:
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
    val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
    insets
}

이 방식은 최상위 컨테이너에 인셋을 적용하므로, 하위 뷰에서는 추가로 인셋 처리를 하지 않아도 되는 구조입니다. SDK 내부의 UI는 이 인셋을 별도로 감지하여 중복 적용하지 않도록 설계할 수 있습니다.

⚠️ 문제를 발생시킨 경우

  1. android.R.id.content에 직접 인셋을 적용하는 코드:
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { v, insets ->
    val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
    insets
}

이 방식은 SDK에서 뷰를 삽입하는 바로 그 지점에 인셋을 적용하는 것으로, SDK 내부에서도 시스템 인셋을 고려해 별도로 패딩을 넣고 있었기 때문에 결과적으로 여백이 중복 적용되는 문제가 발생했습니다.

즉, android.R.id.content 뷰에 인셋이 들어간 상태에서 SDK가 또다시 바텀 마진이나 패딩을 적용하면서 UI가 과도하게 위로 밀리거나, 부자연스러운 여백이 생기는 현상이 발생했습니다.

이 문제를 해결하기 위해 SDK는 parentViewGroup (android.R.id.content)의 현재 padding 값을 검사하여, 외부 앱에서 인셋이 이미 적용된 것으로 판단되면 내부적으로 중복 적용을 하지 않도록 처리했습니다.

✔ 개선 방향 및 로직 원리

중복 인셋 적용 여부를 감지하고, SDK 내부에서 필요한 경우에만 ViewCompat.setOnApplyWindowInsetsListener를 통해 커스텀 컨테이너에 인셋을 재적용하도록 개선했습니다.

  1. 루트 데코 뷰에서 원시 Insets 값을 측정하고,
  2. parentViewGroup의 현재 padding이 이 Insets 이상인지 확인하여 외부에서 이미 처리했는지 판단합니다.
  3. 외부 적용이 감지되지 않으면 SDK가 책임지고 container에 인셋을 수동으로 적용합니다.

이를 통해 다양한 환경에서도 일관된 위치와 여백을 유지할 수 있게 되었고, 다음과 같은 보완 로직을 도입했습니다:

val rawInsets = ViewCompat.getRootWindowInsets(decorView)
    // 시스템 전체의 원시 인셋 값을 가져옵니다 (상태바, 내비게이션 바 등)
    ?.getInsets(WindowInsetsCompat.Type.systemBars())
    // 널이면 기본값으로 Insets.NONE 반환
    ?: Insets.NONE

val topApplied = parentViewGroup.paddingTop >= rawInsets.top
// 상단 패딩이 시스템 인셋 이상인지 확인하여 외부에서 이미 인셋이 적용되었는지 판단
val bottomApplied = parentViewGroup.paddingBottom >= rawInsets.bottom
// 하단 패딩도 동일하게 확인

if (!topApplied || !bottomApplied) {
    // 인셋이 적용되지 않았다면 SDK 내부 컨테이너에 직접 인셋을 적용
    ViewCompat.setOnApplyWindowInsetsListener(container) { v, insets ->
        val sys = insets.getInsets(WindowInsetsCompat.Type.systemBars())
        // 시스템 바 인셋을 패딩으로 설정하여 컨텐츠가 가려지지 않도록 보장
        v.setPadding(sys.left, sys.top, sys.right, sys.bottom)
        WindowInsetsCompat.CONSUMED // 인셋을 소비하여 추가 전달 방지
    }
    ViewCompat.requestApplyInsets(container) // 인셋 적용 요청
}

핵심 원리는 "이미 인셋이 적용된 뷰에는 다시 인셋을 적용하지 않는다"는 조건부 로직으로, 외부 앱과의 충돌을 방지하며 SDK가 독립적으로 Insets를 처리할 수 있게 만든 것입니다.


4. 결론 및 마무리

Edge to Edge UI는 안드로이드 앱의 디자인 완성도를 높이는 중요한 요소입니다.
하지만 자동으로 해결되기를 기대하기보다, 시스템 Insets을 직접 처리할 수 있도록 구조를 이해하고 접근하는 것이 필요합니다.

XML 기반 View에서는 WindowInsetsCompat와 리스너를 적극 활용하고, Compose에서는 제공되는 Modifier를 활용하는 것이 가장 안정적이었습니다.
버전, 테마, 제스처, 제조사 등 다양한 변수에 유의하여 구현하세요.


5. 참고 자료

0개의 댓글