[Android Compose] 골치덩어리 EdgeToEdge를 잘 써보자.

2025. 9. 23. 22:28·Android/Compose

시작은 불편함에서..

Android 15부터 Edge-to-Edge가 기본으로 적용되면서, 이제 앱의 콘텐츠가 시스템 바 아래까지 확장되는 것이 표준이 되었어요. 덕분에 개발자들은 더 넓은 화면을 자유롭게 사용할 수 있게 되었습니다. 하지만 동시에 디자인에 맞춰 상태 바(Status Bar)의 색상을 화면과 자연스럽게 연결해야 하는 과제가 생겼어요😅

저도 이 작업을 진행하면서 고민이 많았어요. 이전에는 systemuicontroller 같은 라이브러리를 사용하면 간단히 해결됐지만 Android 16(Tiramisu)부터 Deprecated 되었고, 더 근본적인 문제가 있었어요.

LaunchedEffect나 DisposableEffect 안에서 시스템 바 색상을 변경하는 코드를 호출해야 했는데, 이 방식은 스크린의 컴포저블이 그려지는 시점과 시스템 바의 색상이 변경되는 시점이 완벽하게 일치하지 않았어요. 그래서 화면이 전환될 때마다 미세하게 혹은 확실하게 딜레이 되는 문제가 있었어요.

Before: 스크린 컴포넌트와 상태 바 색상이 동기화되지 않는 모습

어떻게 하면 팀원들이 이 기능을 더 쉽게 사용하고, 컴포저블이 그려지는 순간과 색상 변경을 완벽하게 동기화할 수 있을까 고민했어요. UX를 해치는 문제이기 때문이에요.

그러다 정말 간단하게 "그냥 그 영역에 직접 색칠하면 되잖아?" 라고 생각했습니다.

어떻게 할건데

LaunchedEffect는 컴포저블이 Composition을 통해 UI 구조를 만들고, 화면에 그려진(Draw) 이후에 실행되는 Side Effect입니다.

즉, 화면의 배경색이 먼저 그려지고, 그 직후에 LaunchedEffect 블록이 실행되어 시스템 바의 색상을 변경하는 코드가 동작하게 되는데요, 이 차이가 바로 UX저하의 원인이에요. 제가 원하는건 완벽하게 동기화가 되었으면 했습니다.

이 문제를 가장 Compose스럽게 푸는 방법은 Modifier를 활용하는 것이라고 생각했어요. 개발자가 복잡한 생명주기를 신경 쓰지 않고, .background()처럼 간단하게 사용하도록 하고자 했습니다.

제가 목표로 한 사용 방식은 아주 간단해요.

fun Modifier.statusBarColor(backgroundColor: Color): Modifier

이렇게 확장 함수를 만들면, 사용할 때는 modifier.statusBarColor(Color.Black)처럼 한 줄만 추가하면 됩니다. 아주 쉽지요?

이제 이 함수의 내부를 하나씩 채워볼게요. 함수는 크게 세 가지 일을 해야 해요.

  1. 지정한 배경색의 밝기에 따라 상태 바 아이콘(시간, 배터리 등)의 색상을 결정해요. (밝은 배경에 흰 아이콘이 보이면 안 되니까요.)
  2. 현재 디바이스의 상태 바 높이를 가져와요.
  3. 그 높이만큼, 지정한 색상으로 컴포저블의 상단 영역을 직접 그려요.

그래서 하나씩 해봤습니다.

fun Modifier.statusBarColor(backgroundColor: Color): Modifier = composed {
    // 지정한 색상에 따라 아이콘을 전환한다.
    // 현재 시스템 바의 높이를 가져온다.
    // 높이만큼 지정한 색상으로 그린다.
}

먼저 필요한 값들을 가져와야 해요. 아이콘 색상 제어를 위한 Activity 정보, 배경색의 밝기(Luminance) 값, 그리고 상태 바의 높이가 필요해요.

fun Modifier.statusBarColor(backgroundColor: Color): Modifier = composed {
    val activity = LocalActivity.current
    // 색상의 휘도를 계산해 0.5f보다 크면(밝으면) 아이콘을 어둡게 설정해요.
    val isDarkIcons = backgroundColor.luminance() > 0.5f
    // WindowInsets으로 상태 바의 높이를 가져와요.
    val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()

    // ...
}

이제 상태 바 아이콘의 색상을 제어해야 해요. 이 작업은 WindowCompat.getInsetsController를 통해 할 수 있는데, 이녀석도 Side Effect이므로 LaunchedEffect 안에서 처리했어요. backgroundColor가 바뀔 때마다 아이콘 색상도 바뀌어야 하니, isDarkIcons를 key로 넣어주었어요.

fun Modifier.statusBarColor(backgroundColor: Color): Modifier = composed {
    // ... 이전 코드
    val activity = LocalActivity.current
    val isDarkIcons = backgroundColor.luminance() > 0.5f

    LaunchedEffect(activity, isDarkIcons) {
        val window = activity?.window ?: return@LaunchedEffect
        val controller = WindowCompat.getInsetsController(window, window.decorView)
        controller.isAppearanceLightStatusBars = isDarkIcons
    }

    // ...
}

