헷갈리는 Compose TextField의 하단 insets 관리하기 (feat. safeDrawing)

SSY·2026년 3월 31일

AndroidFramework

목록 보기
7/8
post-thumbnail

앱에서 하단 입력창(TextField)을 다루다 보면, 어떤 화면은 키보드가 올라와도 정상인데 어떤 화면은 입력창이 키보드 뒤로 가려지는 문제가 반복됐다.

처음에는 화면별로 ime/safeDrawing/고정 padding을 조합해 증상을 줄이는 방식(3.1)으로 대응했다. 그런데 작업 완료 후, 골똘히 생각해보니, 자칫 잘못 적용하면 모든 화면에 navigationBarsPadding(), imePadding()을 적용해야하는 비효율이 발생했다. 즉, "어디서 inset 처리를 끝낼지(consume)"가 중요하다는 것을 알게 됐다.

이 글은 그 시행착오 과정과, 최종적으로 consumeWindowInset까지 적용해 정리한 기준을 함께 기록한 내용이다.


1. 배경지식

IME(WindowInsets.ime)

IME(Input Method Editor)는 소프트 키보드를 포함한 입력 시스템으로, 키패드 자체가 차지하는 하단 화면 공간을 padding으로 제공하는 inset 값이다.

Modifier.windowInsetsPadding(WindowInsets.ime)
[키보드 닫힘]
┌────────────────────┐
│                    │
│                    │
│                    │
│      Content       │
│                    │
│                    │
│                    │
└────────────────────┘
ime.bottom = 0

[키보드 열림]
┌────────────────────┐
│                    │
│      Content       │
│                    │
├────────────────────┤
│                    │
│       IME          │
│                    │
└────────────────────┘
ime.bottom = 키보드 높이

즉, 키보드가 있는지, 없는지에 따라 0에 가까워지거나 커지거나 한다.


이는 하단 시스템 내비게이션 영역(3버튼/제스처 바 포함)을 나타내는 padding값으로, 키보드와 무관하게 존재하는 고정 시스템 영역이다.

Modifier.windowInsetsPadding(WindowInsets.navigationBars)
┌────────────────────┐
│                    │
│                    │
│                    │
│      Content       │
│                    │
│                    │
│                    │
├────────────────────┤
│ Navigation Bars    │
└────────────────────┘

safeDrawing(WindowInsets.safeDrawing)

이는 시스템 UI(상단의 status bar, notch와 하단의 navigation/gesture 영역)에 가려지지 않게 콘텐츠를 배치하기 위한 안전 여백이다.

WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)
[safeDrawing 미적용]
┌────────────────────────────────────┐
│ 상단 시스템 영역 (status/notch)        │  ← 안전하지 않음
├────────────────────────────────────┤
│                                    │
│                                    │
│                                    │
│                                    │
│          App Contents Area         │
│                                    │
│                                    │
│                                    │
│                                    │
├────────────────────────────────────┤
│ 하단 시스템 영역 (nav/gesture)         │  ← 안전하지 않음
└────────────────────────────────────┘

[safeDrawing 적용]
┌────────────────────────────────────┐
│ [피함] 상단 시스템 영역                 │
├────────────────────────────────────┤
│   ┌────────────────────────────┐   │
│   │                            │   │
│   │                            │   │
│   │                            │   │
│   │      App Contents Area     │   │
│   │        (SafeDrawing)       │   │
│   │                            │   │
│   │                            │   │
│   └────────────────────────────┘   │
├────────────────────────────────────┤
│ [피함] 하단 시스템 영역                 │
└────────────────────────────────────┘

Modifier의 insets 조합 함수(only, union, windowInsetsPadding)

ime, navigation, status, safeDrawing 등, 패딩값 이해를 기반으로 이들을 조합해 시스템 padding값 조절이 가능하다. 대표적인 예시 코드는 아래와 같다.

fun Modifier.bottomSafeDrawingAndImePadding(): Modifier = composed {
  // 아래 계산된 insets을 반영
  this.windowInsetsPadding(
    insets = WindowInsets.safeDrawing
       // safeDrawing 전체 중 하단 값만 사용
      .only(WindowInsetsSides.Bottom)
      
      // 앞에 있는 것과 뒤에 있는 인셋을 합칠 때, 더 큰 값을 선택한다. 
      // 즉, 키패드가 올라왔을 떈, ime를 택한다. 반면 없을 땐, bottom을 택한다.
      .union(WindowInsets.ime)
  )
}

숫자 예시를 통해 보여주자면 아래와 같다.

  • 키보드 닫힘:
    • safeDrawing.bottom = 34, ime.bottom = 0
    • 최종 하단 패딩 = 34
  • 키보드 열림:
    • safeDrawing.bottom = 34, ime.bottom = 312
    • 최종 하단 패딩 = 312

더 간단히 말해, 아래와 같이 동작한다.

val finalBottom = max(safeDrawing.bottom, ime.bottom)

inset consume(Modifier.consumeWindowInsets)

지금까지 insets를 계산하는 이야기였다면, 이 단계는 계산한 insets를 누가 최종 소비할지(consume) 정하는 단계다. 같은 insets를 부모와 자식이 동시에 적용하면 패딩이 이중으로 반영될 수 있으므로, 루트에서 적용한 insets는 루트에서 소비 처리해 자식에게 중복 전달되지 않게 해야 한다.

consumeWindowInsets(...)는 "여기서 이 insets를 사용했다"고 하위 트리에 알리는 역할을 한다.

Scaffold(
  contentWindowInsets = WindowInsets.safeDrawing
) { paddingValues ->
  Box(
    modifier = Modifier
      .padding(paddingValues)
      .consumeWindowInsets(paddingValues)
  ) {
    BuddyStockNavHost()
  }
}

중요한 포인트는 아래와 같다.

  • padding(paddingValues): 실제 여백 적용
  • consumeWindowInsets(paddingValues): 같은 여백을 하위에서 다시 적용하지 않도록 소비 표시
[consume 안 한 경우]
Scaffold(safeDrawing)
  -> paddingValues(top=24,bottom=34)
  -> Root에 padding 적용
  -> Child가 safeDrawing 다시 적용
  => top/bottom 이중 패딩 가능

[consume 한 경우]
Scaffold(safeDrawing)
  -> paddingValues(top=24,bottom=34)
  -> Root에 padding 적용 + consume
  -> Child는 이미 소비된 insets 기준으로 배치
  => 단일 적용 유지

정리하면, only/union은 "얼마를 적용할지"를 정하고, consumeWindowInsets는 "어디서 적용을 끝낼지"를 정한다.


2. 문제 정의

여러 화면에서 하단 UI쪽에 TextField를 쓰던 중, 인셋 처리가 화면마다 달랐다. 어떤 곳은 imePadding()만, 어떤 곳은 navigationBarsPadding()만 쓰는 등, 동적 구성이 없었다.

그 결과, 아래의 문제들이 발견됐다

  • insets padding 미 적용 시, 키보드가 닫힌 상태에서 하단 시스템 영역이 하단 TextField를 덮는 문제와 키보드가 올라오면 하단 TextField를 덮음
  • 덮지 않기 위해, Root에서 Scaffold의 innerPadding적용 후, 자식 Composable에 imePadding()을 적용했더니, navigationBarsPadding()도 함께 적용됨

즉, 문제의 본질은 bottom padding값 대응을 '동적'으로 대응하지 못한 것에 있었다. (하단 레이아웃 기준 safeDrawing.bottom union ime 필요)

[safeDrawing.bottom vs navigationsBarPadding]
navigationBarsPadding()은 하단 내비게이션 바 높이만큼만 여백을 주는 방식이다. 반면 safeDrawing의 bottom 값은 내비게이션 바를 포함하면서, 기기별 추가로 필요한 하단 안전영역(예: 제스처 영역 특성)까지 함께 고려하는 방식이다. (위 SafeDrawing 설명 글 이미지 참고) 즉, 두 값이 비슷하게 나오는 경우가 많지만, 실기기 편차까지 고려하면 safeDrawing이 하단 UI 겹침을 더 안정적으로 방지하는 선택이다.

safeDrawing.bottom ≈ navigationBars.bottom + 기기별 추가 안전 여백

  navigationBars ----\
                       >-- safeDrawing.bottom --\
  displayCutout -----/                          \
                                                 >-- union(max) --> final bottom padding							/
  ime (keyboard) ------------------------------/

3. 해결

현재 진행중인 프로젝트는 Single Activity기반, Navigation2가 적용되어있다.

처음에는 문제를 빠르게 막기 위해 화면별 단위 대응(3.1)을 먼저 적용했다. 하지만 이는 현존하는 모든 화면에 inset 적용 후, 추후 생성할 모든 화면에도 동일 적용을 해아흐는 비효율이 존재했다.

방법을 찾아본 결과, 이를 해결하기 위한 효율적 방법은 개별 inset 계산보다 inset 소비 지점(consume) 을 명확히 두는 것이 문제 해결의 핵심이었다.

3.1 하단 인셋 처리 공통 Modifier 확장 함수 추가 및 적용 (초기 대응)

우선 첫 번쨰로 Root의 Scaffold에 safeDrawing에서 상단/좌/우에 대한 inset만 적용할 수 있게 적용했다. 두 번째로 먼저 하단 TextField를 가진 UI에 아래와 같은 동적 padding 확장 함수(bottomSafeDrawingAndImePadding)를 만들었다. 이로써 입력이 있는 모든 화면에 공통 Modifier를 붙여, 위에서 말한 이슈를 제거할 수 있었다.

// Root 적용
Scaffold(
  contentWindowInsets = WindowInsets.safeDrawing.only(
    WindowInsetsSides.Top + WindowInsetsSides.Horizontal
  )
) { innerPadding ->
  BuddyStockNavHost(
    modifier = Modifier.padding(innerPadding)
  )
}

// Child에 적용할 확장함수
fun Modifier.bottomSafeDrawingAndImePadding(): Modifier = composed {
this.windowInsetsPadding(
  insets = WindowInsets.safeDrawing
    .only(WindowInsetsSides.Bottom)
    .union(WindowInsets.ime)
)
}

// Child에 적용
Column(
modifier = Modifier
  .fillMaxSize()
  .bottomSafeDrawingAndImePadding()
) {
// ...
}

3.2 루트 Scaffold 인셋 책임 분리 + consume 적용 (최종 대응)

즉, Root에서 bottom에 대한 책임을 제거하고, Child화면에는 자체적인 bottom 규칙을 사용하도록 처리한 것이다. 이로써 하단에 TextField가 있는 UI (예. 커뮤니티 채팅 입력창)에선 문제가 해결됐으나, 기존에 innerPadding 및 하단 padding이 적용되어 있는 화면에서 bottom navigationsBar가 기존 UI를 덮는 문제가 발생했다.

이를 해결하는 가장 쉬운 방법은 AI에게 모든 화면에게 navigationsBarPadding()을 적용해달라고 주문하면 되었다. 하지만 아까 말했듯, 변경이 너무 많고 앞으로 만들 코드에서도 이를 적용하는 건 비효율적이란 생각이 들었다.

따라서 더 효율적인 방법을 찾아보았고 그 결과, Root의 insets를 consume 하는걸로 효율적인 해결이 되었다.

Scaffold(
  contentWindowInsets = WindowInsets.safeDrawing
) { paddingValues ->
  BuddyStockNavHost(
    modifier = Modifier
      .padding(paddingValues)
      .consumeWindowInsets(paddingValues)
  )
}

위와 같이 적용하면 위 3.1에서 적용했던 bottomSafeDrawingAndImePadding()를 그 어떤 Child화면에서도 쓰지 않으며, navigationBarsPadding(), imePadding()을 그 어떤 화면에서도 쓰지 않고 문제를 해결할 수 있다.


4. 결과 및 요약

처음 적용한 솔루션(3.1) 키보드 가림 이슈는 대부분 줄었으나, 이를 전체 화면에 적용해야하는 문제가 있었고, 이를 consumeWindowInsets로 적용하면서 효율적인 해결이 되었다.

적용 후 변화는 아래와 같다.

  1. 키보드가 닫힌 상태 : 깔끔하게 navigationsBarPadding()만 적용됨
  2. 키보드가 열린 상태 : 깔끔하게 imePadding()만 적용됨 (즉, imePadding + navigationsBarPadding()이 2중적용되지 않음)

이는 루트에서 safeDrawing의 innerPadding을 consume함으로써 하위의 inset은 padding을 중복 적용하지 않게 해줌

정리하면, 하단 입력 UI 문제는 ime(), navigationsBarPadding() 만으로 해결되지 않는 경우가 있다. 그러므로 Single Activity기반, 최상단 Scaffold가 있는 구조에선 SafeDrawing기반 innerPaddingconsumeWindowInsets 하는 방식으로 쉽고 효율적인 해결이 가능하다.

0개의 댓글