๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘/Android

[Android] UI architecture - Phase์™€ State

interfacer_han 2025. 2. 27. 12:06

#1 ์ด์ „ ๊ฒŒ์‹œ๊ธ€

 

[Android] UI architecture - Phases

#1 ๊ฐœ์š” Jetpack Compose ๋‹จ๊ณ„  |  Android Developers์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Jetpack Compose ๋‹จ๊ณ„ ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถ„๋ฅ˜

kenel.tistory.com

์œ„ ๊ฒŒ์‹œ๊ธ€์—์„œ ์ด์–ด์ง„๋‹ค.

 

#2 State

 

[Android] Jetpack Compose - State ๊ธฐ์ดˆ

#1 ๊ฐœ์š” ์ƒํƒœ ๋ฐ Jetpack Compose  |  Android Developers์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒํƒœ ๋ฐ Jetpack Compose ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €์žฅํ•˜๊ณ 

kenel.tistory.com

๋จผ์ €, State์— ๋Œ€ํ•ด ์ดํ•ดํ•ด์•ผ ํ•œ๋‹ค. ๋ชจ๋ฅด๋Š” ์‚ฌ๋žŒ์€ ์œ„ ๊ฒŒ์‹œ๊ธ€์„ ์ฝ์ž. ๊ฒŒ์‹œ๊ธ€์„ ์š”์•ฝํ•˜๋ฉด, "Compose Runtime์€ State ๊ฐ’ ๋ณ€ํ™”์— ๋ฐ˜์‘ํ•œ๋‹ค"์ด๋‹ค.

 

#3 State์™€ Phase

#3-1 Phase ์žฌ์‹คํ–‰์€ ๋…๋ฆฝ์ ์ด๋‹ค

Compose์—๋Š” UI ์ƒ์„ฑ์„ ์œ„ํ•œ 3๋‹จ๊ณ„๊ฐ€ ์กด์žฌํ•œ๋‹ค. Layout ๋‹จ๊ณ„์˜ ์ตœ์ดˆ ์‹คํ–‰์€ Composition ๋‹จ๊ณ„๊ฐ€ ๋ฐ˜๋“œ์‹œ ์„ ํ–‰๋˜์–ด์•ผ ํ•˜๋ฉฐ, Drawing ๋‹จ๊ณ„์˜ ์ตœ์ดˆ ์‹คํ–‰์€ Layout ๋‹จ๊ณ„๊ฐ€ ๋ฐ˜๋“œ์‹œ ์„ ํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค. ํ•˜์ง€๋งŒ, ์žฌ์‹คํ–‰์ด๋ผ๋ฉด ์–˜๊ธฐ๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค. ์žฌ์‹คํ–‰ ์‹œ์—๋Š” Layout ๋‹จ๊ณ„๊ฐ€ Composition ๋‹จ๊ณ„์™€ ์ƒ๊ด€์—†์ด ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘๋™ํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ๋ง ๊ทธ๋Œ€๋กœ ์žฌ์‹คํ–‰์ด๊ธฐ์—, Composition ๋‹จ๊ณ„์—์„œ ๋งŒ๋“ค์—ˆ๋˜ UI Tree๋ฅผ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ฐธ๊ณ ๋กœ State๊ฐ€ ์–ด๋””์— ์ €์žฅ๋˜๋Š”๊ฐ€? ๋”ฐ์œ„๋Š” ์ค‘์š”์น˜ ์•Š๋‹ค. ์—ฌ๊ธฐ์—์„œ ์ค‘์š”ํ•œ ๊ฒƒ์€ State์˜ ๊ฐ’์ด ์–ด๋–ค ๋‹จ๊ณ„์—์„œ ์ฝํžˆ๋Š๋ƒ๋‹ค. State์˜ ๊ฐ’์ด ์–ด๋Š ๋‹จ๊ณ„์—์„œ ์ฝํžˆ๋Š๋ƒ์— ๋”ฐ๋ผ, State ์ธ์Šคํ„ด์Šค ๋ณ„๋กœ ํ˜•์„ฑ๋˜๋Š” Restart Scope๋ฅผ ์–ธ์ œ ์žฌ์‹คํ–‰ํ• ์ง€๊ฐ€ ๊ฒฐ์ •๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

#3-2 Restart Scope

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Button(onClick = { count++ }) {
            Text("Increment")
        }

        Text("Current count: $count")
    }
}

