๐Ÿ“ŒJetpack Compose์—์„œ Callback ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ์™€ ์žฅ์ 

LoyalFlowerยท2025๋…„ 3์›” 7์ผ

โœ… 0. ์‚ฌ์ „ ์ •๋ณด

Callback ํ•จ์ˆ˜๋ž€?

Callback ํ•จ์ˆ˜๋Š” ํŠน์ • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜๋กœ, ๋ณดํ†ต ๊ณ ์ฐจ ํ•จ์ˆ˜(higher-order function) ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. Jetpack Compose์—์„œ๋Š” ์ƒํƒœ(state) ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด ์ฝœ๋ฐฑ์„ ์ž์ฃผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.


Callback์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ 

Jetpack Compose์—์„œ ์ปดํฌ์ €๋ธ”(Composable) ํ•จ์ˆ˜๋Š” ์ƒํƒœ(state)๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ , ์ƒ์œ„ ๊ณ„์ธต(๋ถ€๋ชจ)์—์„œ ์ „๋‹ฌ๋ฐ›์€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ(Callback)๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„(Unidirectional Data Flow, UDF) ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค.


๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด๋ž€?

๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด๋ž€, ์•ฑ์˜ ์ƒํƒœ(State)๊ฐ€ ์˜ค์ง ํ•œ ๋ฐฉํ–ฅ์œผ๋กœ๋งŒ ๋ณ€๊ฒฝ๋˜๋„๋ก ๊ด€๋ฆฌํ•˜๋Š” ํŒจํ„ด์ž…๋‹ˆ๋‹ค.
์ฆ‰, ๋ฐ์ดํ„ฐ๋Š” ์œ„(๋ถ€๋ชจ)์—์„œ ์•„๋ž˜(์ž์‹)๋กœ ํ๋ฅด๊ณ , ์ด๋ฒคํŠธ๋Š” ์•„๋ž˜(์ž์‹)์—์„œ ์œ„(๋ถ€๋ชจ)๋กœ ํ๋ฅด๋Š” ๊ตฌ์กฐ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

โœ… 1. ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์œ ์ง€

Jetpack Compose๋Š” ์ƒํƒœ(state)์™€ UI๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ์›์น™์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. UI ์š”์†Œ๊ฐ€ ์ง์ ‘ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ต๊ณ , ์˜ˆ์ธก์ด ํž˜๋“ค์–ด์ง‘๋‹ˆ๋‹ค. Callback์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ƒํƒœ๋ฅผ ์ƒ์œ„ ๊ณ„์ธต์—์„œ๋งŒ ๋ณ€๊ฒฝํ•˜๋„๋ก ๊ฐ•์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”น ์˜ˆ์‹œ: ์ž˜๋ชป๋œ ๋ฐฉ์‹ (UI์—์„œ ์ƒํƒœ ์ง์ ‘ ๋ณ€๊ฒฝ)

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

    Column {
        Text(text = "Count: $count")
        Button(onClick = { count++ }) {  // UI์—์„œ ์ง์ ‘ ์ƒํƒœ ๋ณ€๊ฒฝ โŒ
            Text("Increase")
        }
    }
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด UI๊ฐ€ ์ƒํƒœ๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด ๊นจ์ง‘๋‹ˆ๋‹ค.

๐Ÿ”น ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ์‹: Callback์„ ์‚ฌ์šฉํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ

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

    Counter(count = count, onIncrement = { count++ })  // Callback ์‚ฌ์šฉ โญ•
}

@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
    Column {
        Text(text = "Count: $count")
        Button(onClick = onIncrement) { // ์ƒํƒœ ๋ณ€๊ฒฝ์„ ๋ถ€๋ชจ์—๊ฒŒ ์œ„์ž„
            Text("Increase")
        }
    }
}

โœ… ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด CounterScreen์ด ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , Counter ์ปดํฌ์ €๋ธ”์€ UI ์—ญํ• ๋งŒ ๋‹ด๋‹นํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ… 2. ์žฌ์‚ฌ์šฉ์„ฑ & ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ

Callback์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํฌ์ €๋ธ”์„ ๋” ๋ชจ๋“ˆํ™”ํ•˜๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, ์ƒํƒœ๋ฅผ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›์€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๐Ÿ”น ์˜ˆ์‹œ: ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ

@Composable
fun CustomButton(label: String, onClick: () -> Unit) {
    Button(onClick = onClick) {  // ์ด๋ฒคํŠธ๋ฅผ ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›์Œ
        Text(text = label)
    }
}

@Composable
fun Screen() {
    Column {
        CustomButton(label = "Click Me", onClick = { println("Button Clicked!") })
        CustomButton(label = "Delete", onClick = { println("Item Deleted!") })
    }
}

โœ… CustomButton์€ ๋ฒ„ํŠผ์„ ๊ทธ๋ฆฌ๊ธฐ๋งŒ ํ•˜๊ณ , ์‹ค์ œ ๋™์ž‘์€ ํ˜ธ์ถœํ•˜๋Š” ๊ณณ์—์„œ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ์–ด ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋†’์•„์ง‘๋‹ˆ๋‹ค.