마지막으로 가장 중요한 배경색을 그리는 부분이에요. Modifier.drawBehind를 사용하면 컴포저블이 그려지기 전에 드로잉 코드를 실행할 수 있어요. 이 코드는 컴포저블의 드로잉 단계(Draw Phase)에 포함되기 때문에, LaunchedEffect와 달리 시간 차 없이 함께 그려지게 됩니다 :)

fun Modifier.statusBarColor(backgroundColor: Color): Modifier = composed {
    val activity = LocalActivity.current
    val isDarkIcons = backgroundColor.luminance() > 0.5f
    val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()

    LaunchedEffect(activity, isDarkIcons) {
        val window = activity?.window ?: return@LaunchedEffect
        val controller = WindowCompat.getInsetsController(window, window.decorView)
        controller.isAppearanceLightStatusBars = isDarkIcons
    }

    this.drawBehind {
        drawRect(
            color = backgroundColor,
            topLeft = Offset.Zero,
            size = Size(size.width, statusBarHeight.toPx())
        )
    }
}

이렇게 간단한 코드로 깜빡임 없는 상태 바 색상 변경 기능이 완성되었어요.

After: 스크린 컴포넌트와 상태 바 색상이 완벽하게 동기화되는 모습

그러나

하지만 이 확장함수를 사용할 때는 선언 순서가 중요해요.

Column(
    modifier = Modifier
        .statusBarColor(Color.Black) // (1) statusBarColor로 그리고
        .fillMaxSize()
        .background(Color.White) // (2) 그 위에 배경을 덮은뒤
                .padding(paddingValues) // (3) 시스템 패딩을 적용
) {
    // Composable 함수들..
}

만약 위 코드처럼 statusBarColor를 먼저 호출하고 그 뒤에 background를 호출하면, .background(Color.White)가 .statusBarColor(Color.Black)이 그린 영역을 다시 덮어버려서 상태 바 색상이 적용되지 않아요. Modifier는 선언된 순서대로 적용된다는 점을 기억해야 해요.

"그럼 패딩까지 확장 함수에 넣으면 안 되나요?????"라고 생각할 수도 있어요.

하지만 Edge-to-Edge 디자인은 상태 바 영역 아래로 콘텐츠가 이어지는 등 활용 가능성이 다양하다고 생각했어요. 그래서 패딩 값까지 이 함수가 제어하는 것은 단일 책임 원칙에 어긋나고, 유연성을 해친다고 판단해 적용하지 않았습니다.

Modifier.drawBehind를 활용해 컴포저블의 드로잉 단계에 직접 관여하는 방식으로 상태 바 색상 동기화 문제를 해결한 경험을 적어봤습니다.

이 방법으로 Deprecated 된 라이브러리를 대체하고, LaunchedEffect의 타이밍 이슈를 해결해 더 나은 사용자 경험을 제공할 수 있었어요. 무엇보다 팀원들이 복잡한 구현을 신경 쓸 필요 없이 modifier.statusBarColor(color) 한 줄로 기능을 사용할 수 있게 된 점이 가장 큰 성과라고 생각해요.

긴 글 읽어 주셔서 감사합니다😊

'Android > Compose' 카테고리의 다른 글

[Android Compose] AboutLibraries로 오픈소스 라이선스 화면을 만들기  (0) 2025.09.23
[Android Compose] @Composable 종속성을 StateFlow로 바꿔보자  (0) 2025.08.09
[Android Compose] flowWithLifecycle은 언제 쓰면 좋을까?  (0) 2025.08.08
[Android Compose] Effect Handlers 딥다이브  (0) 2025.06.20
[Android Compose] 상태 읽기 지연(Defer State Reads)으로 리컴포지션 최적화하기  (0) 2025.06.18
'Android/Compose' 카테고리의 다른 글
  • [Android Compose] AboutLibraries로 오픈소스 라이선스 화면을 만들기
  • [Android Compose] @Composable 종속성을 StateFlow로 바꿔보자
  • [Android Compose] flowWithLifecycle은 언제 쓰면 좋을까?
  • [Android Compose] Effect Handlers 딥다이브
한민돌
한민돌
Android 개발자가 되기까지.
  • 한민돌
    미래 가젯 연구소
    한민돌
  • 전체
    오늘
    어제
    • 분류 전체보기 (20) N
      • Android (4)
        • Compose (10)
        • Jetpack (2)
      • Kotlin (2)
        • Kotlin In Action (0)
      • 외부 활동 (2) N
        • 우아한테크코스 8기 (2) N
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • GitHub
  • 인기 글

  • 태그

    OssLicensePlugin
    backing property
    Google Recommend Architecture
    Multi-module
    derivedstateof
    producestate
    compose
    rememberupdatedstate
    LaunchedEffect
    jetpack
    Baseline Profile
    API35
    AboutLibraries
    우테코
    custom plugin
    build-logic
    runcatching
    Android
    coroutine
    sideeffect
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
한민돌
[Android Compose] 골치덩어리 EdgeToEdge를 잘 써보자.
상단으로

티스토리툴바