Restart Scope๋Š” Compose์—์„œ ํŠน์ • State ๊ฐ’์˜ ๋ณ€๊ฒฝ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ, ์žฌ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ ๋ธ”๋ก์˜ ๋ฒ”์œ„๋‹ค. ์œ„ ์ฝ”๋“œ์—์„œ Text("Current count: $count")๋Š” Stateํ˜• ๋ณ€์ˆ˜์ธ count์— ์˜์กดํ•œ๋‹ค. Button() ์‚ฌ์šฉ์ž๊ฐ€ ํด๋ฆญํ•ด์„œ count ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด, Compose๋Š” Text ์ปดํฌ์ €๋ธ”๋งŒ ๋‹ค์‹œ ์‹คํ–‰ํ•œ๋‹ค. ์ด ๋‹ค์‹œ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ์˜ ๋ฒ”์œ„๋ฅผ Restart Scope๋ผํ•œ๋‹ค. Stateํ˜• ์ธ์Šคํ„ด์Šค๋“ค์€ ๊ฐ ์ธ์Šคํ„ด์Šค๋งˆ๋‹ค ์ž์‹ ๋“ค๋งŒ์˜ Restart Scope๋ฅผ ํ˜•์„ฑํ•œ๋‹ค. ๊ฐ ์ธ์Šคํ„ด์Šค๋Š” (๋‹น์—ฐํžˆ) ์ผ๋ถ€ ํ˜น์€ ์ „๋ถ€๊ฐ€ ๊ฒน์น˜๋Š” Restart Scope๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.

 

์•„๋ž˜์—์„œ ๊ฐ ๋‹จ๊ณ„ ๋ณ„ State์™€์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์„œ์ˆ ํ•œ๋‹ค.

 

#3-3 Composition ๋‹จ๊ณ„

var padding by remember { mutableStateOf(8.dp) }
var anyState by remember { mutableStateOf(0)}
Text(
    text = "Hello",
    // `padding` ์ƒํƒœ๋Š” Modifier๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ Composition ๋‹จ๊ณ„์—์„œ ์ฝํž˜
    // `padding` ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด Composition์ด ์žฌ์‹คํ–‰๋จ
    modifier = Modifier.padding(padding)
)

Composition ๋‹จ๊ณ„๋Š” ํ•œ ๋งˆ๋””๋กœ UI Tree๋ฅผ ๊ทธ๋ฆฌ๋Š” ๊ณผ์ •์ด๋‹ค. UI Tree๋ฅผ ๊ทธ๋ฆด ๋•Œ Compose Runtime์€ UI Tree์— ๊ด€๋ จ๋œ State์˜ ๊ฐ’์„ ์ฝ๊ณ  ์ถ”์  ๋ชฉ๋ก์— ๋„ฃ๋Š”๋‹ค (!= Compose Runtime์ด ๋ฌด์กฐ๊ฑด ๋ฐ˜๋“œ์‹œ ๋ชจ๋“  State๋ฅผ ์ฝ๊ณ  ์ถ”์ ํ•œ๋‹ค). ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ UI Tree์— ๊ด€๋ จ๋œ State๋งŒ์„ ์ฝ๊ณ  ์ถ”์  ๋ชฉ๋ก์— ๋„ฃ๋Š”๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋ฐ˜๋Œ€๋กœ ๋งํ•ด, UI Tree์™€ ๊ด€๋ จ์—†๋Š” State๋Š” Composition ๋‹จ๊ณ„์—์„œ ๋ฌด์‹œ๋œ๋‹ค (= Restart Scope ์ž์ฒด๊ฐ€ ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š์Œ). ๋”ฐ๋ผ์„œ ์œ„ ์ฝ”๋“œ ์† ๋ณ€์ˆ˜ anyState๋Š” ์ ์–ด๋„ Composition ๋‹จ๊ณ„์—์„  ์—†๋Š” ๋ณ€์ˆ˜์ฒ˜๋Ÿผ ์ทจ๊ธ‰๋œ๋‹ค. Composition ๋‹จ๊ณ„์˜ ์ถ”์  ๋ชฉ๋ก์— ๋“ฑ๋ก๋œ State์˜ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด, Composition์ด ์žฌ์‹คํ–‰(Recomposition)๋œ๋‹ค.

 

๋„์‹๋„

