brunch

Compose 상태 호이스팅

하루 10분 Compose (8)

by 서준수

Compose에서 Stateful 컴포저블과 Stateless 컴포저블을 명확히 분리하는 것은 중요한 설계 원칙입니다. 이를 구현하는 핵심 방식 중 하나가 상태 호이스팅(State Hoisting)입니다.


상태 호이스팅(State Hoisting)이란 무엇인가?

상태 호이스팅은 자식 컴포저블 내부에서 관리하던 상태를 외부로 끌어올려 부모 컴포저블에서 관리하도록 만드는 디자인 패턴입니다. 이렇게 하면 자식 컴포저블은 상태를 직접 가지지 않고, 외부에서 주입받은 값과 이벤트를 기반으로 UI만 렌더링 하는 Stateless 컴포저블이 됩니다. 결국 상태와 UI의 책임을 명확히 분리하고 재사용성과 테스트 용이성을 높입니다. 상태 호이스팅이란 용어만 생소할 뿐 개념 자체는 Stateful 컴포저블과 Stateless 컴포저블을 나누는 것입니다. 상태를 직접 UI 렌더링에 사용하는 컴포저블보다 더 상위 레벨의 컴포저블로 끌어올려서 관리하는 것이 핵심입니다.


이전에 봤던 익숙한 예제를 다시 보겠습니다.

Stateful 컴포저블에서 아래와 같이 Text를 Stateless 컴포저블로 분리했습니다.

아직 Stateful 컴포저블과 Stateless 컴포저블로 완전하게 분리되지 않은 부분이 있습니다. 어떤 부분일까요? 바로 Button입니다. Button은 상태(count)를 관리하는 동시에 UI(Text)를 직접 그리고 있습니다. 상태를 관리하고 있는 곳은 onClick 로직입니다. 따라서 Button의 UI 렌더링 부분을 Stateless 컴포저블로 분리할 수 있습니다.

이렇게 분리하고 나면 Counter() 컴포저블 함수는 상태와 이벤트만 관리하는 Stateful 컴포저블이 됩니다. UI 구성은 자식 컴포저블에게 맡기고 있습니다. Counter() 컴포저블 함수는 그저 상태만 전달해 줄 뿐입니다.


상태뿐만 아니라 이벤트까지 분리하니 훨씬 유연한 컴포저블이 되었습니다. 유연해지면 어떤 장점이 있을까요? 재사용성이 높아집니다. CountButton의 이벤트 처리를 외부에서 정의할 수 있기 때문에 count 값을 증가하는 것뿐만 아니라 감소하도록 처리할 수 있습니다.

여기서 이런 의문을 품을 수 있습니다. 재사용성은 증가했지만 CountButton의 용도가 모호하지 않나요? 함수명을 봤을 때 증가와 감소 모두 담당한다고 유추할 수 있을까요? 그렇다고 증가 버튼과 감소 버튼을 별도로 두자니 재사용성이 낮아집니다. 이러한 상황은 전형적인 트레이드오프 상황이죠. 둘 다 장단점이 있습니다. Compose로 개발을 하면서 이러한 상황을 자주 마주할 수 있습니다. 그럴 때 재사용을 어디까지 할 것인가에 대한 본인만의 기준을 만들어 가는 연습도 필요합니다.


버튼의 UI 스타일은 재사용하면서도 함수명을 의미 있게 가져가기 위한 절충안은 아래 정도로 표현해 볼 수 있지 않을까 싶습니다. 물론 정답은 없습니다. 이 경우 컴포저블 함수가 많아져서 불편함을 느낄 수도 있습니다.


단방향 데이터 흐름(Unidirectional Data Flow, UDF)

상태 호이스팅을 잘 따르면 결국 단방향 데이터 흐름(이하 UDF)을 구축하게 됩니다. UDF는 이름 그대로 데이터가 오직 한 방향으로만 흐르도록 설계하는 방식입니다. 결국 상태 호이스팅은 Compose라는 특정 프레임워크 내에서 UDF를 구현하는데 유용한 방식을 제시한 것입니다. 마치 MVVM 아키텍처 패턴을 구현할 때 AAC ViewModel을 반드시 사용할 필요는 없지만, 안드로이드 개발에서는 유용하게 사용되는 것과 비슷합니다. UDF의 핵심은 상태는 아래로, 이벤트는 위로(State flows down, events flow up) 보내는 것입니다.


예제 State_Hoisting_02.kt의 흐름을 도식화하면 다음과 같습니다.

스크린샷 2025-09-05 031503.png

흐름을 살펴보면 '상태 → UI 렌더링 → 이벤트 → 상태 변경 → UI 렌더링'으로 요약됩니다.


Counter가 count라는 상태를 소유하고 관리합니다. 이 상태는 항상 아래(하위 컴포저블)로 전달됩니다. CountText는 전달받은 상태를 화면에 보여주기만 합니다. CountButton에서 발생한 이벤트는 위로 전달됩니다. 해당 이벤트로 변경된 상태는 다시 아래로 전달되어 화면을 갱신합니다. 앞서 언급했듯이 이런 식으로 '상태는 아래로, 이벤트는 위로' 보내는 단방향 흐름을 유지하는 것이 UDF의 핵심입니다. 처음부터 UDF를 의도한 것은 아니지만 이런 구조가 나올 수 있었던 이유는 바로 상태 호이스팅 때문입니다. 결국 Compose의 상태 호이스팅은 단순히 상태를 올리는 기법이 아니라 UDF를 구현하는 설계 원칙입니다.

keyword
매거진의 이전글Compose Stateful & Stateless