ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ComposeView가 언제 죽는지 아세요? — ViewCompositionStrategy 파헤치기
    Android📱 2026. 7. 10. 20:38

    ComposeView가 언제 죽는지 아세요? — ViewCompositionStrategy 파헤치기

    기존 View 기반 앱에 Compose를 조금씩 얹다 보면 반드시 ComposeView를 만나게 됩니다. XML 레이아웃 한복판에 ComposeView를 하나 놓고 setContent { } 안에서 Composable을 그리는 거죠.

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    대부분은 잘 됩니다. 그런데 Fragment에서 쓰다 보면 어느 순간 이상한 크래시나 상태 꼬임을 만나게 되는데, 그 범인이 바로 오늘의 주인공 ViewCompositionStrategy 입니다.


    컴포지션에도 "생명주기"가 있다

    Compose를 쓰면 화면 뒤에 Composition(컴포지션)이라는 게 생깁니다. setContent로 만든 UI 트리의 상태를 들고 있는 실체죠. 이 컴포지션은 언젠가는 정리(dispose) 되어야 합니다. 안 그러면 메모리 누수가 나니까요.

    그럼 "언제" 정리해야 할까요? 이걸 결정하는 정책이 바로 ViewCompositionStrategy입니다.

    한 줄 요약: ViewCompositionStrategy = ComposeView의 컴포지션을 언제 dispose 할지 정하는 규칙


    기본값은 뭘까?

    ComposeView는 아무것도 설정 안 하면 ViewCompositionStrategy.Default 를 씁니다. 이게 현재 가리키는 실제 전략은:

    DisposeOnDetachedFromWindowOrReleasedFromPool

    공식 문서 표현을 그대로 옮기면:

    "The default disposes the Composition when the underlying ComposeView detaches from the window, unless it is part of a pooling container such as a RecyclerView."

    즉, 뷰가 윈도우에서 detach 될 때 컴포지션을 정리합니다. 단, RecyclerView 같은 pooling 컨테이너 안에 있으면 pool에서 재활용/폐기될 때 정리하고요.

    일반적인 Activity + 단순 View 계층에서는 이 기본값으로 충분합니다.


    문제는 Fragment에서 터진다

    Fragment의 함정은 유명하죠. Fragment 자체의 생명주기Fragment의 View 생명주기가 다릅니다.

    • Fragment가 백스택으로 들어가면 → View는 destroy 되지만 Fragment 인스턴스는 살아있음
    • 다시 돌아오면 → onCreateView다시 호출되어 View가 새로 만들어짐

    그런데 기본 전략은 "윈도우에서 detach 될 때" 정리합니다. Fragment가 백스택에 들어갈 때 뷰가 detach는 되지만, 이 타이밍이 Fragment의 view lifecycle과 딱 맞아떨어지지 않아서 컴포지션이 원치 않는 시점까지 살아있거나, 상태가 꼬이는 문제가 생깁니다.

    그래서 Fragment 안의 ComposeView에는 명시적으로 다른 전략을 지정하는 게 정석입니다.

    class ExampleFragment : Fragment() {
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            val view = inflater.inflate(R.layout.fragment_example, container, false)
            val composeView = view.findViewById<ComposeView>(R.id.compose_view)
            composeView.apply {
                // ✅ 뷰의 LifecycleOwner가 destroy될 때 컴포지션 정리
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                setContent {
                    MaterialTheme {
                        Text("Hello Compose!")
                    }
                }
            }
            return view
        }
    }

    DisposeOnViewTreeLifecycleDestroyed뷰 트리에서 가장 가까운 LifecycleOwner(Fragment의 viewLifecycleOwner)를 자동으로 찾아, 그 생명주기가 destroy될 때 컴포지션을 정리합니다. Fragment의 view lifecycle과 정확히 일치하죠.


    4가지 전략 총정리

    전략 언제 dispose 되나 언제 쓰나
    DisposeOnDetachedFromWindowOrReleasedFromPool ⭐기본값 윈도우에서 detach될 때 (pool이면 폐기될 때) 대부분의 일반적인 경우, RecyclerView
    DisposeOnViewTreeLifecycleDestroyed 뷰 트리의 LifecycleOwner가 destroy될 때 Fragment 안에서 쓸 때
    DisposeOnLifecycleDestroyed(lifecycle) 직접 넘긴 Lifecycle이 destroy될 때 특정 Lifecycle에 명시적으로 묶고 싶을 때
    DisposeOnDetachedFromWindow ⚠️deprecated 윈도우에서 detach될 때 (pooling 미고려) 사용 지양 (기본값으로 대체됨)

    RecyclerView는 어떻게?

    의외로 아무것도 안 해도 됩니다. 기본값인 DisposeOnDetachedFromWindowOrReleasedFromPool이 이미 pooling 상황을 고려해서 설계됐거든요. RecyclerView가 item view를 재활용할 때 컴포지션을 알아서 정리해줍니다.

    굳이 DisposeOnViewTreeLifecycleDestroyed 같은 걸 RecyclerView item에 넣으면 오히려 재활용 최적화를 방해하니 주의하세요.


    결론

    • Compose를 View에 얹으면 컴포지션 정리 타이밍을 신경 써야 한다.
    • 기본값 = DisposeOnDetachedFromWindowOrReleasedFromPool → 일반 View / RecyclerView는 그대로 OK.
    • Fragment 안에서는 반드시 DisposeOnViewTreeLifecycleDestroyed로 바꿔라. (view lifecycle 불일치 문제 예방)
    • DisposeOnDetachedFromWindow는 deprecated, 쓰지 말 것.

    "내 ComposeView는 언제 죽는가?" — 이 질문에 한 번쯤 답해보면, interop 단계에서 만나는 미묘한 버그의 절반은 예방됩니다.


    참고 자료

Designed by Tistory.