https://developer.android.com/develop/ui/compose/layouts/constraints-modifiers#modifiers-ui

Modifier๊นŒ์ง€ ํฌํ•จํ•œ UI ํŠธ๋ฆฌ์˜ ๋ชจ์Šต

 

#3-4 Layout ๋‹จ๊ณ„

var offsetX by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.offset {
        // 'offsetX' ์ƒํƒœ๋Š” Layout ๋‹จ๊ณ„์˜ ๋ฐฐ์น˜ ๋‹จ๊ณ„(Placement)์—์„œ ์ฝํž˜
        // 'offsetX' ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด Layout ๋‹จ๊ณ„๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋จ
        IntOffset(offsetX.roundToPx(), 0)
    }
)

Layout ๋‹จ๊ณ„๋Š” UI Tree์— ๊ธฐ๋ฐ˜ํ•ด (ํฌ๊ธฐ ๋ฐ ์œ„์น˜๋ฅผ) ๊ณ„์‚ฐํ•˜๋Š” ๊ณผ์ •์ด๋‹ค. ์ด ๊ณ„์‚ฐ ๊ณผ์ •์—์„œ Compose Runtime์€ ํฌ๊ธฐ ๋ฐ ์œ„์น˜์— ๊ด€๋ จ๋œ State์˜ ๊ฐ’์„ ์ฝ๊ณ  ์ถ”์  ๋ชฉ๋ก์— ๋„ฃ๋Š”๋‹ค. ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ ํฌ๊ธฐ ๋ฐ ์œ„์น˜์— ๊ด€๋ จ๋œ State๋งŒ์„ ์ฝ๊ณ  ์ถ”์  ๋ชฉ๋ก์— ๋„ฃ๋Š”๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋ฐ˜๋Œ€๋กœ ๋งํ•ด, ํฌ๊ธฐ ๋ฐ ์œ„์น˜์™€ ๊ด€๋ จ์—†๋Š” State๋Š” Layout ๋‹จ๊ณ„์—์„œ ๋ฌด์‹œ๋œ๋‹ค.

 

์˜๋ฌธ์ : padding()๊ณผ offset()์ด ์™œ ์„œ๋กœ ๋‹ค๋ฅธ ๋‹จ๊ณ„์—์„œ ์ถ”์ ๋˜๋Š”๊ฐ€?

๋”๋ณด๊ธฐ

์ž์—ฐํžˆ ์˜๋ฌธ์ด ๋“ ๋‹ค. #3-2์˜ padding() ๋˜ํ•œ ํฌ๊ธฐ ๋ฐ ์œ„์น˜์™€ ๊ด€๋ จ๋œ ๊ฐœ๋…์ด ์•„๋‹ˆ์—ˆ๋Š”๊ฐ€? ์™œ padding()์€ Composition ๋‹จ๊ณ„์—์„œ ์ถ”์ ๋˜๋ฉฐ, offset()์€ Layout ๋‹จ๊ณ„์—์„œ ์ถ”์ ๋˜๋Š”๊ฐ€? ๋‘˜์€ ๋ฌด์Šจ ์ฐจ์ด๊ฐ€ ์žˆ๋Š”๊ฐ€? ๋‘˜์€ '๋ชจ์–‘' ์ด๋ผ๋Š” ์ถ”์ƒ์  ๊ด€๋…์„ ๊ณต์œ ํ•˜์ง€๋งŒ, ๊ทธ ๊ตฌํ˜„ ๋ฐฉ์‹์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. padding()์€ ํ•จ์ˆ˜ ์ž์ฒด๊ฐ€ ์ •์  ๊ฐ’์„ ์ธ์ˆ˜๋กœ ๋ฐ›๊ณ  offset์€ ํ•จ์ˆ˜ ์ž์ฒด๊ฐ€ ๋žŒ๋‹ค ํ•จ์ˆ˜(๋™์  ๊ฐ’)์„ ์ธ์ˆ˜๋กœ ๋ฐ›๋Š”๋‹ค. ์ „์ž๋Š” ๊ณ ์ •๋œ ๊ฐ’์„ Modifier๊ฐ€ ์ €์žฅํ•˜๋Š” ๋ฐ˜๋ฉด ํ›„์ž๋Š” ๋žŒ๋‹ค ์ž์ฒด๋งŒ์„ ์ €์žฅํ•˜์—ฌ Offset ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋กœ์ง๋งŒ ๋‚˜์ค‘์— ์‹คํ–‰๋œ๋‹ค. #3-2์˜ ๋„์‹๋„ ์† Modifier์— padding(start = 8)๊ณผ ๊ฐ™์€ ๊ตฌ์ฒด์  ์ˆ˜์น˜๊ฐ€ ์•„๋‹ˆ๋ผ, offset { IntOffset(offsetX.roundToPx(), 0) }์™€ ๊ฐ™์€ ๋žŒ๋‹ค ํ•จ์ˆ˜๊ฐ€ ๋‹ด๊ฒจ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

 

๊ทธ๋ ‡๋‹ค๋ฉด ์งˆ๋ฌธ์„ ๋ฐ”๊ฟ”๋ณด๊ฒ ๋‹ค. ์™œ padding์€ ์ •์  ๊ฐ’์„ ๋ฐ›๋„๋ก ๋งŒ๋“ค์–ด์ ธ์žˆ๊ณ  offset์€ ๋žŒ๋‹ค ํ•จ์ˆ˜๋ฅผ ์ธ์ˆ˜๋กœ ๋ฐ›๊ฒŒ ๋งŒ๋“ค์–ด์ง„ ๊ฒƒ์ผ๊นŒ? ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ˆ„๊ตฌ๋‚˜ ๊ฐ„๋‹จํ•œ Web ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ํ•ด๋ณธ ์ ์ด ์žˆ์„ ๊ฒƒ์ด๋‹ค. css ์†์„ฑ์—์„œ padding๊ฐ’์„ ์„ค์ •ํ•  ๋•Œ๋ฅผ ์ƒ๊ฐํ•ด๋ณด์ž. padding์„ ์–ด๋–ป๊ฒŒ ์„ค์ •ํ•˜๋Š๋ƒ์— ๋”ฐ๋ผ ๋ถ€๋ชจ ๋ฐ ์ž์‹์˜ ๋ชจ์–‘์ด ๋ณ€ํ•œ๋‹ค. ์ฆ‰, ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ”๊พธ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์–ด๋–ค ์—ฐ์‡„ ๋ฐ˜์‘์„ ์ผ์œผํ‚จ๋‹ค. ์ด๋Š” ์„ค๊ณ„ํ•˜๋Š” ์ž…์žฅ(ํ”„๋กœ๊ทธ๋ž˜๋จธ ์ž…์žฅ)์—์„œ ๋ˆˆ์Œ€์„ ์ฐŒํ‘ธ๋ฆฌ๊ฒŒ ๋งŒ๋“œ๋Š” ๋ถˆํ™•์‹คํ•จ์„ ์ž์•„๋‚ธ๋‹ค. ๋ฐ˜๋ฉด offset์€ ์ž๊ธฐ ์ž์‹ ์˜ ์œ„์น˜๋งŒ์„ ๋ณ€๊ฒฝํ•œ๋‹ค. ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ”๊พธ๊ธฐ์— ์•ˆ์ „ํ•˜๋‹ค. ์‹ค์ œ๋กœ Jetpack Compose์—์„œ offset๊ฐ’์„ ์ด์ƒํ•˜๊ฒŒ ์„ค์ •ํ•˜๋ฉด ๋ถ€๋ชจ์˜ ์˜์—ญ์„ ๋šซ๊ณ  ๋‚˜๊ฐ€๋ฒ„๋ฆฐ๋‹ค๋“ ๊ฐ€ ํ•˜๋Š” ์ผ์ด ์ƒ๊ธด๋‹ค. ์ž๊ธฐ ์ž์‹ ์˜ ๋ชจ์–‘๋งŒ ๋ฐ”๊พธ๋‹ˆ๊นŒ ๊ทธ๋ ‡๋‹ค. ์ •๋ฆฌํ•˜๋ฉด, ์šฐ๋ฆฌ(๊ฐœ๋ฐœ์ž)๊ฐ€ ๊ฐœ๋ฐœํ•˜๊ธฐ ํŽธํ•˜๊ฒŒ Google์ด ์˜๋„์ ์œผ๋กœ ๊ตฌ๋ถ„ํ•ด ๋†“์€ ๊ฒƒ์ด๋‹ค.

 

