Stability์ ๋ํ ์ดํด๋ฅผ ํตํด Jetpack Compose์์ Recomposition ์ต์ ํํ๊ธฐ
- #Android
- #Jetpack Compose
- #Compose
- #Recomposition
- #Stable
๋ค์ด๊ฐ๋ฉฐ
์๋ ํ์ธ์. ์๋์ ๋ชจ๋ฐ์ผํ ์๋๋ก์ด๋ ๊ฐ๋ฐ์ ์ค์์์ ๋๋ค.
Jetpack Compose๊ฐ ์ ์ ๋ฒ์ ์ผ๋ก ์ถ์๋์ง๋ ๋ฒ์จ 2๋ ์ด ์ง๋ฌ๋๋ฐ์. ์ ํฌํ์์๋ ์กฐ๊ธ ๋ฆ๊ธดํ์ง๋ง, ์ง๋ 6์๋ถํฐ ํ๋ก์ ํธ์ Jetpack Compose๋ฅผ ๋์ ํ๊ธฐ ์์ํ์ต๋๋ค. ๊ฐ์ฅ ์ฒ์ Compose๋ฅผ ์ฌ์ฉํ์ฌ ๋ง๋ ํ๋ฉด์ fromm์ฑ์ ๊ด์ฌ์ํฐ ์ ํ ํ๋ฉด์ด์์ต๋๋ค. ํ์ง๋ง ์ด ํ๋ฉด์ ๊ฐ๋ฐํ์์ ๋น์์๋ Compose์ ๋ํ ๊น์ด ์๋ ์ดํด๊ฐ ์ ํ๋์ง ์์๋ ํฐ๋ผ ์์ํ๋ ๊ฒ๊ณผ ๋ฌ๋ฆฌ ์ฌ๊ตฌ์ฑ(Recomposition)์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
๋ฐ๋ผ์ ์ด๋ฒ ๊ธ์์๋ ์ด๊ธฐ์ ๊ด์ฌ์ํฐ ํ๋ฉด๊ณผ ์ฝ๋๋ค์ ์ดํด๋ณด๋ฉฐ ์ ์ฌ๊ตฌ์ฑ์ด ๋ฐ์ํ๋์ง, ์ด๋ป๊ฒ ๋ถํ์ํ ์ฌ๊ตฌ์ฑ์ ์ค์ฌ๋๊ฐ๋์ง ์ดํด๋ณด๊ณ ์ ํฉ๋๋ค. ํนํ ์ฌ๊ตฌ์ฑ์ด ๋์ํ๋ ๋ฐ ์์ด ํต์ฌ๊ฐ๋ ์ธ Stability(์์ ์ฑ)์ ์ค์ฌ์ผ๋ก Compose์ ์ฌ๊ตฌ์ฑ์ ์์๋ณด๊ฒ ์ต๋๋ค. ์ด๋ฅผ ํตํด ์ปดํฌ์ ๋ธ์ ์ฌ๊ตฌ์ฑ์ ์ต์ ํํ์ฌ ์ฑ๋ฅ์ ํฅ์์ํค๋ ๋ฐ ๋์์ ์ป์ ์ ์๊ธฐ๋ฅผ ๋ฐ๋๋๋ค.
๋ถํ์ํ ์ฌ๊ตฌ์ฑ ๋ฐ์ ์ฌ๋ก
Icon
๊ด์ฌ์ํฐ ์ ํ ํ๋ฉด์๋ ๋ค์๊ณผ ๊ฐ์ด ํ๋กฌ์ ์
์ ํด ์๋ ์ํฐ๋ค์ ๊ฒ์ํ ์ ์๋ SearchTextField
๋ผ๋ ์ปดํฌ์ ๋ธ์ด ์์ต๋๋ค.
์ด ์ปดํฌ์ ๋ธ์ query
๋ผ๋ ์ํ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค.
@Composable
fun SearchTextField() {
val (query, setText) = remember { mutableStateOf("") }
BasicTextField(
value = query,
onValueChange = { setText(it) },
decorationBox = { innerTextField ->
Box(
) {
if (query.isEmpty()) {
Text(text = "์ํฐ์คํธ๋ช
๊ฒ์")
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.baseline_search_24),
contentDescription = "๊ฒ์ ์์ด์ฝ",
)
Box(
) {
innerTextField()
}
if (query.isNotEmpty()) {
Icon(
painter = painterResource(id = R.drawable.baseline_search_24),
contentDescription = "์ง์ฐ๊ธฐ ์์ด์ฝ",
modifier = Modifier.clickable { setText("") }
)
}
}
}
}
)
}
๊ทธ๋ฐ๋ฐ Layout Inspector๋ฅผ ํตํด ์ดํด๋ณด๋, SearchTextField
์ ํ
์คํธ๋ฅผ ์
๋ ฅํ ๋๋ง๋ค ๊ฒ์ ์์ด์ฝ๊ณผ ์ง์ฐ๊ธฐ ์์ด์ฝ์ด ๋ชจ๋ ์ฌ๊ตฌ์ฑ๋๋ค๋ ์ฌ์ค์ ๋ฐ๊ฒฌํ์ต๋๋ค.
์ ์ ๊ฐ ํ
์คํธ๋ฅผ ์
๋ ฅํ์ฌ query
๊ฐ ๋ณ๊ฒฝ๋์์ผ๋ฏ๋ก ์ฌ๊ตฌ์ฑ์ด ๋ฐ์ํ๋ ๊ฒ์ธ๋ฐ์. ๊ทธ๋ฐ๋ฐ ์ ์
๋ฐ์ดํธ๊ฐ ํ์ํ ๋ถ๋ถ๋ง์ ์ฌ๊ตฌ์ฑํ๋ ๊ฒ์ด ์๋๋ผ ๊ฑด๋๋ฐ์ด์ผ ํ Icon๋ ํญ์ ์ฌ๊ตฌ์ฑ๋๋ ๊ฒ์ผ๊น์?
๋ต์ Painter
ํด๋์ค๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์
๋๋ค.
๊ณต์๋ฌธ์์ ๋ฐ๋ฅด๋ฉด, Painter
ํด๋์ค๋ ์ด๋ฏธ์ง ์ฒ๋ฆฌ์ ๋ณต์ก์ฑ์ผ๋ก ์ธํด ์์ ์ ์ธ(stable) ํด๋์ค๋ก ๋ช
์ํ์ง ์๋๋ค๊ณ ๋์์์ต๋๋ค.
์ด โ์์ ์ โ์ด๋ผ๋ ์๋ฏธ๊ฐ ๋ฌด์์ธ์ง๋ ์ด๋ฐ๊ฐ ์ดํด๋ณด๋๋ก ํ๊ณ ์ฌ๊ธฐ์๋ ๋ค์ ๋ ๊ฐ์ง๋ง ๊ธฐ์ตํ๊ณ ๋์ด๊ฐ๊ฒ ์ต๋๋ค.
- ์ ๋ ฅ์ด ์์ ์ ์ด๊ณ ๋ณ๊ฒฝ๋์ง ์์์ผ ์ฌ๊ตฌ์ฑ์ ๊ฑด๋๋ธ ์ ์๋ค.
- ์์ ์ ์ธ ํ์ ์ผ๋ก๋ ์์ํ์ (Int, Float, Boolean ๋ฑ)๊ณผ ๋ฌธ์์ด, ๋ชจ๋ ํจ์ ํ์ (๋๋ค)์ด ์๋ค.
๋ฐ๋ผ์ Painter
๋ฅผ ์ฌ์ฉํ๋ ๋์ Int ํ์
์ ๋ฆฌ์์ค ์์ด๋๋ฅผ ๋งค๊ฐ๋ณ์๋ก ์ ๋ฌํ๋ ์๋ก์ด ์ปดํฌ์ ๋ธ์ ๋ง๋ค๋ฉด ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋ฉ๋๋ค.
@Composable
fun StableImage (
@DrawableRes drawableResId: Int,
description : String,
) {
val painter = painterResource(id = drawableResId)
Image(
painter = painter,
contentDescription = description
)
}
์ด์ ๊ธฐ์กด Icon ๋์ StableImage
๋ฅผ ์ฌ์ฉํ๋๋ก ์ฝ๋๋ฅผ ์์ ํ๋ฉด Int ํ์
์ธ drawableResId
๊ฐ ๋ณ๊ฒฝ๋์ง ์๋ ์ด์ Icon์ ์ฌ๊ตฌ์ฑ๋์ง ์๊ณ ๊ฑด๋๋ฐ๊ฒ ๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๋๋, ImageVector
๋ฅผ ์ฌ์ฉํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์
๋๋ค.
์ด ๊ฒฝ์ฐ์๋ query
๊ฐ ์๋ฌด๋ฆฌ ๋ณ๊ฒฝ๋์ด๋ Icon์ ์ฌ๊ตฌ์ฑ๋์ง ์์ต๋๋ค!
Button
๋ ๋ค๋ฅธ ์ผ์ด์ค๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ๊ด์ฌ์ํฐํ๋ฉด ์๋จ์๋ DefaultTopBarWithLeftIconAndRightText
(์ดํ DefaultTopBar)๊ฐ ์๋๋ฐ์.
DefaultTopBar
๋ด๋ถ์๋ ํ๋ฉด์ ๋ซ์ ์ ์๋ ํ์ดํ IconButton๊ณผ โ์ ์ฅโ์ด๋ผ๊ณ ์ฐ์ฌ์ง Text๊ฐ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ FavoriteScreen
์ DefaultTopBar
๋ฅผ ๊ฐ์ง๊ณ ์์ผ๋ฉด์ ๊ด์ฌ์ํฐ ํธ์ง ์ฌ๋ถ๋ฅผ isEdited
๋ณ์์ ์ ์ฅํ์ฌ, isEdited
๊ฐ true๋ก ๋ณ๊ฒฝ๋์์ ๊ฒฝ์ฐ ๊ฐ์ฅ ์ค๋ฅธ์ชฝ์ โ์ ์ฅโ ๋ฒํผ์ ํด๋ฆญํ ์ ์๋๋ก enabled ์์ฑ์ ๋ณ๊ฒฝํด ์ค๋๋ค.
@Composable
fun FavoriteScreen() {
val isEdited = remember { mutableStateOf(false) }
DefaultTopBarWithLeftIconAndRightText(
isRightTextEnabled = isEdited.value,
onClickLeftIcon = {
viewModel.onBackClicked()
},
onClickRightText = {
viewModel.saveClicked()
}
)
}
์ ์ ๊ฐ ๊ด์ฌ์ํฐ๋ฅผ ์ ํํ๋ฉด isEdited
๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด์ ์ ์ฅ๋ฒํผ์ธ Text๊ฐ ์ฌ๊ตฌ์ฑ๋๋๋ฐ ๋ฌธ์ ๋ ์ด ๋ IconButton๋ ์ฌ๊ตฌ์ฑ์ด ์ผ์ด๋๋ค๋ ๊ฒ์
๋๋ค.
IconButton์ ์ค์ง onClickLeftIcon
์ด๋ผ๋ ๋๋ค๋ง์ ๋งค๊ฐ๋ณ์๋ก ๋ฐ๋๋ฐ์. ์์ ์ดํด๋ณธ ๊ฒ์ฒ๋ผ ๋๋ค๋ ์์ ์ ์ธ ํ์
์ด๋ฏ๋ก, onClickLeftIcon
์ด ๋ณ๊ฒฝ๋์ง ์๋ ์ด์ IconButton์ ์ฌ๊ตฌ์ฑ๋์ง ์์์ผ ํฉ๋๋ค.
๊ทธ๋ฐ๋ฐ ์ ์ฌ๊ตฌ์ฑ์ด ๋ฐ์ํ ๊น์?
๊ทธ๊ฒ์ ๋ฐ๋ก onClickLeftIcon
๋๋ค์์ ๋ถ์์ ํ(unstable) ViewModel ํด๋์ค๋ฅผ ์บก์ฒํ์ฌ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์
๋๋ค. ์ฆ, ์ปดํฌ์ฆ ์ปดํ์ผ๋ฌ ์
์ฅ์์๋ ViewModel์ด ๋ณ๊ฒฝ๋์ง ์๋์ง ํ์ ํ ์ ์๊ธฐ ๋๋ฌธ์ FavoriteScreen
์ด ์ฌ๊ตฌ์ฑ๋ ๋๋ง๋ค onClickLeftIcon
์ธ์คํด์ค๋ ๋งค๋ฒ ์๋กญ๊ฒ ์์ฑ์ด ๋์ด ์ฌ๊ตฌ์ฑ์ด ๋ฐ์ํ๋ ๊ฒ์
๋๋ค.
๋ฐ๋ผ์ ๋์ผํ onClickLeftIcon
์ธ์คํด์ค๋ฅผ ์ฌ์ฉํ๋๋ก ์ฝ๋๋ฅผ ์์ ํ๋ฉด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
์๋ฅผ ๋ค์ด, ๋ทฐ๋ชจ๋ธ์ ์ฐธ์กฐํ๋ ๋๋ค๋ฅผ ์ฌ์ฉํ๋ ๋์ ๋ฉ์๋ ์ฐธ์กฐ๋ฅผ ์ฌ์ฉํ๋ฉด ํญ์ ๋์ผํ ์ฃผ์๊ฐ ์ ๋ฌ๋๊ธฐ ๋๋ฌธ์ ์ฌ๊ตฌ์ฑ์ด ๋ฐ์ํ์ง ์์ต๋๋ค.
DefaultTopBarWithLeftIconAndRightText(
isRightTextEnabled = isEdited.value,
onClickLeftIcon = viewModel::onBackClicked,
onClickRightText = viewModel::saveClicked
)
๋๋ remember
๋ฅผ ํตํด ๋๋ค ์ธ์คํด์ค๋ฅผ ์๋ก์ด ๋ณ์๋ก ์ ์ฅํ์ฌ ์ฌ๊ตฌ์ฑ์ด ๋ฐ์ํ๋๋ผ๋ ์ ์ง๋๋๋ก ํ๋ ๋ฐฉ๋ฒ๋ ์์ต๋๋ค.
@Composable
fun FavoriteScreen() {
val isEdited = remember { mutableStateOf(false) }
val onClickLeftIcon = remember { viewModel.onBackClicked() }
val onClickRightText = remember { viewModel.saveClicked() }
DefaultTopBarWithLeftIconAndRightText(
isRightTextEnabled = isEdited.value,
onClickLeftIcon = onClickLeftIcon,
onClickRightText = onClickRightText
)
}
๋ ๊ฐ์ง ํด๊ฒฐ์ฑ ๋ชจ๋ Layout Inspector ์์์ IconButton์ด ์ฌ๊ตฌ์ฑ๋์ง ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
Stability์ ๋ํ ์ดํด
์์ ๋ ๊ฐ์ง ์ผ์ด์ค๋ฅผ ํตํด ์ปดํฌ์ ๋ธ์ ๋งค๊ฐ๋ณ์ ํ์ ์ด ๋ถ์์ ํ๊ฑฐ๋ ๋ณ๊ฒฝ๋ ๋ ์ฌ๊ตฌ์ฑ์ด ๋๋ค๋ ์ฌ์ค์ ์ ์ ์์์ต๋๋ค. ๋ฐ๋๋ก ๋ชจ๋ ์ ๋ ฅ์ด ์์ ์ ์ด๊ณ ๋ณ๊ฒฝ๋์ง ์๋๋ค๋ฉด ์ฌ๊ตฌ์ฑ์ ๊ฑด๋๋ธ ์ ์๊ฒ ๋ฉ๋๋ค. ๋ฐ๋ผ์ Compose ์ปดํ์ผ๋ฌ๋ ์ฌ๊ตฌ์ฑ ์ค์ ๊ฑด๋๋ฐ์ด์ผ ํ ํจ์๋ฅผ ๊ฒฐ์ ํ๊ธฐ ์ํด ์ปดํฌ์ ๋ธ์ ๋งค๊ฐ๋ณ์ ํ์ ์ด ์์ ์ ์ธ์ง ์ฌ๋ถ๋ฅผ ํ์ํฉ๋๋ค.
์์ ์ธ๊ธํ๋ฏ์ด, ๊ธฐ๋ณธ์ ์ผ๋ก ์์ ์ ์ธ ํ์ ์ผ๋ก ๊ฐ์ฃผ๋๋ ๊ฒ์๋ ์์ํ์ , ๋ฌธ์์ด, ํจ์ ํ์ ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ค์ ์กฐ๊ฑด์ ๋ง์กฑ์ํจ๋ค๋ฉด ์ปดํ์ผ๋ฌ๋ ํด๋น ํด๋์ค๋ฅผ ์์ ์ ์ธ ํ์ ์ผ๋ก ์ถ๋ก ํฉ๋๋ค. ์ฐธ๊ณ
- ๋ ์ธ์คํด์ค์
equals
๊ฒฐ๊ณผ๊ฐ ๋์ผํ ๋ ์ธ์คํด์ค์ ๊ฒฝ์ฐ ํญ์ ๋์ผํฉ๋๋ค. - ์ ํ์ ๊ณต๊ฐ ์์ฑ์ด ๋ณ๊ฒฝ๋๋ฉด ์ปดํฌ์ง์ ์ ์๋ฆผ์ด ์ ์ก๋ฉ๋๋ค.
- ๋ชจ๋ ๊ณต๊ฐ ์์ฑ ์ ํ๋ ์์ ์ ์ ๋๋ค.
๋๋ @Stable
, @Immutable
annotation์ ์ฌ์ฉํ๋ฉด ๊ฐ๋ฐ์๊ฐ ์์๋ก ์ด๋ค ํด๋์ค๋ฅผ ์์ ์ ์ธ ํ์
์ผ๋ก ๋ง๋ค ์๋ ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์์ ์ฑ ์กฐ๊ฑด์ ๋ง์ง ์๋ ํด๋์ค์ ์ด๋ฌํ annotation์ ์ฌ์ฉํ ๊ฒฝ์ฐ ์ปดํ์ผ๋ฌ๊ฐ ์ถ๋ก ํ ๋ด์ฉ๊ณผ ๋ฌ๋ผ ์์ํ์ง ๋ชปํ ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก ์ฃผ์ํ์ฌ์ผ ํฉ๋๋ค. ๊ฐ๋ฅํ annotation์ ์ฌ์ฉํ์ง ์๊ณ ์์ ์กฐ๊ฑด์ ์งํค๋๋ก ํ์ฌ ํด๋์ค๋ฅผ ์์ ์ ์ผ๋ก ๋ง๋๋ ๊ฒ์ด ์ข์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ค์ Icon๊ณผ Button ์ฌ๋ก๋ก ๋์๊ฐ ์ ์ฌ๊ตฌ์ฑ์ด ๋ฐ์ํ๋ ๊ฒ์ธ์ง ๋งค๊ฐ๋ณ์๋ก ์ฌ์ฉ๋ ํด๋์ค๋ค์ ์ดํด๋ณด๋ฉฐ ๋ถ์ํด๋ณด๊ฒ ์ต๋๋ค.
์ฒซ ๋ฒ์งธ ์ฌ๋ก์์ Icon ์ปดํฌ์ ๋ธ์ ๋ง๋ค ๋ ImageVector
๋ฅผ ์ฌ์ฉํจ์ผ๋ก์จ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์์ต๋๋ค. ImageVector
์ ๊ฒฝ์ฐ @Immutable
annotation์ด ์ถ๊ฐ๋์ด ๋ช
์์ ์ผ๋ก stableํ ํด๋์ค์ด๊ธฐ ๋๋ฌธ์
๋๋ค.
๋ฐ๋ฉด, Painter
ํด๋์ค์ ๊ฒฝ์ฐ ์์ ์ ์ธ ํด๋์ค๋ก ๋ช
์๋์ง ์์์ต๋๋ค. ์์ ์ ์ธ ํ์
์ด ๋๋ ค๋ฉด ๋ ์ธ์คํด์ค์ equals ๊ฒฐ๊ณผ๋ฅผ ๋น๊ตํด์ผ ํ๋๋ฐ, Bitmap์
equals() ํจ์๋ฅผ ์์ฑํ๋ ๋ฐ ๋๋ ๊ณ์ฐ ๋น์ฉ์ด ๋๊ธฐ ๋๋ฌธ์
๋๋ค. ๋ฐ๋ผ์ Painter
ํด๋์ค๋ฅผ ์ฌ์ฉํ์ ๋ ์๋ํ์ง ์์๋ ์ฌ๊ตฌ์ฑ์ด ๊ณ์ ๋ฐ์ํ๋ ๊ฒ์
๋๋ค.
๋ํ, ๋ ๋ฒ์งธ ์ฌ๋ก์์ ViewModel
ํด๋์ค ์ญ์ ์์ ์กฐ๊ฑด์ ๋ง์กฑํ์ง ์๊ธฐ ๋๋ฌธ์ ์์ ์ ์ธ ํ์
์ด ์๋๋๋ค. @Stable
annotaion์ ์ถ๊ฐํ์ฌ ๊ฐ์ ๋ก ์์ ์ ์ธ ํ์
์ผ๋ก ๋ง๋ค์๋ ์์ง๋ง ๊ทธ๋ ๊ฒ ๋๋ฉด ์๋๋ก์ด๋ ํ๋ ์์ํฌ(Compose)์ ๋ํ ์์กด์ฑ์ด ์๊ธฐ๊ธฐ ๋๋ฌธ์ ์ ์ ํ ํด๊ฒฐ์ฑ
์ด ์๋๋๋ค. ๋ฐ๋ผ์ ViewModel์ ์ฐธ์กฐํ๋ ๋์ ๋ณ๊ฒฝ๋์ง ์๋ ๊ฐ์ ๋๊ฒจ ์ฌ๊ตฌ์ฑ์ด ๋ฐ์ํ์ง ์๋๋ก ํ ์ ์์์ต๋๋ค.
๋ง์น๋ฉฐ
์ด๋ฒ ๊ธ์์๋ Jetpack Compose์์ ๋ฐ์ํ๋ ์ฌ๊ตฌ์ฑ ์ฌ๋ก์ ๊ทธ ํด๊ฒฐ์ฑ ์ ๋ํด ์ดํด๋ณด์์ต๋๋ค. ์ด๋ฅผ ํตํด Stableํ ํ์ ์ ํ์ฉํ์ฌ ์ฌ๊ตฌ์ฑ์ ์ต์ํํ๋ ๊ฒ์ ์ค์์ฑ์ ์ ์ ์์์ต๋๋ค.
์์ง๋ Compose ๋ํด ๊ณต๋ถํ๊ณ ๊ฐ์ ํ ๋ถ๋ถ์ด ๋ง์ง๋ง, ์ ์ธํ UI์ ์ฅ์ ๊ณผ ํฅ์๋ ๊ฐ๋ฐ ์์ฐ์ฑ์ ๊ฒฝํํ๋ฉฐ ์ฆ๊ฒ๊ฒ ๊ฐ๋ฐํ๊ณ ์์ต๋๋ค. ์ด ๊ธ์ด Compose๋ฅผ ์ดํดํ๋๋ฐ ์กฐ๊ธ์ด๋๋ง ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค. ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค.