이번 주에 갑자기 Android 앱의 UI가 깨진다는 문의가 들어왔다.
┌─────────────────┐ ┌─────────────────┐
│ ▓▓ Status Bar ▓▓│ │ ▓▓ Status Bar ▓▓│
├─────────────────┤ ├─────────────────┤
│ ≡ Header │ │ (빈 공간) │ ← 추가 패딩
│ │ │ ≡ Header │
│ 컨텐츠 │ │ │
│ │ │ 컨텐츠 │
│ │ │ │
│ [ Tab Bar ] │ │ [ Tab Bar ] │
├─────────────────┤ │ (빈 공간) │ ← 추가 패딩
│ ▓▓▓ Nav Bar ▓▓▓ │ ├─────────────────┤
└─────────────────┘ │ ▓▓▓ Nav Bar ▓▓▓ │
└─────────────────┘
앱의 UI가 위에 처럼 위아래로 빈 공간이 추가되어 찌부(?)가 되어 보인다는 것이었다.
앱이랑 프론트에서 따로 추가적인 배포는 없는 상황이었다.
이러한 문제가 발생한 원인을 무엇인지 파악하고 어떻게 해결하면 되는지 방법을 정리하고자 한다.

safe-area-inset-* values are always 0px in webview
env(safe-area-inset-*) 값이 항상 0px 반환 (Chromium 버그)// https://medium.com/androiddevelopers/make-webviews-edge-to-edge-a6ef319adfac
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.ui.unit.Dp
// ...
@Composable
fun MainScreen() {
Box(modifier = Modifier.fillMaxSize()) {
// Retrieve insets as raw pixels
val insets = WindowInsets.safeDrawing
WebViewInCompose(
initialUrl = "file:///android_asset/example.html",
insets = insets
)
}
}
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebViewInCompose(
initialUrl: String,
insets: WindowInsets,
density: Density = LocalDensity.current,
layoutDirection: LayoutDirection = LocalLayoutDirection.current,
myWebViewClient: CustomWebViewClient = remember { CustomWebViewClient(
insets, density, layoutDirection
) }
) {
// Don't apply insets to the container
AndroidView(
factory = { context ->
WebView(context).apply {
webViewClient = myWebViewClient
settings.javaScriptEnabled = true
loadUrl(initialUrl)
}
}, update = { view ->
// Updates webpage when software keyboard expands or collapses.
// If your webpage doesn't have an input field that opens the
// software keyboard, remove this line.
applySafeAreaInsetsToWebView(insets, density, layoutDirection, view)
}
)
}
class CustomWebViewClient(
private val insets: WindowInsets,
private val density: Density,
private val layoutDirection: LayoutDirection
) : WebViewClient(){
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// Inject insets into the webpage once the page has fully loaded
applySafeAreaInsetsToWebView(insets, density, layoutDirection, view)
}
}
private fun applySafeAreaInsetsToWebView(
insets: WindowInsets,
density: Density,
layoutDirection: LayoutDirection,
webView: WebView?){
// Convert raw pixels to density independent pixels
val top = insets.getTop(density).toDp(density).value
val right = insets.getRight(density, layoutDirection).toDp(density).value
val bottom = insets.getBottom(density).toDp(density).value
val left = insets.getLeft(density, layoutDirection).toDp(density).value
val safeAreaJs = """
document.documentElement.style.setProperty('--safe-area-inset-top', '${top}px');
document.documentElement.style.setProperty('--safe-area-inset-right', '${right}px');
document.documentElement.style.setProperty('--safe-area-inset-bottom', '${bottom}px');
document.documentElement.style.setProperty('--safe-area-inset-left', '${left}px');
"""
// Inject the density independent pixels into the CSS variables as CSS pixels
webView?.evaluateJavascript(safeAreaJs, null)
}
private fun Int.toDp(density: Density): Dp = with(density) { this@toDp.toDp() }
env(safe-area-inset-*) 값을 정상적으로 반환WebView 업데이트 후 safe area가 갑자기 적용되면서 UI가 깨지는 이슈 다수 보고, 우리 회사 앱도
env(safe-area-inset-*)를 사용하는 Android WebView UI 영역에 의도하지 않은 패딩 발생
Ionic 팀이 "Chromium WebView Regression(회귀 버그)"로 확인, v141에서 수정

