| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 |
- 66챌린지
- 안드로이드 갤러리 접근
- Kotlin FCM
- Android 12 대응
- Android 12
- scope function
- DataBinding
- Java
- 알고리즘 자바
- Android Navigation
- MVP Architecture
- Android Interceptor
- 습관만들기
- android recyclerview
- OkHttp Interceptor
- Android ViewPager2
- 영어공부
- Android ProgressBar
- 영어독립365
- 안드로이드 카카오 로그인
- 코틀린 코루틴
- coroutine
- Kotlin
- 프로그래머스 알고리즘
- 카카오 알고리즘
- 안드로이드
- Android WebView
- Android Jetpack
- Android
- WebView
- Today
- Total
나미래 Android 개발자
[Jetpack Compose] Recomposition, 왜 Jetpack Compose는 순수한 함수만 허용하는가? 본문
[Jetpack Compose] Recomposition, 왜 Jetpack Compose는 순수한 함수만 허용하는가?
Moimeme Futur 2025. 11. 16. 14:40왜 Jetpack Compose는 순수 함수만 허용한는가: Recomposition 원리와 안전한 코드 작성법
이 글은 안드로이드 공식 문서 중 CORE AREAS > UI 내용을 기반으로 작성되었습니다.
ref). 공식 문서
Jetpack Compose는 선언형 UI(Declarative UI) 패러다임을 기반으로 하여, 기존 Android View 시스템에서 사용하던 명령형 UI(Impreative UI) 방식과 완전히 다른 사고방식을 요구한다. 선언형 UI의 핵심은 "데이터가 바뀌면 UI를 다시 그리는 것"이며, Compose에서는 이를 Recomposition이라고 부른다.
Recomposition을 올바르게 이해하기 위해서는 다음과 같은 Compose의 특성과 미래 방향성까지 함께 고려해야 한다.
- Recomposition은 필요한 부분만 다시 실행한다
- Recomposition은 추정 기반 재구성이며 언제든지 취소될 수 있다
- Composable은 매우 자주 실행될 수 있다
- Composable은 미래에 병렬로 실행될 수 있다
- Composable은 미래에 어떤 순서로 실행될지 보장되지 않는다.
따라서 모든 Composable은 빠르고 idempotent이며, side-effect가 없어야 한다.
1. Recomposition이란?
명령형 UI에서는 View의 속성을 setter로 변경해 UI를 업데이트했다. 반면 Compose에서는 데이터가 바뀌면 Composable을 다시 호출하여 UI를 갱신하고 이를 Recomposition이라 한다.
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
clicks가 바뀌면 Compose는 이 Composable을 재실행(Recomposition)하여 새로운 UI로 업데이트한다.
참고, Compose는 의존성이 있는 Composable만 다시 실행하여 효율적으로 UI를 업데이트한다.
2. Recomposition은 필요한 부분만 다시 실행한다
Compose에서 Recomposition은 전체 UI를 다시 실행하는 방식이 아니다. 입력값이 바뀌었을 떄, 그 값에 직접적으로 의존하는 Composable만 선택적으로 다시 실행된다. 이를 통해 Compose는 불필요한 계산을 피하고, 화면 전체를 다시 그리는 데 드는 비용을 최소화한다.
이 개념을 잘 보여주는 예제가 아래 코드다. 이 UI는 header와 names라는 두 가지 독립적인 데이터에 의존하는 두 영역으로 구성된다. 상단의 Header(Text)는 header값에만 의존하고, 리스트 아이템은 names[i] 각각에 의존한다.
/**
* Display a list of names the user can click with a header
*/
@Composable
fun NamePicker(
header: String,
names: List<String>,
onNameClicked: (String) -> Unit
) {
Column {
// this will recompose when [header] changes, but not when [names] changes
Text(header, style = MaterialTheme.typography.bodyLarge)
HorizontalDivider()
LazyColumn {
items(names) { name ->
// When an item's [name] updates, the adapter for that item
// will recompose. This will not recompose when [header] changes
NamePickerItem(name, onNameClicked)
}
}
}
}
/**
* Display a single name the user can click.
*/
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}
이 예제의 핵심은 다음과 같다.
header가 변경되면Text(header)만 다시 실행되고, 리스트는 영향을 받지 않는다names[i]중 일부가 변경되면 그 아이템만 다시 실행되며, Header나 다른 아이템은 재구성되지 않는다LazyColumn전체가 다시 실행되는 것도 아니며,Column이나 그 부모 Composable도 필요하지 않으면 건너뛰어진다
Compose는 내부적으로 UI트리를 기억하고 있고, 각 Composable이 어떤 입력에 의존하는지 추적한다. 이러한 정보가 있기 때문에 변경된 부분만 정확히 재실행하여 성능을 최적화할 수 있는 것이다. 즉, Composable의 핵심 장점 중 하나는 필요한 부분만 정교하게 다시 그리는 능력(Recomposition)이다.
3. Recomposition은 추정 기반 재구성이며 언제든 취소될 수 있다
Compose는 "입력이 바뀌었을 가능성이 있다"고 추정하는 순간 Recomposition을 시작한다. 하지만 Recomposition이 끝나기 전에 또 입력이 바뀌면 진행 중이던 Recomposition을 취소하고, 새로운 값으로 Recomposition을 다시 시작한다. 하지만 취소된 Recomposition 내부에서 실행된 side-effect는 되돌릴 수 없다. UI는 변경되지 않았는데, 내부 상태는 바뀌는 불일치 문제가 발생할 수 있는 것이다.
즉, Composable 내부에서 ViewModel 업데이트, 전역 상태 변경 등 side-effect를 절대 발생시키면 안되는 이유다.
4. Composable은 매우 자주 실행될 수 있다
애니메이션이 포함된 UI는 1초에 수십~수백 번 Recomposition 될 수 있다. 따라서 다음 작업은 Composable 내부에서 동작하게 하는 것은 금지된다.
- SharedPreferences 읽기
- DB 쿼리
- 파일 IO
- 네트워크 호출
- 무거운 계산
이러한 작업은 반드시 ViewModel 또는 Background coroutine에서 수행하고, 결과만 Composable로 전달해야 한다.
5. Composable은 미래에 병렬로 실행될 수 있다
Compose는 현재 단일 메인 스레드에서 실행되지만, 공식 문서에서 명시한 것처럼 "미래에는 Composable이 병렬로 실행될 수 있다"는 점이 매우 중요하다. 이는 단순한 가능성이 아니라 Compose 설계 철학의 핵심이며, 개발자는 지금부터 이 설계 방향을 고려해 코드를 작성해야 한다.
Compose가 병렬 Recomposition을 지원하게 되면 다음과 같은 최적화가 가능해진다.
- 여러 Composable은 서로 다른 CPU 코어에서 동시에 실행
- 화면에 보이지 않는 요소는 낮은 우선순위 스레드에서 Recomposition
- 전체 UI를 더 빠르게 계산하기 위한 백그라운드 스레드 기반 Recomposition
하지만 이러한 기능이 도입되면, 개발자가 잘못 작성한 Composable은 치명적인 race condition이나 잘못된 UI 결과로 이어질 수 있다.
병렬 실행이 도입되면 무엇인 문제인가?
Composable 함수가 서로 다른 스레드에서 동시에 호출될 수 있기 때문에 다음 두 경우가 가장 위험하다.
- Composable 내부에서 ViewModel의 상태를 직접 변경하는 경우
- Composable 내부에서 지역 변수(var)를 변경하는 경우
Compose는 Composable을 백그라운드 스레드 풀에서 실행될 수 있다. 이때 같은 Composable이 여러 스레드에서 동시에 호출되면 다음과 같은 현상이 발생한다.
- 단순 증가 연산(
++)같은 비원자적인 연산이 섞여 race condition 발생 - UI 결과가 의도와 다르게 꼬임
- 내부 상태가 예측 불가능해짐
- Recomposition 타이밍에 따라 값이 계속 달라져 디버깅이 불가능해짐
이것이 Compose가 "Composable에 side-effect를 절내 넣지 말라"라고 강조하는 이유다.
아래 코드는 공식문서에서 제공한 Composable 내부에서 로컬 상태를 변경하는 잘못된 예시 코드이다.
@Composable
fun ListWithBug(myList: List<String>) {
var items = 0
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Card {
Text("Item: $item")
items++ // Avoid! Side-effect of the column recomposing.
}
}
}
}
}
이 코드는 두 가지 이유로 잘못되었다.
- Recomposition 때마다
items가 증가한다 - 병렬 Recomposition 환경에서는
items값이 원자적이지 않아 치명적으로 꼬인다
이런 식으로 이론적으로는 10개의 아이템이 있어도 items가 7이 될 수 있고, 20이 될 수도 있다. 그러므로 이러한 위험을 원천 방지하기 위해 Composable 내부에서 mutable state 변경을 금지하는 것이다.
6. Composable은 미래에 어떤 순서로 실행될지 보장되지 않는다
Compose는 현제 메인 스레드에서 동작하지만, 공식 문서에서 강조하듯 Compose는 처음부터 멀티스레드를 염두에 두고 설계되었다. 따라서 미래에는 Composable이 병렬로 실행되거나, 우선순위 기반으로 재배치되어 실행될 수 있다. 이 점은 개발자가 Composable을 작성할 때 반드시 고려해야 하는 중요한 특성이다.
그러므로 미래의 Compose에서는 Composable 함수가 코드에 적힌 순서대로 실행될 것이라고 가정할 수 없다.
즉, 다음처럼 Composable을 나열해도
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
실제로는 아래와 같은 순서로 실행될 수 있다.
- EndScreen -> StartScreen -> MiddleScreen
- MiddleScreen -> EndScreen -> StartScreen
- 혹은 일부는 병렬로 실행되고, 일부는 뒤늦게 실행됨
이러한 동작은 Compose가 UI의 요소의 우선순위나 렌더링 필요성을 고려하여 가장 효울적인 순서를 선택하도록 설계되었기 때문이다. StartScreen이 항상 MiddleScreen 보다 먼저 실행될 것이라고 생각하면 안되고, Composable에서 side-effect를 넣지 말라고 하는 이유다.
Compose는 현재 단일 스레드 환경에서도 순서를 보장하지 않는 설계 철학을 가지고 있으며, 미래에는 멀티스레드를 통한 병렬 실행이 도입될 가능성이 높다.
따라서 Composable을 작성할 때, 개발자는 다음을 항상 고려해야 한다.
- Composable은 순수 UI 생성 함수여야 하며, 다른 Composable의 실행 순서에 의존해서는 안 된다
- 전역 상태 변경이나 값 누적 같은 side-effect는 절대 Composable 내부에 포함되어서는 안된다
이 원칙을 지키며 Compose의 현재 및 미래 실행 모델 모두에서 안전하게 동작하는 UI 구조를 만들 수 있다.
'안드로이드 > Jetpack Compose' 카테고리의 다른 글
| [Android Compose] Composition·Composable·Composition 관계 정리 (0) | 2025.11.19 |
|---|
