이전 포스팅에서는 젯팩 컴포즈 스터디를 시작하게 된 동기를 작성했다.
지금부터는 본격적으로 젯팩 컴포즈에 대해 스터디를 해보자. 이번 포스팅에서 다룰 챕터는 다음과 같다.
- Ch18 컴포즈 개요
- Ch19 컴포저블 함수 개요
- Ch20 컴포즈 상태와 재구성
- Ch21 CompositionLocal
Ch18 컴포즈 개요
젯팩 컴포즈는 1)앱을 더 쉽고, 빠르게 개발할수 있도록 해주고, 2)일반적인 버그에 취약하지 않도록 하는 것이 목표라고 한다.
# 더 쉽고 빠르게?
기존 안드로이드 앱 개발방식은 화면을 구성하는 UI 컴포넌트를 xml 에 명시한 후, 사용자와의 동적인 상호작용을 코드로 구현하는 방식이었다. xml 과 Kotlin 을 함께 작성해야 하는 번거로움이 존재했고, xml 에 정의한 리소스를 Kotlin 에서 접근하기 위한 보일러플레이트 코드양이 많았다. 이런 기존의 개발방식은 효율적이지 않았기 때문에, 이를 젯팩 컴포즈로 개선한다는 의미로 이해했다. 젯팩 컴포즈가 어떻게 더 쉽게/빠르게 앱 개발을 가능하게 하는지는 이제 살펴보면 될겠다.
# 일반적인 버그에 취약하지 않도록
나는 안드로이드 앱 개발자는 아니라서 개인적인 경험을 언급할 순 없지만, 그냥 생각해봐도 앱 화면을 구성하는 수많은 UI 컴포넌트들이 특정 '상태'에 완벽하게 동기화되도록 구현하는 것은 꽤나 까다로운 작업일 것이다. 음악을 들으면서, 인터넷을 하면서, 채팅을 하면서, 사진을 업로드 하면서, 화면을 전환하는 작업을 할때 이러한 여러 상황에 맞게 화면 구성요소들의 속성값들이 동기화되지 않는 경우가 빈번하게 발생할 수 있다. 이러한 문제를 젯팩 컴포즈는 어떻게 해결하는 것일까?
컴포즈는 선언적 구문 도입 (무슨말이야)
기존 안드로이드의 화면 구성방식과 다르게, 젯팩 컴포즈를 사용하면, 컴포넌트 모양 및 세부속성을 일일이 설정하지 않아도 직관적인 Kotlin 코드를 사용하여 화면을 구성할수 있다고 한다. UI 컴포넌트가 화면에 보이는 방식을 Kotlin 코드로 '선언'함으로써 앱의 화면(레이아웃)을 구성하는 방법인 것이다. 이것이 '선언적 구문'의 의미이다. 개발자가 UI 컴포넌트를 '선언'하기만 하면, 화면에 렌더링하기 위한 번거로운/복잡한 작업은 젯팻 컴포즈가 알아서 처리해준다.
컴포즈는 데이터 주도적 (무슨말이야222)
여기서 데이터는 'UI 컴포넌트의 상태 데이터'를 생각하면 된다. 젯팩 컴포즈 도입전에는 화면에 보이는 UI 컴포넌트의 상태값을 개발자가 잘 관리해야 했다. 어떤 이벤트로 인해 값이 변경되었다면 해당 UI 컴포넌트의 변경된 값이 계속 유지될 수 있도록 로직을 잘 구현해야 한다는 의미이다. 젯팩 컴포즈는 '상태기반 시스템'을 제공하여 이러한 번거로움을 제거했다. UI 데이터를 '상태'로서 저장하면 데이터 변경 감지 및 UI 업데이트 로직은 (개발자가 신경쓰지 않아도) 젯팩 컴포즈가 알아서 업데이트 해준다. 이것을 recomposition 이라고 한다. 즉, '데이터' 주도적으로 UI 가 관리된다는 의미이다.
Ch19 컴포저블 함수 개요
컴포저블 함수란?
컴포저블 함수는 젯팩 컴포즈를 사용해서 화면을 구성하기 위한 '빌딩 블럭' 이다. 컴포즈에서 기본적으로 제공하는 함수도 있고, 사용자가 정의할 수도 있다. 코드레벨로 본다면 함수이름에 Composable 이라고 annotation 이 붙어 있는 Kotlin 함수가 컴포저블 함수인 것이다. 이는 Kotlin 의 일반함수와는 구별되는 특징이 있다.

