💡 / 키를 눌러 빠르게 검색하세요!

Compose Navigation - WebView Recomposition 해결?

본문

Compose Navigation에서 WebView 활용 시 Recompistion이 발생한다.

  • A Composable에서 WebView를 그린다.
  • B Composable에서 Other Composable을 그린다.

A에서 B로 navigate 했다가 다시 A로 돌아오는 경우 Recomposition이 발생한다.(당연하게도)

Recomposition을 막을 순 없지만 WebView를 유지할 수 있는 방법은 없을까?


이 글에서 알아볼 내용

  • Compose Navigation에서 WebView를 잘 활용하기 위한 방법은?


현상부터 보자

현상을 파악하기 위해 샘플 코드를 작성하였다.

  • Navigation WEB - WebView
  • Navigation OTHER - Other Screen

이 경우 Other Screen에서 WebView Navigation으로 다시 돌아온다면 어떻게 동작할까? 아래 이미지를 참고하시길

  • OTHER 화면에서 백을 하면 잠시 흰 화면이 노출되어지고, Web load가 일어난다.

before


화면별 코드를 살펴보자

전체 코드는 아래에서 확인할 수 있고 주요 코드는 아래와 같다.

Github - Compose Navigation - WebView


MainScreen

MainScreen에서 Compose Navigation을 활용하여 이동하도록 구현하였다.

WEB에서 OTHER로 넘어갈 때는 saveState를 적용해두었다.

NavHost(
    navController = navController,
    startDestination = "WEB",
) {
    composable(route = "WEB") {
        Box(
            modifier = Modifier
                .fillMaxSize()
        ) {
            WebSampleScreen()

            Button(
                onClick = {
                    navController.navigate("OTHER") {
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        restoreState = true
                    }
                },
            ) { /* 생략 */ }
        }
    }
    composable(route = "OTHER") {
        OtherSampleScreen()
    }
}


WebViewScreen

아직 Compose에서 직접 WebView를 제공하지는 않아서 AndroidView를 이용하여 사용해야 한다.

WebView는 보통 라이브러리를 활용하기보단 직접 구현하는 편이 적합한데 적용 시 요구사항이 다르니 직접 만드는 것이 더 좋은 선택이다. 그래서 Accompanist WebView를 Deprecated 시켜두었지만 그래도 UI가 안 나오는 버그까지는 패치해둔 상태인데, FrameLayout을 한 번 더 감싼 코드까지는 제공해 주고 있다.

대부분의 코드는 생략하고 아래와 같다.

var webView by remember { mutableStateOf<WebView?>(null) }
AndroidView(
    factory = {
            webView = WebView(context).apply {
                this.layoutParams = layoutParams
                settings.run {
                    javaScriptEnabled = true
                    defaultTextEncodingName = "UTF-8"
           }
        }
        webView
    },
    update = {
       // 생략
    },
    onRelease = {
       // 생략
    }
)

OtherScreen

테스트용이니 Text 하나 노출하는 화면을 구성하였다.

Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
        .fillMaxSize()
) {
    Text(
        text = "other screen",
    )
}


문제를 파악해 보자

Navigation의 route - OTHER에서 route - WEB으로 돌아오면 WEB은 Recomposition이 발생한다. 이때 AndroidView를 이용한 WebView 역시 다시 그려진다. 간단하게 로그를 출력하면 아래와 같다.

// 처음 진입 시
AndroidView factory - null
AndroidView update - android.webkit.WebView

// navigate OTHER로 이동하는 경우
AndroidView onRelease - android.webkit.WebView

// 다시 돌아오면
AndroidView factory - null
AndroidView update - android.webkit.WebView

로그로 볼 수 있지만 remember로 들고 있던 WebView는 null로 초기화 되어 있음을 알 수 있다.

WebView 자체에 대한 상태를 저장하지 않기 때문에 당연한 결과이다.


rememberSaveable을 활용할 수 있을까?

답은 그렇지 않다.

Save UI state in Compose

적용 시 즉시 아래와 같은 오류를 볼 수 있다.

The default implementation only supports types which can be stored inside the Bundle.

사실 이게 가능하다면 가장 쉬운 방법이다. 상태 관리를 가장 쉽게 할 수 있으니깐 말이다.


유지할 수 있을까?

유지하기 위해서 어떤 코드를 작성해야 할까?

일반적으로 Navigation에서 상태를 유지하고, 복귀하기 위한 코드는 아래와 같다.