Layout ๋‹จ๊ณ„๋Š” ์ธก์ •(Measurement) ๋ฐ ๋ฐฐ์น˜(Placement)๋ผ๋Š” ๋‘ ํ•˜์œ„ ๋‹จ๊ณ„๋ฅผ ๊ฐ€์ง„๋‹ค.

 

#3-5 Layout - Measurement

์—ฌ๊ธฐ์„œ ์„ค๋ช…ํ•˜๋Š” ๊ฑด ํ˜ผ๋ž€๋งŒ ๊ฐ€์ค‘์‹œํ‚จ๋‹ค. ์ด ๊ฒŒ์‹œ๊ธ€์—์„œ ์‹ค์ œ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ๋ด์•ผํ•œ๋‹ค. ์ € ๊ฒŒ์‹œ๊ธ€์— ๋‚˜์˜ค๋Š” measurable.measure()๊ฐ€ Measurement ๋‹จ๊ณ„์— ํ•ด๋‹นํ•œ๋‹ค.

 

#3-6 Layout - Placement

์—ฌ๊ธฐ์„œ ์„ค๋ช…ํ•˜๋Š” ๊ฑด ํ˜ผ๋ž€๋งŒ ๊ฐ€์ค‘์‹œํ‚จ๋‹ค. ์ด ๊ฒŒ์‹œ๊ธ€์—์„œ ์‹ค์ œ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ๋ด์•ผํ•œ๋‹ค. ์ € ๊ฒŒ์‹œ๊ธ€์— ๋‚˜์˜ค๋Š” MeasureScope.layout()์ด Placement ๋‹จ๊ณ„์— ํ•ด๋‹นํ•œ๋‹ค.

 

#3-7 Drawing ๋‹จ๊ณ„

var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
    // `color` ์ƒํƒœ๋Š” Drawing ๋‹จ๊ณ„์—์„œ ์ฝํž˜
    // `color` ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด Drawing ๋‹จ๊ณ„๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋จ
    drawRect(color)
}

Drawing ๋‹จ๊ณ„๋Š” ์‹ค์ œ ํ™”๋ฉด์— ๊ทธ๋ž˜ํ”ฝ์„ ํ‘œ์‹œํ•˜๋Š” ๊ณผ์ •์ด๋‹ค. ์ด ๊ณ„์‚ฐ ๊ณผ์ •์—์„œ Compose Runtime์€ ๊ทธ๋ž˜ํ”ฝ์— ๊ด€๋ จ๋œ State์˜ ๊ฐ’์„ ์ฝ๊ณ  ์ถ”์  ๋ชฉ๋ก์— ๋„ฃ๋Š”๋‹ค. ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ ๊ทธ๋ž˜ํ”ฝ์— ๊ด€๋ จ๋œ State๋งŒ์„ ์ฝ๊ณ  ์ถ”์  ๋ชฉ๋ก์— ๋„ฃ๋Š”๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋ฐ˜๋Œ€๋กœ ๋งํ•ด, ๊ทธ๋ž˜ํ”ฝ๊ณผ ๊ด€๋ จ์—†๋Š” State๋Š” Drawing ๋‹จ๊ณ„์—์„œ ๋ฌด์‹œ๋œ๋‹ค.

 

#4 State์˜ '์ข‹์€' ์œ„์น˜

#4-1 2๊ฐ€์ง€ Modifier.offset()

ํ›„์ˆ ํ•  ์ฝ”๋“œ ์„ค๋ช…์„ ์œ„ํ•ด ๋จผ์ € ๋ฉ”์†Œ๋“œ ์˜ค๋ฒ„๋กœ๋”ฉ๋œ Modifier.offset()์˜ 2๊ฐ€์ง€ ํ˜•ํƒœ๋ฅผ ๋จผ์ € ์•Œ์•„๋ณธ๋‹ค.

 

// https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).offset(androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp)

fun Modifier.offset(x: Dp = 0.dp, y: Dp = 0.dp): Modifier

#3-2์˜ padding()์ฒ˜๋Ÿผ ์ •์ ์ธ ์ •๋ณด๋ฅผ ๋‹ด๋Š”๋‹ค.

 

// https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).offset(kotlin.Function1)

fun Modifier.offset(offset: Density.() -> IntOffset): Modifier

#3-3์—์„œ ์“ฐ์ธ offset()์œผ๋กœ, ๋™์ ์ธ ์ •๋ณด(๋žŒ๋‹ค ํ•จ์ˆ˜)๋ฅผ ๋‹ด๋Š”๋‹ค.

 