젯팩 컴포즈를 사용하여 앱 화면을 구성하는 경우, xml 없이 Kotlin 코드만을 사용한다. Composable annotation 이 붙어있는 컴포저블 함수를 계층적으로 호출하여 UI 를 구성한다.
제약사항:
컴포저블 함수내에서 일반 Kotlin 함수는 호출할수 있지만, 일반 Kotlin 함수에서 컴포저블 함수를 호출할수는 없다.
상태 컴포저블, 비상태 컴포저블
컴포저블은 앱 화면을 구성하는데 영향을 미치는 함수라고 보면 되는데, 이때 '상태 값'을 가지고 있는 컴포저블 함수를 '상태 컴포저블', 그렇지 않은 경우 '비상태 컴포저블' 이라고 한다.
'상태'라는 단어가 계속 나오는데, 예를 들어 이해해보자. 사용자가 좌우로 움직일 수 있는 슬라이더(UI 컴포넌트)가 있다고 해보자. 이 슬라이더에 대한 컴포저블 함수는 슬라이더의 위치값을 가지고 있을것이다. 이 위치값이 '상태' 인 것이다.

상태 컴포저블, 비상태 컴포저블을 왜 구분하는걸까?
1) 비상태 컴포저블은 상태를 가지지 않기 때문에 stateless 라고 할수 있고 이는 일반적인 SW개발에서 '재사용' 측면의 장점을 가진다. 따라서 비상태 컴포저블이 많을수록 앱개발의 개발속도(생산성) 및 유지보수성이 향상된다고 볼수 있다.
2) 상태 컴포저블은 '상태'에 의존적이다. 따라서 '상태'를 잘 관리하는 것이 중요한데, 여러 소스에서 '상태'를 동시에 변경할 경우 관리가 어려워진다. 뿐만 아니라 변경된 상태정보에 영향을 받는 컴포넌트에게는 누락없이 전달(propagation)해줘야 한다. 상태 컴포저블의 경우에는 이런 부분을 잘 고려해야 한다.
즉 안드로이드 앱 개발시 상태 컴포저블과 비상태 컴포저블에 따른 주요 고려사항을 잘 파악하기 위해 둘을 구분짓는 것이다.
컴포저블 함수의 예
아래는 컴포저블 함수의 예시이다.
- Composable annotation 이 있으며
- 컴포저블 함수내에서 다른 컴포저블 함수를 호출할수 있고,
- 컴포저블 함수도 파라미터를 받을 수 있다.
- 컴포저블 함수는 값을 반환하지 않는다.
@Composable
fun MyFunction() { }
@Composable
fun MyFuntion() {
Text("Hello")
}
@Composable
fun CustomText(text: String) {
Text(text=text)
}
컴포저블 함수의 상태는 어떻게 정의할까?
아래 CustomSwitch 컴포저블 함수 내부에 checked 라는 변수를 정의했다.
이 변수는 일반 Kotlin 변수와 다르게 by remember {mutableStateOf(true)} 가 뒤에 붙어있다.
- remember: checked 상태변수는 CustomSwitch UI 컴포넌트의 마지막 상태값을 '기억'하라는 의미
- mutableStateOf(true): checked 상태변수는 변경가능하며, 초기값은 true 라는 의미
@Composable
fun CustomSwitch() {
val checked by remember {mutableStateOf(true)}
...
}
Ch20 컴포즈 상태와 재구성
젯팩 컴포즈에서 '상태'란?
"시간에 따라 변경될 수 있는 값"
그렇다면 '상태'는 코틀린의 일반 변수와 무슨 차이가 있는걸까? 아래 2가지 차이가 있다.
- 컴포저블 함수의 '상태'는 "기억"된다.
'상태'값을 포함하는 컴포저블 함수를 호출할때마다, 상태변수가 초기화되는 것이 아니라, 이전 호출시의 '상태'값을 기억해서 값을 유지한다. - '상태'는 컴포저블 함수계층에 전파된다.
부모 컴포저블에서 정의한 '상태' 값이 변경되면, 변경된 값이 컴포저블 함수 계층을 따라 자식 컴포저블에 전파된다. 변경된 '상태'값이 계층을 따라 전파되면, 영향을 받는 자식 컴포저블에 해당하는 UI 컴포넌트들은 변경된 상태에 맞게 UI 가 다시 렌더링된다. 이를 재구성(recomposition)이라고 한다.
'재구성'이란
컴포저블 함수의 상태값의 변화에 영향을 받는 모든 컴포저블 함수를 재구성하는 것.
즉, 해당 컴포저블 함수를 다시 호출하고 새로운 상태값을 전달하는 것.
컴포저블의 '상태'는 자식 컴포저블 함수에 의해 '직접적으로' 변경되면 안된다. 이를 '단방향 데이터 흐름(unidirectional data flow)' 이라고 한다. 아래 그림을 보면 FunctionA 컴포저블이 "Switch 상태값"을 가지고 있고, FunctionB, Switch 로 전달되고 있다. 사용자가 Switch 를 변경하면, 상태값을 곧바로 변경하는 것이 아니라, Switch 상태값 변경을 처리하는 FunctionA 의 이벤트 핸들러가 호출되고, Function A 의 이벤트핸들러에서 상태값을 변경한다.