popUpTo(navController.graph.findStartDestination().id) {
    saveState = true
}
// Restore state when reselecting a previously selected item
restoreState = true

뷰 안에서의 상태를 유지해야 하는 경우를 뜻한다. 우리가 개발하는 대부분의 안드로이드는 AAC-ViewModel을 활용하고 있을 것이다. 그래서 이 방식을 굳이 적용하지 않아도 문제는 없지만 scroll 상태 유지를 위해서는 이 코드는 필수로 필요하다.

그럼 웹뷰는 어떻게 유지하는게 좋을까?


가장 쉽게 싱글턴 사용하기

싱글턴에 WebView를 임시 보관하는 것이다. 하지만 이 방법은 Context 메모리 릭을 발생시킨다.

생성한 WebView를 싱글턴으로 저장하는 것이다. 결과적으로 원하는 동작은 한다.

object KeepWebView {

    var webView: WebView? = null
}

그리고 아래와 같이 저장한다.

// Don't

AndroidView(
    factory = {
        val parentLayout = FrameLayout(context).apply {
            val web = KeepWebView.webView ?: WebView(context).apply {}.also {
                KeepWebView.webView = it
            }
        }
    }
)

이론적으론 싱글턴으로 사용하여 유지할 수 있다. 싱글 액티비티를 사용한다면 큰 문제는 없을 수도 있다.(onDestroy에서 clear는 해야 한다.)


Compose에서 제공하는 방법을 활용해 보자.

Compose에선 CompositionLocalProvider 사용해 범위를 지정하여 사용할 수 있는데, 이를 활용해 해결할 수 있다.

CompositionLocalProvider 링크

Navigation을 가진 뷰 상위에 CompositionLocalProvider 적용하여 WebView를 keep 할 수 있도록 구현하는 방법이다.

이렇게 하면 최소한 WebView를 네비게이션 이동할 때마다 다시 생성하는 부분은 막아 줄 수 있다.


적용 코드

WebView 용 CompositionLocalProvider 아래와 같이 작성하였다.

internal object LocalWebOwner {

    private val LocalComposition = staticCompositionLocalOf<WebView?> { null }

    val current: WebView?
        @Composable
        get() = LocalComposition.current

    infix fun provides(registerOwner: WebView?): ProvidedValue<WebView?> =
        LocalComposition provides registerOwner
}

사용할 때는 아래와 같이 사용할 수 있도록 기존 remember 부분의 코드를 변경한다.

val webView = LocalWebOwner.current

AndroidView(
    factory = {
        val parentLayout = FrameLayout(context).apply {
            val web = webView ?: WebView(context).apply {
                this.layoutParams = layoutParams
            }
            addView(web)
        }
    }
)

webView가 결국 null 일 수 있으니 초기화 코드는 동일하게 포함되어야 한다.

이 코드를 그대로 적용하면

The specified child already has a parent. You must call removeView() on the child's parent first.

그래서 기존 View가 release 동작시 remove를 시켜주어야 한다는 점이다.

onRelease = { parentFrame ->
    (parentFrame.children.first() as? WebView)?.let {
        parentFrame.removeView(it)
    }
}

마지막으로 Navigation을 감싸는 Screen 코드를 아래와 같이 수정해 주어야 한다.

  • 위치는 Activity일 수도 있다.
val context = LocalContext.current
CompositionLocalProvider(
    LocalWebOwner provides WebView(context)
) {
    NavHost(){}
}


마무리

이 방법은 WebView는 Navigation Scope 내부에서는 동일한 WebView를 사용하게 됨으로써 최소한 WebView 객체를 처음부터 다시 생성하는 부분은 막을 수 있지만 Web 자체의 리로드를 막는 코드는 아니다.

그래도 WebView가 새로 생성되어 두 배 세배 더 느려지지 않는다면 만족스러운 결과를 볼 수 있을 것이다.

결과적으론 아래와 같은데, 웹 자체는 reload가 호출됨을 알 수 있다.

  • Before before
  • After after


About Taehwan

안드로이드 개발 12년차, 모바일앱 개발자 권태환입니다. 코드와 아키텍처, 그리고 개발 문화에 관심이 많습니다. GDG Korea Android와 DroidKnights에서 개발자 커뮤니티를 함께 만들어가고 있습니다.

Comments

테오 AI powered by Gemini 2.5 Flash
🤖
안녕하세요! 테오입니다. 😊
이 블로그의 모든 포스트를 학습했어요.
Android, Kotlin, 개발 관련 궁금한 점을 물어보세요!