โœ… 3. ํ…Œ์ŠคํŠธ๊ฐ€ ์šฉ์ดํ•ด์ง

Callback์„ ์‚ฌ์šฉํ•˜๋ฉด ํŠน์ • UI ์š”์†Œ๊ฐ€ ์‹คํ–‰๋  ๋•Œ ์–ด๋–ค ๋™์ž‘์ด ์ˆ˜ํ–‰๋˜๋Š”์ง€ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. UI๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ, ์‹ค์ œ ์ƒํƒœ ๋ณ€๊ฒฝ ์—†์ด ํ•จ์ˆ˜ ํ˜ธ์ถœ ์—ฌ๋ถ€๋งŒ ํ™•์ธํ•˜๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๐Ÿ”น ์˜ˆ์‹œ: UI ํ…Œ์ŠคํŠธ

@Test
fun testButtonClick() {
    var clicked = false
    val onClick = { clicked = true }

    composeTestRule.setContent {
        CustomButton(label = "Test", onClick = onClick)
    }

    composeTestRule.onNodeWithText("Test").performClick()
    assertTrue(clicked)  // Callback์ด ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
}

โœ… Callback์„ ์‚ฌ์šฉํ•˜๋ฉด UI๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‹ค์ œ ์ƒํƒœ ๋ณ€๊ฒฝ ์—†์ด ์ด๋ฒคํŠธ ๋™์ž‘๋งŒ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ… 4. Compose์˜ Recomposing ์ตœ์ ํ™”

Jetpack Compose๋Š” ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ์ž๋™์œผ๋กœ ๋ฆฌ์ปดํฌ์ง€์…˜(recomposition) ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ƒํƒœ๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ถˆํ•„์š”ํ•œ UI ๋ฆฌ์ปดํฌ์ง€์…˜์ด ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ํฝ๋‹ˆ๋‹ค. Callback์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ถ€๋ชจ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ๋ฆฌ์ปดํฌ์ง€์…˜์„ ์œ ๋„ํ•˜๋ฏ€๋กœ ์„ฑ๋Šฅ์ด ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”น ์˜ˆ์‹œ: ์„ฑ๋Šฅ ์ตœ์ ํ™”

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

    Counter(count, onIncrement = { count++ })
}

@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
    Column {
        Text("Count: $count") // count๋งŒ ๋ณ€๊ฒฝ๋  ๋•Œ ๋ฆฌ์ปดํฌ์ง€์…˜๋จ
        Button(onClick = onIncrement) {
            Text("Increase")
        }
    }
}

โœ… Counter ์ž์ฒด๋Š” ์ƒํƒœ๋ฅผ ๊ฐ€์ง€์ง€ ์•Š์œผ๋ฏ€๋กœ, ํ•„์š”ํ•  ๋•Œ๋งŒ ๋ฆฌ์ปดํฌ์ง€์…˜์ด ์ผ์–ด๋‚˜ ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.


๐Ÿš€ ์ •๋ฆฌ: Callback ํ•จ์ˆ˜์˜ ์žฅ์ 

์žฅ์ ์„ค๋ช…
โœ… ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์œ ์ง€UI์—์„œ ์ง์ ‘ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ , ๋ถ€๋ชจ๊ฐ€ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋„๋ก ๊ฐ•์ œํ•จ
โœ… ์ปดํฌ๋„ŒํŠธ์˜ ์žฌ์‚ฌ์šฉ์„ฑ ์ฆ๊ฐ€UI์™€ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๋‹ค์–‘ํ•œ ๊ณณ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ
โœ… ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์‹ค์ œ ์ƒํƒœ ๋ณ€๊ฒฝ ์—†์ด ํ•จ์ˆ˜ ํ˜ธ์ถœ ์—ฌ๋ถ€๋งŒ ํ™•์ธ ๊ฐ€๋Šฅ
โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ถˆํ•„์š”ํ•œ ๋ฆฌ์ปดํฌ์ง€์…˜์„ ๋ฐฉ์ง€ํ•˜๊ณ  ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ๋ฆฌ๋ Œ๋”๋ง

๐Ÿ’ก ๊ฒฐ๋ก 

Jetpack Compose์—์„œ Callback์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋‹จ์ˆœํ•œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ๋„˜์–ด์„œ, ๊ตฌ์กฐ์ ์œผ๋กœ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ํ•„์ˆ˜์ ์ธ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ํŠนํžˆ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„(UDF)์„ ์œ ์ง€ํ•˜๊ณ , ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉด์„œ๋„ ์„ฑ๋Šฅ์ด ์ตœ์ ํ™”๋œ UI๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

profile
๋А๋‚€ ์ ์„ ์ ์–ด๋‚˜๊ฐ€๋ฉฐ ์„ฑ์žฅํ•˜๋Š” ๋ธ”๋กœ๊ทธ

0๊ฐœ์˜ ๋Œ“๊ธ€