젯팻 컴포즈는 왜 이렇게 설계를 했을까?
- 예측 가능성 및 디버깅 용이성
- 데이터(상태)의 흐름이 부모에서 자식으로 전달되고, 이벤트(상태 변경 요청)는 자식에서 부모로 전달되면 상태가 어디서 변경되었는지 추적이 용이함. 만약 자식 컴포저블이 부모 상태를 변경하는 것을 허용하면 동시에 여러곳에서 상태변경이 발생하여 앱의 동작 예측이 어렵고, 버그 발생시 디버깅이 어려워짐
- 상태 고립 및 재사용성 향상
- 중요한 소프트웨어 설계원칙 중 하나는 "관심사의 분리"
- 상태 정의 및 관리 역할은 부모 컴포저블, 상태에 따라 화면에 출력하는 역할은 자식 컴포저블로 역할을 분리
- 컴포저블 마다 관심사를 분리하여 로직이 단순해지고, 의존성이 줄어들어 재사용성이 향상됨
- 테스트 용이성
- 자식 컴포저블은 상태값에 따라 화면에 출력만 하기 때문에 단순하게 동작하여 테스트가 간단해짐.
상태 호이스팅(hoist)이란
컴포저블 함수를 작성할때는 '재사용성'을 최대화 하는 것이 중요하다. 컴포저블 함수를 비상태 컴포저블 함수로 만들어야 재사용성이 향상된다. 따라서 컴포저블의 상태값은 되도록 상위 컴포저블에서 관리되고, 자식 컴포저블로 전파되는 방향으로 설계를 해야 비상태 컴포저블이 많아지고 재사용성이 향상된다. 이런식으로 자식에서 부모 컴포저블로 컴포즈 계층의 상위로 상태를 올리는 것을 상태 호이스팅이라고 한다.
아래 컴포저블 계층예시에서 NameField 와 NameText 가 모두 textState 상태값이 따라 렌더링 되는 컴포저블이라고 할때, 이 상태값은 MainScreen 에서 관리되는 것이 좋을것이다. 만약 기존에 SubScreen 에서 상태값이 정의되었다고 할때, 이를 Main Screen 으로 올리는 것이 상태호이스팅이다.

Ch21 CompositionLocal
앞에서 부모 컴포저블에서 정의한 상태는 자식 컴포저블로 전파된다고 했는데, 아래와 같은 상황을 가정해보자. 컴포저블1에서 정의한 상태값(textState)을 모든 자식 컴포저블이 필요한것이 아니라, 컴포저블4만 필요로 하는 경우이다. 상태값을 필요로 하지 않는 컴포저블2, 3에도 상태값이 전파되는 문제가 발생한다.
이때 CompositionLocal 을 사용하면, 컴포저블2, 3에 상태를 전달하지 않고 컴포저블4에서 상태값을 사용할 수 있다.

아래코드는 CompositionLocal 에 대한 샘플코드이다. Composable3 하위의 컴포저블 함수에 한정하여 color 상태값이 전파된다.
// ....
val LocalColor = staticCompositionLocalOf { Color(0xFFffdbcf) }
@Composable
fun Composable1() {
var color = if (isSystemInDarkTheme()) {
Color(0xFFFFFFFF)
} else {
Color(0xFFffdbcf)
}
Column {
Composable2()
CompositionLocalProvider(LocalColor provides color) {
Composable3()
}
}
}
아직 앱을 직접 개발하지 않고, 예제코드만 봐서 그런지, CompositionLocal 은 필요성을 크게 느끼지는 못하겠다.
그래도 이런게 있구나 정도를 인지하고 있어야 향후에 개발할 때 필요한 순간에 사용할수 있다.
오늘은 여기까지.
Spark. Then. Move !
'안드로이드 앱' 카테고리의 다른 글
| 안드로이드 젯팩 컴포즈 (ch26~28) - Box레이아웃, 커스텀 레이아웃 (0) | 2025.11.28 |
|---|---|
| 안드로이드 젯팩 컴포즈 (ch22~25) - Slot API, 모디파이어(Modifier), Row/Column 레이아웃 (0) | 2025.11.26 |
| 안드로이드 젯팩 컴포즈 스터디 시작. (0) | 2025.11.23 |
| 안드로이드 앱 개발에 필요한 모든 것 (목차) (0) | 2025.11.22 |
| 안드로이드 앱 3개 따라만들기 (0) | 2025.11.22 |