#4-2 State์˜ ๋‚˜์œ ์œ„์น˜

Box {
    val density = LocalDensity.current
    val listState = rememberLazyListState()

    Image(
        // ...
        // ๋น„ํšจ์œจ์ ์ธ ๊ตฌํ˜„ ๋ฐฉ์‹!
        Modifier.offset(
            // Composition ์ค‘ firstVisibleItemScrollOffset ์ƒํƒœ ์ฝ๊ธฐ
            (listState.firstVisibleItemScrollOffset / 2).toDp(density)
        )
    )

    LazyColumn(state = listState) {
        // ...
    }
}

์ •์ ์ธ ์ •๋ณด๋ฅผ ๋‹ด๋Š” Modifier.offset()์ด ์“ฐ์ธ ์ฝ”๋“œ๋‹ค. ์ž‘๋™์€ ํ•˜์ง€๋งŒ ์„ฑ๋Šฅ์ƒ์œผ๋ก  ์ข‹์ง€ ์•Š์€ ์ฝ”๋“œ๋‹ค. ์™œ๋ƒํ•˜๋ฉด Composition ๋‹จ๊ณ„์—์„œ ์ฝํžŒ listState.firstVisibleItemScrollOffset์˜ ๊ฐ’์ด ์Šคํฌ๋กค ํ•  ๋•Œ๋งˆ๋‹ค ๋ณ€๋™๋˜๊ธฐ์—, Composition ๋‹จ๊ณ„๊ฐ€ ๋„ˆ๋ฌด ์ž์ฃผ ์žฌ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

#4-3 ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        Modifier.offset {
            // Layout ๋‹จ๊ณ„์—์„œ firstVisibleItemScrollOffset ์ƒํƒœ ์ฝ๊ธฐ
            IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2)
        }
    )

    LazyColumn(state = listState) {
        // ...
    }
}

๋™์ ์ธ ์ •๋ณด๋ฅผ ๋‹ด๋Š” Modifier.offset()์œผ๋กœ ๋ฐ”๊ฟจ๋‹ค. ์ด๋Ÿฌ๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กค์„ ํ•  ๋•Œ๋งˆ๋‹ค, Composition ๋‹จ๊ณ„ ๋Œ€์‹  Layout ๋‹จ๊ณ„๊ฐ€ ์žฌ์‹คํ–‰๋  ๊ฒƒ์ด๋‹ค. ์œ„์—์„œ ๋งํ–ˆ๋“ฏ ๊ฐ Phase์˜ ์žฌ์‹คํ–‰์€ ์„œ๋กœ ๋…๋ฆฝ์ ์ด๊ธฐ์—, ๊ฐ€๋ น Composition ๋‹จ๊ณ„ ์žฌ์‹คํ–‰์ด ๊ผญ layout ๋‹จ๊ณ„ ์žฌ์‹คํ–‰์„ ์œ ๋ฐœํ•˜์ง„ ์•Š๋Š”๋‹ค. ํ•˜์ง€๋งŒ #4-2์˜ ์ฝ”๋“œ์—์„œ๋Š” listState.firstVisibleItemScrollOffset์˜ ๋ณ€ํ™”๊ฐ€ Image ๋ฐ LazyColumn์˜ Layout ๋‹จ๊ณ„ ์žฌ์‹คํ–‰์„ ์œ ๋ฐœํ•œ๋‹ค. ์ •๋ฆฌํ•˜๋ฉด Composition์ด ๊ตณ์ด ์žฌ์‹คํ–‰๋  ํ•„์š”๊ฐ€ ์—†๋Š” ๊ตฌ์กฐ์ด๊ธฐ์—, Layout๋งŒ ์žฌ์‹คํ–‰๋˜๋„๋ก ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•œ ๊ฒƒ์ด๋‹ค.

 

#4-4 ์ผ๋ฐ˜ํ™”

#4-2์˜ ์ฝ”๋“œ๋ฅผ #4-3์˜ ์ฝ”๋“œ๋กœ ๋ณ€๊ฒฝํ•œ ๊ฒƒ์€ ํ•˜๋‚˜์˜ ์˜ˆ์‹œ์— ๋ถˆ๊ณผํ•˜๋‹ค. ์ด๋ฅผ ์ผ๋ฐ˜ํ™”ํ•ด ์„ค๋ช…ํ•˜๋ฉด, State๊ฐ€ ์ฝํžˆ๋Š” ๋‹จ๊ณ„๊ฐ€ ์ตœ๋Œ€ํ•œ ๋‚ฎ์€ ๋‹จ๊ณ„๊ฐ€ ๋˜๊ฒŒ๋” ๋กœ์ปฌํ™”(localize)ํ•˜์—ฌ Compose Runtime๊ฐ€ ์“ธ๋ฐ์—†๋Š” ์ผ์„ ํ•˜์ง€ ์•Š๋„๋ก ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค. ๋˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” Side-Efftects ์ค‘ ํ•˜๋‚˜์ธ derivedStateOf๋ฅผ ์ด์šฉํ•ด์„œ, ์—ฌ๋Ÿฌ State ์ธ์Šคํ„ด์Šค ๊ฐ’์ด ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ๋ฌด์ˆ˜ํžˆ ๋งŽ์€ ์กฐํ•ฉ์„ ๋ช‡ ๊ฐ€์ง€์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ์ค„์—ฌ Composition ์žฌ์‹คํ–‰ ํšŸ์ˆ˜๋ฅผ ์ค„์ผ ์ˆ˜๋„ ์žˆ๋‹ค.

 

#5 ์ˆœํ™˜ ์ฐธ์กฐ ํ”ผํ•˜๊ธฐ

#5-1 ์•ˆ ์ข‹์€ ์ฝ”๋“œ

Box {
    var imageHeightPx by remember { mutableStateOf(0) }
    val density = LocalDensity.current.density

    Image(
        painter = painterResource(R.drawable.rectangle),
        contentDescription = "I'm above the text",
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                imageHeightPx = size.height // ์•ˆ ์ข‹์€ ์ฝ”๋“œ!
            }
    )

    Text(
        text = "I'm below the image",
        modifier = Modifier.padding(
            top = (imageHeightPx / density).dp
        )
    )
}

์œ„๋Š” ์ด๋ฏธ์ง€๊ฐ€ ์œ„์—, ํ…์ŠคํŠธ๊ฐ€ ์•„๋ž˜์— ๋ฐฐ์น˜๋œ ํ˜•ํƒœ์˜ ์ˆ˜์ง Column์„ ์ž˜๋ชป ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ๋‹ค. ์ด ์ฝ”๋“œ์˜ ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ๋Š” "๋‹จ์ผ Frame๋งŒ์œผ๋กœ ์ตœ์ข… Layout์ด ์™„์„ฑ๋˜์ง€ ์•Š๋Š”๋‹ค"๋Š” ๊ฒƒ์ด๋‹ค.

 

#5-2 ๋ฌธ์ œ ๋ถ„์„

https://developer.android.com/develop/ui/compose/phases#recomp-loop

imageHeightPx๋Š” onSizeChaged()์—์„œ๋„ ์“ฐ์ด๊ณ  padding()์—์„œ๋„ ์“ฐ์ธ๋‹ค. ์ „์ž์—์„œ ์ฝํž ๋• Layout ๋‹จ๊ณ„์—์„œ ์ถ”์ ๋˜๊ณ , ํ›„์ž์—์„œ ์ฝํž ๋• Composition ๋‹จ๊ณ„์—์„œ ์ถ”์ ๋œ๋‹ค. ๋•Œ๋ฌธ์—, imageHeightPx์˜ ๊ฐ’์ด Layout ๋‹จ๊ณ„์—์„œ ๋ณ€ํ•˜๋ฉด ๊ทธ ๋‹ค์Œ ํ”„๋ ˆ์ž„์—์„œ Composition์ด ์žฌ์‹คํ–‰๋œ๋‹ค. ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ ์˜๋„ํ•œ ํ™”๋ฉด์„ ์œ„ํ•ด์„œ ์ด 2ํ”„๋ ˆ์ž„์ด ์š”๊ตฌ๋˜๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋Ÿฌ๋ฉด ํ™”๋ฉด์ด ๋š๋š ๋Š์–ด์ ธ์„œ ๋ณด์ผ ๊ฒƒ์ด๋‹ค. ์ด ๋ถ€์ •์  ํŒจํ„ด์„ Cyclic Phase Dependency (๋‹จ๊ณ„ ๊ฐ„ ์ˆœํ™˜ ์ฐธ์กฐ)๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ๋ถ€๋ฅธ๋‹ค.

 

#5-4 ํ•ด๊ฒฐ

Column()์„ ์ด์šฉํ•˜๊ธฐ

Column {
    Image(
        painter = painterResource(R.drawable.rectangle),
        contentDescription = "I'm above the text",
        modifier = Modifier.fillMaxWidth()
    )

    Text(
        text = "I'm below the image",
        modifier = Modifier.padding(top = 16.dp)
    )
}

 

Jetpack Compose์˜ ๊ธฐ๋ณธ ์ปจํ…Œ์ด๋„ˆ์ธ Column()์„ ์‚ฌ์šฉํ•œ ๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ํ•ด๊ฒฐ๋ฒ•. 

 

SubcomposeLayout() ์ด์šฉํ•˜๊ธฐ

@Composable
fun CustomLayoutWithImageAndText() {
    Layout(
        content = {
            Image(
                painter = painterResource(R.drawable.rectangle),
                contentDescription = "I'm above the text"
            )
            Text(text = "I'm below the image")
        }
    ) { measurables, constraints ->
        // ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ธก์ •
        val imagePlaceable = measurables[0].measure(constraints)
        val textPlaceable = measurables[1].measure(constraints)

        // ์ „์ฒด ๋ ˆ์ด์•„์›ƒ ํฌ๊ธฐ ๊ณ„์‚ฐ
        val layoutHeight = imagePlaceable.height + textPlaceable.height

        layout(constraints.maxWidth, layoutHeight) {
            // ์ด๋ฏธ์ง€ ๋ฐฐ์น˜
            imagePlaceable.place(x = 0, y = 0)
            // ํ…์ŠคํŠธ ๋ฐฐ์น˜ (์ด๋ฏธ์ง€ ์•„๋ž˜)
            textPlaceable.place(x = 0, y = imagePlaceable.height)
        }
    }
}

๋‹จ์ผ ์ง„์‹ค ์†Œ์Šค(Single Source of Truth) ์›์น™์„ ์ค€์ˆ˜ํ•ด์„œ State์˜ ๊ฐ’์˜ ๊ด€๋ฆฌ๊ฐ€ (Image ๋ฐ Text์—์„œ) ์ค‘๋ณต๋˜์ง€ ์•Š๋„๋ก ๋งŒ๋“ ๋‹ค.

 

์žฅ์ :
SubcomposeLayout์€ ๋ ˆ์ด์•„์›ƒ ๊ณ„์‚ฐ ๋‹จ๊ณ„๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ์ ํ”„ ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
ํ…์ŠคํŠธ๊ฐ€ ์ด๋ฏธ์ง€์˜ ๋†’์ด์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋”ฐ๋ผ๊ฐ€๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ๋” ๋ณต์žกํ•œ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„ ์‚ฌ์šฉ ์‚ฌ๋ก€์—์„œ๋Š” **์‚ฌ์šฉ์ž ์ •์˜ ๋ ˆ์ด์•„์›ƒ(custom layout)**์„ ์ž‘์„ฑํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ •์˜ ๋ ˆ์ด์•„์›ƒ ์ž‘์„ฑ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ Custom layouts ๊ฐ€์ด๋“œ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.

 

์œ„ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜์ž๋ฉด, Compose์—์„œ ๋‹ค์Œ ๋‹จ๊ณ„์— ์˜์กดํ•˜๋Š” ์ˆœํ™˜์ ์ธ ์ƒํƒœ ํ๋ฆ„์„ ๋„์ž…ํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ ์ ˆํ•œ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ™œ์šฉํ•˜๊ณ , ํ•„์š”ํ•˜๋‹ค๋ฉด ์‚ฌ์šฉ์ž ์ •์˜ ๋ ˆ์ด์•„์›ƒ์„ ๋งŒ๋“ค์–ด ๋™์ž‘์„ ์ œ์–ดํ•จ์œผ๋กœ์จ ์žฌ๊ตฌ์„ฑ ๋ฃจํ”„ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

'๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘ > Android' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Android] App layout - Custom layouts  (0) 2025.02.27
[Android] UI architecture - Phases  (0) 2025.02.27
[Android] App layout - ๊ธฐ์ดˆ  (0) 2025.02.27
[Android] Pointer input - Nested Scroll  (0) 2025.02.18
[Android] Pointer input - Scroll  (0) 2025.02.17