https://github.com/ionic-team/ionic-framework/issues/30654
기존에 정상 동작하던 것이 업데이트로 인해 깨지는 현상을 소프트웨어에서 Regression이라 부름.
이 맥락에서는 env(safe-area-inset-*) = 0px이 기술적으로는 버그였지만, 많은 앱이 그 동작에 의존하고 있었음 → Chromium 141에서 "수정"했더니 기존 앱 UI가 깨짐 → Ionic 팀 입장에서 이걸 regression으로 분류한 것.

https://developer.android.com/about/versions/16/behavior-changes-16?hl=ko
| 상황 | 기존 (Chromium ≤ 140) | 현재 (Chromium > 140) |
|---|---|---|
| env(safe-area-inset-*) | 0px (WebView 버그) | 실제 system bar 높이 |
| 웹에서 해당 값 사용 시 | 0px 이므로 레이아웃 영향 없음 | 의도하지 않은 추가 패딩 발생 |
| 앱에서 브릿지로 주입한 값 | 정상 적용 | 브라우저에서 계산한 값과 충돌 가능 |
즉, 기존에는 env(safe-area-inset-*)가 항상 0px을 반환(Chromium 버그)했기 때문에, 앱에서 JS 브릿지로 주입하지 않는 이상 웹에서 해당 값을 사용하더라도 레이아웃에 영향이 없었지만, Chromium 140 이후 실제 inset 값이 반환되면서 의도하지 않은 추가 패딩이 발생하게 됨.
또한 앱에서 브릿지로 해당 값을 직접 주입하더라도, env(safe-area-inset-*)는 브라우저가 관리하는 글로벌 CSS 환경 변수이므로 주입 타이밍에 따라 브라우저 값에 덮어씌워지거나, 반대로 앱 주입 값이 브라우저 값을 덮어쓸 수 있어 문제가 발생할 수 있음
단기적으로, UI 깨짐을 해결하기 위해선 env(safe-area-inset-*) 사용하는 곳을 0px로 교체하는 식으로(사용하는 부분 제거) 당장을 해결 가능
단, iOS 환경에서 영향을 받지 않으려면 User Agent를 통한 분기 처리 필요
장기적으로는 현재 JS 브릿지를 통해 inset 값을 주입 중이라면 브릿지 주입 제거 + env() 정상 동작 수용(Android WebView도 edgeToEdge로 SystemBar 영역까지 영역 확장)
┌─────────────────┐
│ ▓▓(빈 공간)▓▓▓▓ │ ← safe-area-inset-top (status bar 영역)
│ ≡ Header │
│ │
│ 컨텐츠 │
│ │
│ │
│ [ Tab Bar ] │
│ ▓▓(빈 공간)▓▓▓▓ │ ← safe-area-inset-bottom (nav bar 영역)
└─────────────────┘
Chromium 140 이하 기기에서 JS 브릿지를 통한 Inset 주입 없이, enableEdgeToEdge로 뷰 영역을 확장할 경우, 위 아래 빈 공간이 0px로 잡혀 SystemBar 영역에 Header, Tab Bar등의 컨텐츠가 침범할 수 있다.
env(safe-area-insets-*)을 사용하고 있었음safe-area-insets-* 값들이 WebView 엔진 업데이트로 정상화 되면서 inset 값이 반영되어 추가 패딩이 적용됨.safe-area-insets-* 값이 항상 0px로 들어올 것을 산정하고 WebView UI 개발)
이슈 제보 2020년 6월...
구글이 5년 넘게 일을 안했다가, 일을 해서 수 많은 앱들에게 문제가 발생했다.

v141 release는 2025년 8월 경으로 보이는데 왜 그동안 문제가 없다가 26년 2월에 이런 문제가 발생했을까?
https://issues.chromium.org/issues/457682720

fix 된 버전이 144.0.7514.0 버전에 정상 반영된 것으로 추측된다.
144.0.7514.0 버전에서 수정된건 키보드가 올라올때 관련 bottom-insets 값 관련 이슈라 살짝 다른 문제인듯 하다. 이것보다 더 예전 버전에서 이미 반영된 것으로 추측
https://github.com/react-native-webview/react-native-webview/issues/3828
본문에서 언급한 140 버전 전후에 반영된게 맞는듯 하다.
또한 기기마다 WebView 엔진 버전의 차이가 있을 수 있어, 사내 테스트 기기에선 그 이전에 위 문제가 발생하지 않았던 것으로 추측...
https://issuetracker.google.com/issues/40699457
해당 이슈 트래커 글의 코멘트를 통해 각 버전별 상태를 전부 정리해보면
| 버전 | 상태 |
|---|---|
| ~135 이하 | env() = 0 (시스템바 미지원, display cutout만 부분 지원) |
| 136~138 | 동작 (#34 Darryl의 시스템바 포함 커밋, #36에서 136 확인) |
| 139 | 리그레션 - 다시 0 반환 (#44, #45) |
| 140+ | 수정 백포트 완료 (#63, #64) |
139 버전에 다시 env(safe-area-insets-*) 값이 0으로 출력되는 버그가 재현이 되었다고 한다...
따라서 앱에서 웹뷰 버전에 따라 분기처리하기엔 좀 깔끔하지 않기에, 앱에선 버전에 상관없이 항상 css --safe-area-inset-* 값들을 웹뷰에 주입하고, 웹뷰에선 기존 env(safe-area-inset-*)를 사용하는 부분들을 var(--safe-area-inset-*, env(safe-area-inset-*)) 로 교체하여
Android에선 주입된 --safe-area-inset- 값을 사용, iOS에선 env(safe-area-inset-)를 그대로 사용(--sare-area-inset-*이 정의 되지 않았기에 fallback)하는 방식이 최선이라고 생각한다..!
enableEdgeToEdge()로 시스템바 영역까지 콘텐츠를 확장한 뒤, WindowInsetsCompat으로 시스템바 높이를 가져와 루트 뷰에
setPadding()으로 패딩을 적용한다. 네이티브 뷰는 CSS가 없으므로 네이티브 코드에서 직접 패딩을 처리해야 한다.
마찬가지로 enableEdgeToEdge()로 콘텐츠를 확장하지만, 루트 뷰에 setPadding()은 적용하지 않는다.
대신, WindowInsetsCompat으로 가져온 시스템바 높이를 onPageFinished 시점에 JavaScript를 통해 CSS 커스텀 속성(--safe-area-inset-top 등)으로 웹에 주입한다.
Padding 처리를 웹 CSS에 위임하는 방식으로, 네이티브에서 패딩을 주면 웹의 env(safe-area-inset-*)와 이중 적용되는 문제를 방지한다.
reference)
https://issues.chromium.org/issues/40699457
https://github.com/ionic-team/ionic-framework/issues/30654
https://medium.com/androiddevelopers/make-webviews-edge-to-edge-a6ef319adfac
https://developer.chrome.com/docs/css-ui/edge-to-edge
https://issuetracker.google.com/issues/396827865
https://developer.chrome.com/docs/css-ui/edge-to-edge?hl=ko
https://issues.chromium.org/issues/457682720
https://www.androidpolice.com/android-16-full-screen-edge-to-edge-no-opt-out/
https://chromereleases.googleblog.com/2026/02/chrome-for-android-update.html
https://github.com/react-native-webview/react-native-webview/issues/3828