Introduce Android Jetpack Compose ๐Ÿš€

Imagem de capa

Jetpack Compose๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

Compose๋Š” Native UI๋ฅผ ์ฝ”๋“œ๋ ˆ๋ฒจ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ์‹  ํˆดํ‚ท์ด๋‹ค. Compose๋ฅผ ์ด์šฉํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์žฅ์ ์ด ์žˆ๋‹ค.

image

์ถœ์ฒ˜ Android Developers#Jetpack Compse

Jetpack Compose์„ ์‚ฌ์šฉํ•ด์•ผํ•˜๋Š” ๊ฐœ์ธ์ ์ธ ์ด์œ 

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

์ด๋Ÿฐ๋ฌธ์ œ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ํ•ด๊ฒฐํ•ด์ค„ ์ˆ˜ ์žˆ๋Š” Toolkit์ธ Compose๊ฐ€ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์„ ์–ธํ˜• UI Builder ํŒจํ„ด๊ณผ ํ•จ๊ป˜ ์šฐ๋ฆฌ์•ž์— ์„ ๋ณด์ด๊ฒŒ ๋˜์—ˆ๋Š”๋ฐ, ์ฝ”๋“œ๋งŒ ๋ณด๋ฉด ํ™”๋ฉด์˜ ์ƒํƒœModel ์— ๋”ฐ๋ผ UI์˜ ์ƒํƒœ๋ฅผ ๊ฒฐ์ •์ง“๊ธฐ ๋–„๋ฌธ์—, ๋” ์ด์ƒ UI ์ƒํƒœ๋ฅผ ๋ณด๊ณ  UI๋กœ์ง์„ ๊ตฌ์„ฑํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค๋ผ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

๋˜ํ•œ, Gof์˜ State ํŒจํ„ด๊ณผ ๊ฐ™์ด ํ™”๋ฉด์˜ ์ƒํƒœ๋ฅผ ๋‹จ์ผ๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋‹จ์ด ์žˆ๋‹ค๋ฉด, ํ™”๋ฉด์„ ์ œ์–ดํ•  ๋•Œ ๋”ํ• ๋‚˜์œ„์—†์ด ์‰ฌ์šด๋ฐฉ๋ฒ•์œผ๋กœ ๊ฐœ๋ฐœ์„ ๊ฒฝํ—˜ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ ๋˜ํ•œ ์žฅ์ ์ด๋‹ค.

์ผ๋‹จ ์ฝ”๋“œ๋žฉ์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๊ณ  ํ•œ๋ฒˆ ์–ด๋–ค ๋Š๋‚Œ์ธ์ง€ ๋ณด๋„๋กํ•˜์ž. Compose CodeLab

Codelab ํ•˜๋‚˜์”ฉ ์ •๋ฆฌํ•ด๋ณด๊ธฐ - Jetpack Compose basics

์œ„ ์ฝ”๋“œ์—์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ OverView๋ฅผ ๋ณธ ๊ฒƒ์— ๋”ํ•ด, ์‚ฌ์šฉ๋ฒ•์„ ์ตํ˜€๋ณด์ž. ์šฐ๋ฆฌ๋Š” ๋„ˆ๋ฌด ๋งŽ์€๊ฒƒ์„ ์ตํ˜€๋ณด๊ธฐ์—” ์‹œ๊ฐ„์ด ๋ถ€์กฑํ•˜๋ฏ€๋กœ, ๊ฐ„๋‹จํ•˜๊ฒŒ 1 ~ 5๊นŒ์ง€ ํ•จ๊ป˜ ๋ณด๋„๋กํ•ด๋ณด์ž.

์ƒˆ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ - Empty Compose Activity ์„ ํƒ

ํ”„๋กœ์ ํŠธ ์„ธํŒ…

์„ ํƒ ์ดํ›„ Next๋ฅผ ํด๋ฆญํ•˜๊ณ , Compose๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ์†Œ API ๋ ˆ๋ฒจ์ธ 21์„ ์„ ํƒํ•ด์•ผํ•œ๋‹ค.

ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด app/build.gradle์— ์˜์กด์„ฑ ์„ค์ • ๋ฐ ์ถ”๊ฐ€๊ฐ€ ๋˜์–ด ์žˆ๋Š”๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
}

dependencies {
    ...
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.activity:activity-compose:1.3.0-alpha03"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    ...
}

์ด์ œ MainActivity.kt๋ฅผ ๋ณด์ž.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greeting("Android")
    }
}

๊ทธ๋Ÿฌ๋ฉด ์•„๊นŒ ๋ณด์•˜๋˜ ์ฝ”๋“œ๋ฅผ ๋ณด์•˜์„ ๋•Œ, ์ด ์„ธ๊ฐ€์ง€์˜ ์š”์†Œ๋ฅผ ๊ฐ€์ง€๋Š” ๊ฒƒ์œผ๋กœ ์ •๋ฆฌํ•ด๋ณผ ์ˆ˜ ์žˆ๊ฒ ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ์šฐ๋ฆฌ๊ฐ€ ์•„๋Š” Activity์˜ ๋ผ์ดํ”„์‚ฌ์ดํด ์ฝœ๋ฐฑ onCreate()์—์„œ setContentView(Int) ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋˜๊ฒƒ์ด setContent() ํ•จ์ˆ˜๋กœ ๋ฐ”๋€๊ฒƒ์ด ๊ฐ€์žฅ ํฐ ํŠน์ง•์œผ๋กœ ๋ณด์—ฌ์ง„๋‹ค.

๊ทธ๋ฆฌ๊ณ  ํ•˜๋‹จ์— @Composable ์ด๋ผ๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ณด์ด๊ณ , @Preview ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ณด์ด๋Š”๋ฐ, ์ด ๋‘๊ฐ€์ง€๋ฅผ ํ†ตํ•ด ์šฐ๋ฆฌ๋Š” ์œ ์ถ”ํ•ด๋ณผ ์ˆ˜ ์žˆ๋Š” ๋‹จ์–ด์˜ ์˜๋ฏธ๊ฐ€ ์žˆ๋‹ค.

์ž, ๊ทธ๋Ÿฌ๋ฉด ํ•˜๋‚˜ํ•˜๋‚˜์”ฉ ์ฝ”๋“œ๋ฅผ ๋ณด์ž.

Composable Function

Composable Function์€ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•œ ๊ธฐ์ˆ ์ด๋‹ค. ํ•จ์ˆ˜์œ„์— @Composable ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ด๊ฒŒ ๋˜๋ฉด ํ•จ์ˆ˜ ์•ˆ ๋‹ค๋ฅธ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ๋œ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ๋ณด์ž.

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

๋‹จ์ˆœํ•˜๊ฒŒ ๋‚ด๋ถ€์—๋Š” Text๋ผ๋Š” ํ•จ์ˆ˜๊ฐ€ ์กด์žฌํ•˜๋Š”๋ฐ, ์ด๋ฅผ ํ†ตํ•ด UI๊ณ„์ธต ๋ณ„ ์š”๊ตฌํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ค€๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ณด์ด๋Š” text ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋‚ด๋ถ€ ์†์„ฑ์—์„œ ๋ฐ›๋Š” ์ผ๋ถ€ ์ค‘ ํ•˜๋‚˜์ด๋‹ค.

TextView ๋งŒ๋“ค๊ธฐ

์œ„ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰์‹œ์ผœ๋ณด๋ฉด ๋‹น์—ฐํ•˜๊ฒŒ๋„ Hello๋กœ ์‹œ์ž‘ํ•˜๋Š” TextView๊ฐ€ ํ™”๋ฉด์— ๊ทธ๋ ค์งˆ๊ฒƒ์„ ์•”์‹œํ•œ๋‹ค.

setContent {
  BasicsCodelabTheme {
    // A surface container using the 'background' color from the theme
    Surface(color = MaterialTheme.colors.background) {
      Greeting("Android")
    }
  }
}

output

@Preview

๋ง ๊ทธ๋Œ€๋กœ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•˜์—ฌ IDE์—์„œ Preview๋ฅผํ•˜๊ธฐ ์œ„ํ•œ ์šฉ๋„์ด๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด @Preview ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋‹ค์Œ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

@Preview("Greeting Preview")
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Surface(color = MaterialTheme.colors.background) {
            Greeting("Android")
        }
    }
}

Greeting Preview

ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•˜๋Š”๋ฐ ํ•„์ˆ˜์ ์ธ ์š”์†Œ๋“ค

setContent {
  BasicsCodelabTheme {
    // A surface container using the 'background' color from the theme
    Surface(color = MaterialTheme.colors.background) {
      Greeting("Android")
    }
  }
}

๊ธฐ์กด์— onCreate์‹œ์ ์— ํ™”๋ฉด์„ ๊ทธ๋ ค์ฃผ๊ธฐ ์œ„ํ•œ ํ•„์ˆ˜์ ์ธ ์š”์†Œ๋ฅผ ์ •๋ฆฌํ•ด๋ณด์ž๋ฉด

private val DarkColorPalette = darkColors(
    primary = purple200,
    primaryVariant = purple700,
    secondary = teal200
)

private val LightColorPalette = lightColors(
    primary = purple500,
    primaryVariant = purple700,
    secondary = teal200

    /* Other default colors to override
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black,
    */
)

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        content = content
    )
}
@Composable
fun Surface(
    modifier: Modifier = Modifier,
    shape: Shape = RectangleShape,
    color: Color = MaterialTheme.colors.surface,
    contentColor: Color = contentColorFor(color),
    border: BorderStroke? = null,
    elevation: Dp = 0.dp,
    content: @Composable () -> Unit
) {
    val elevationPx = with(LocalDensity.current) { elevation.toPx() }
    val elevationOverlay = LocalElevationOverlay.current
    val absoluteElevation = LocalAbsoluteElevation.current + elevation
    val backgroundColor = if (color == MaterialTheme.colors.surface && elevationOverlay != null) {
        elevationOverlay.apply(color, absoluteElevation)
    } else {
        color
    }
    CompositionLocalProvider(
        LocalContentColor provides contentColor,
        LocalAbsoluteElevation provides absoluteElevation
    ) {
        Box(
            modifier.graphicsLayer(shadowElevation = elevationPx, shape = shape)
                .then(if (border != null) Modifier.border(border, shape) else Modifier)
                .background(
                    color = backgroundColor,
                    shape = shape
                )
                .clip(shape),
            propagateMinConstraints = true
        ) {
            content()
        }
    }
}

์„ ์–ธํ˜• UI

๋…ธ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ์„ ์ž…ํ˜€ ๊ธฐ์กด TextView์— ์ถ”๊ฐ€ํ•ด๋ณด์•˜๋‹ค. ๋˜ํ•œ, Greeting์—๋Š” Modifier๋ผ๋Š” ๊ฒƒ์„ ์ด์šฉํ•˜์—ฌ Padding์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๋˜์—ˆ๋‹ค.

BasicsCodelabTheme {
  // A surface container using the 'background' color from the theme
  Surface(color = Color.Yellow) {
    Greeting("Android")
  }
}
@Composable
fun Greeting(name: String) {
    var isSelected by remember { mutableStateOf(false) }
    val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)

    Text(
        text = "Hello $name!",
        modifier = Modifier
            .padding(24.dp)
            .background(color = backgroundColor)
            .clickable(onClick = { isSelected = !isSelected })
    )
}

๊ฒฐ๊ณผ

์„ ์–ธํ˜• UI์˜ ์žฅ์ ์€ ๋ง ๊ทธ๋Œ€๋กœ ๋‚ด๊ฐ€ UI๋ฅผ ์ •์˜ํ•œ๋Œ€๋กœ ์‹œ๊ฐ์ ์œผ๋กœ ํ‘œํ˜„์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค. ๊ธฐ์กด์—๋Š” ์†์„ฑ์„ ๋งค๋ฒˆ On/Off์™€ ๊ฐ™์€ ์˜ต์…˜์„ ํ†ตํ•ด ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ๋‹ค๋ฐ˜์‚ฌ์˜€์ง€๋งŒ, ์ด์ œ๋Š” ๋งค๋ฒˆ ์†์„ฑ์— ๋ณ€๊ฒฝ์ด ์ƒ๊ธธ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ ๊ทธ๋ ค์ฃผ๊ฒŒ ๋˜๋Š”๊ฒƒ์ด๋‹ค.

Compose reusability

์ปดํฌ์ฆˆ์˜ ์žฅ์  ์ค‘ ํ•˜๋‚˜๋Š” ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋›ฐ์–ด๋‚œ๊ฒƒ์ธ๋ฐ, XML์—์„œ ์šฐ๋ฆฌ๊ฐ€ include ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ๊ณณ์—์„œ ๊ฐ–๋‹ค์“ธ ์ˆ˜ ์žˆ๋˜๊ฒƒ์ฒ˜๋Ÿผ, ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ๊ณณ์—์„œ ์ •์˜ํ•˜์—ฌ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์ฐธ๊ณ ํ•ด์•ผํ•  ์ ์€ ์ปดํฌ์ฆˆ ์ปดํฌ๋„ŒํŠธ ํ™•์žฅ ์‹œ @Composable ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ ํ™•์žฅ์ด ํ•„์š”ํ•˜๋‹ค.

Container ์ž‘์„ฑ

MyApp์ด๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ์ปดํฌ์ฆˆ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํšฌํ•˜์—ฌ ์—ฌ๋Ÿฌ๊ณณ์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” Composable์„ ๊ตฌํ˜„ํ•˜์˜€๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ Container๋‚ด ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋„ฃ์–ด์ฃผ๋ ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ธ์ž๋กœ @Composable () -> Unit ํƒ€์ž…์„ ๋„˜๊ฒจ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

@Composable
fun MyApp(content: @Composable () -> Unit) {
    BasicsCodelabTheme {
        Surface(color = Color.Yellow) {
            content()
        }
    }
}

์œ„ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ด์ œ๋Š” ์–ด๋””์„œ๋“  ๋ฐ˜๋ณตํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” Container๋ฅผ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋˜์–ด ์•„๋ž˜์™€ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ๋˜์—ˆ๋‹ค.

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MyApp {
        Greeting("Android")
      }
    }
    ...
  }

Calling Composable functions multiple times using Layouts

์ง€๊ธˆ๊นŒ์ง€๋Š” ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ์„ ๊ฐ–๊ณ  ์‚ฌ์šฉํ–ˆ์ง€๋งŒ, ์—ฌ๋Ÿฌ๊ฐœ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋„ฃ๋Š”๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

@Composable
fun MyScreenContent() {
    Column {
        Greeting("Android")
        Divider(color = Color.Black)
        Greeting("there")
    }
}

Column๊ณผ ์œ„์—์„œ๋ถ€ํ„ฐ ์‚ฌ์šฉํ•˜๋˜ Greeting ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๋ผ์ธ์„ ๊ทธ์–ด์ฃผ๊ธฐ ์œ„ํ•œ Divider๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒฐ๊ณผ๋ฌผ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

multiple component

์œ„ ์ปดํฌ๋„ŒํŠธ ์ค‘ ๋ชป๋ณด๋˜ ์ปดํฌ์ €๋ธ”์ด ์žˆ๋Š”๋ฐ, ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค๋ช…์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์ด๋ฅผ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ๋„ ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

@Composable
fun MyColumnScreen(names: List<String> = listOf("Line One", "Line Two")) {
    Column {
        names.forEach {
            Greeting(name = it)
            Divider(color = Color.Black)
        }
    }
}

์ƒํƒœ๊ฐ’ ๊ด€๋ฆฌ

์œ„ ์ปดํฌ๋„ŒํŠธ์— ๋ฒ„ํŠผ์„ ํด๋ฆญํ–ˆ์„ ๋•Œ ํด๋ฆญํ•œ ์นด์šดํŠธ๋ฅผ ์ง‘๊ณ„ํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์•˜๋‹ค.

@Composable
fun MyColumnScreen(names: List<String> = listOf("Line One", "Line Two")) {
    val counterState = remember { mutableStateOf(0) } // 

    Column {
        names.forEach {
            Greeting(name = it)
            Divider(color = Color.Black)
        }
        Counter(
            count = counterState.value,
            updateCount = { newCount ->
                counterState.value = newCount
            }
        )
    }


}

rememer๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ธฐ์กด์— ์กด์žฌํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๊ฐ’์„ ๊ธฐ์–ตํ•˜๊ฒŒ ํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ ์žˆ๋‹ค. ๋‚ด๋ถ€๋ฅผ ํ•œ๋ฒˆ๋ณด๋ฉด

/**
 * Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition.
 * Recomposition will always return the value produced by composition.
 */
@OptIn(ComposeCompilerApi::class)
@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

๋งค๋ฒˆ๋งˆ๋‹ค Recomposition(์žฌ์กฐํ•ฉ)ํ•˜๊ฒŒ๋˜๋Š” ๊ฒฝ์šฐ ์ปดํฌ๋„ŒํŠธ์— ๊ฐ’์„ ๋‹ค์‹œ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. @Composable ์–ด๋…ธํ…Œ์ด์…˜์— ๋“ค์–ด๊ฐ„ ํ•จ์ˆ˜๋Š” ๋งค๋ฒˆ ํ•ด๋‹น ์ƒํƒœ๋ฅผ ๊ตฌ๋…ํ•˜๊ณ , ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋ ๋•Œ๋งˆ๋‹ค ์•Œ๋ฆผ์„ ๋ฐ›์•„ ๊ธฐ์กด ํ™”๋ฉด์„ ๊ฐฑ์‹ ํ•ด์ค€๋‹ค.

๊ทธ๋ฆฌ๊ณ , ์•„๋ž˜ Counter๋ฅผ ๋ณด๋ฉด Button์„ ์ด์šฉํ•˜์—ฌ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ–ˆ๋‹ค.

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(
        onClick = { updateCount(count + 1) },
        colors = ButtonDefaults.buttonColors(
            backgroundColor = if (count > 5) Color.Green else Color.White
        )
    ) {
        Text("I've been clicked $count times")
    }
}

updateCount(Int) ํ•จ์ˆ˜๋ฆ‰ ํ†ตํ•ด ๋งค๋ฒˆ ๊ฐ’์„ ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ๋Š”๋ฐ, ์ด๋ฅผ ํ†ตํ•ด counterState์— ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋ฉด์„œ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งค๋ฒˆ ๋ณ€๊ฒฝ์ด ๋˜๋Š”๊ฒƒ์ด๋‹ค.

๋”ฐ๋ผ์„œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. Count๊ฐ€ 5๊ฐ€ ๋„˜์–ด๊ฐ€๋ฉด ์ดˆ๋ก์ƒ‰์œผ๋กœ ๋ฐ”๋€๋‹ค.

count

๊ทธ ์™ธ์—๋„ ์—ฌ๋Ÿฌํ˜•ํƒœ์˜ ๋ชจ์–‘์„ ๊ตฌ์„ฑํ• ์ˆ˜ ์žˆ๋„๋ก ์˜ต์…˜์ด ์ œ๊ณต๋˜์–ด ์žˆ๋‹ค. ์ž์„ธํ•œ ์ •๋ณด๋Š” ๋‚˜์ค‘์— Codelabs์— ๋” ๋‚˜์™€ ์žˆ์œผ๋‹ˆ ๋ณด๋„๋กํ•˜๊ณ , ์ด๋ฒˆ์— setContent์— ๋Œ€ํ•œ ๋™์ž‘์›๋ฆฌ๋ฅผ ํ•จ๊ป˜ ๊ณ ๋ฏผํ•ด๋ณด์ž.

Activity์—์„œ ์ž‘์„ฑ

Compose๋ฅผ ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด Activity, Fragment์™€ ๊ฐ™์€๊ณณ์—์„œ contentView๋กœ ๋ฟŒ๋ ค์ค˜์•ผํ•œ๋‹ค. ๊ธฐ์กด์— ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•˜๋˜ ํ•จ์ˆ˜๋ฅผ ๋ณด์ž.

/**
* Set the activity content from a layout resource.  The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
  getWindow().setContentView(layoutResID);
  initWindowDecorActionBar();
}

UI ์ปดํฌ๋„ŒํŠธ์—์„œ ํ™”๋ฉด์„ ๋ถ™์ผ ์ˆ˜ ์žˆ๋Š” Window๋ผ๋Š” ๋…€์„์—์„œ Layout Resource Id๋ฅผ ํ†ตํ•ด ๊ธฐ์กด์— ๋“ฑ๋ก๋˜์–ด์žˆ๋˜ Layout XML ํŒŒ์ผ์„ ๋กœ๋“œํ•˜์—ฌ ์ธํ”Œ๋ ˆ์ดํ„ฐ์—์„œ ํŒŒ์‹ฑํ•˜๊ณ , ์ด๋ฅผํ†ตํ•ด ๋ ˆ์ด์•„์›ƒ ๊ณ„์ธต์— ์žˆ๋Š” ๋ทฐ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ˆœ์ฐจ์ ์œผ๋กœ ViewGroup, View๋ฅผ ๋งŒ๋“ค์–ด ๋„ฃ์–ด์ฃผ๊ฒŒ ๋œ๋‹ค.

PhoneWindow๋ฅผ ๋ณด๋ฉด ์ž์„ธํ•˜๊ฒŒ ์•Œ ์ˆ˜ ์žˆ๋Š”๋ฐ, Window๋ฅผ ๊ตฌํ˜„ํ•œ setContentView์—์„œ ์ฒ˜์Œ์— ์ƒ์„ฑ๋˜๋Š” ์ตœ์ƒ์œ„ ๋ ˆ์ด์•„์›ƒ ๊ทธ ์œ„์— ๋”ฐ๋กœ ์—†๋‹ค๋ฉด installDecor() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด mContentParent(๋ ˆ์ด์•„์›ƒ ๋ฆฌ์†Œ์Šค๊ฐ€ ๋ถ™๊ฒŒ๋  ViewGroup)๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ํ•˜์œ„์— ๋„ฃ์–ด์ฃผ๊ฒŒ ๋œ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹์€ ์ด์ •๋„๋กœ ์„ค๋ช…์„ํ•˜๊ณ , ์ด๋ฒˆ์—” Compose์—์„œ setContent() ๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๋ณด์ž.

/**
 * Composes the given composable into the given activity. The [content] will become the root view
 * of the given activity.
 *
 * This is roughly equivalent to calling [ComponentActivity.setContentView] with a [ComposeView]
 * i.e.:
 *
 * ```
 * setContentView(
 *   ComposeView(this).apply {
 *     setContent {
 *       MyComposableContent()
 *     }
 *   }
 * )
 * ```
 *
 * @param parent The parent composition reference to coordinate scheduling of composition updates
 * @param content A `@Composable` function declaring the UI contents
 */
public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

์ด๋…€์„๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ window.decorView.findViewById<ViewGroup>(android.R.id.content) ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ decorView๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. ๋งŒ์•ฝ compose๋ฅผ ํ†ตํ•ด ๋งŒ๋“ค์–ด์ง„ ์ตœ์ƒ์œ„ ๋ ˆ์ด์•„์›ƒ์ด ์กด์žฌํ•˜๋ฉด, ๊ธฐ์กด์— inflator์—์„œ ViewGroup, View๋ฅผ ์ƒ์„ฑํ•ด์„œ ๋„ฃ์–ด์ฃผ๋˜๊ฒƒ ์ฒ˜๋Ÿผ setContent() => window๊ฐ€ Activity/Fragment์— ๋ถ™์œผ๋ฉด createComposition()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ฒ€์ฆ ํ›„ ensureCompsositionCreated() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ํ˜„์žฌ๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ViewGroup.setContent() ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ๊ณง ๊ต์ฒด ๋  ์˜ˆ์ •์ด๋ผ๊ณ  ํ•œ๋‹ค. ์ด์ฝ”๋“œ๋„ ๋ณด๋ฉด ๊ธฐ์กด์— ์žˆ๋Š” ViewGroup์— ํ™•์žฅํ•จ์ˆ˜๋กœ ๊ตฌํ˜„ํ•œ ๋…€์„์ธ๋ฐ, ์‰ฝ๊ฒŒ ๋งํ•ด ViewGroup์— ํ•˜์œ„ View, ViewGroup์— Composable๋กœ ๊ตฌํ˜„๋œ ํ•จ์ˆ˜๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋„ฃ์–ด์ค„ ๋•Œ AndroidComposeView๋ผ๋Š” ๊ฐ์ฒด๋ฅผ ๊บผ๋‚ด์˜ค๊ฑฐ๋‚˜ ์—†๋‹ค๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์—ฌ ๋„ฃ์–ด์ค€๋‹ค.

/**
 * Composes the given composable into the given view.
 *
 * The new composition can be logically "linked" to an existing one, by providing a
 * [parent]. This will ensure that invalidations and CompositionLocals will flow through
 * the two compositions as if they were not separate.
 *
 * Note that this [ViewGroup] should have an unique id for the saved instance state mechanism to
 * be able to save and restore the values used within the composition. See [View.setId].
 *
 * @param parent The [Recomposer] or parent composition reference.
 * @param content Composable that will be the content of the view.
 */
internal fun ViewGroup.setContent(
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    GlobalSnapshotManager.ensureStarted()
    val composeView =
        if (childCount > 0) {
            getChildAt(0) as? AndroidComposeView
        } else {
            removeAllViews(); null
        } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
    return doSetContent(composeView, parent, content)
}

๋‹ค์‹œ ๋Œ์•„์™€์„œ, ComposeView์˜ setContent() ์ด๋ผ๋Š” ๋…€์„์„ ๋ณด์ž.

/**
 * A [android.view.View] that can host Jetpack Compose UI content.
 * Use [setContent] to supply the content composable function for the view.
 *
 * This [android.view.View] requires that the window it is attached to contains a
 * [ViewTreeLifecycleOwner]. This [androidx.lifecycle.LifecycleOwner] is used to
 * [dispose][androidx.compose.runtime.Composition.dispose] of the underlying composition
 * when the host [Lifecycle] is destroyed, permitting the view to be attached and
 * detached repeatedly while preserving the composition. Call [disposeComposition]
 * to dispose of the underlying composition earlier, or if the view is never initially
 * attached to a window. (The requirement to dispose of the composition explicitly
 * in the event that the view is never (re)attached is temporary.)
 */
class ComposeView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {

    private val content = mutableStateOf<(@Composable () -> Unit)?>(null)

    @Suppress("RedundantVisibilityModifier")
    protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
        private set

    @Composable
    override fun Content() {
        content.value?.invoke()
    }

    /**
     * Set the Jetpack Compose UI content for this view.
     * Initial composition will occur when the view becomes attached to a window or when
     * [createComposition] is called, whichever comes first.
     */
    fun setContent(content: @Composable () -> Unit) {
        shouldCreateCompositionOnAttachedToWindow = true
        this.content.value = content
        if (isAttachedToWindow) {
            createComposition()
        }
    }
}

์œ„์— ๋ญ๋ผ๋ญ๋ผ ์จ์ ธ ์žˆ๋Š”๋ฐ, ๊ฒฐ๋ก ์ ์œผ๋กœ AbstractComposeView ๋ผ๋Š” ๋…€์„์€ ViewGroup์„ ์ƒ์†๋ฐ›์€ ๋…€์„์ด๋ฉฐ, ๋ชจ๋“  composable์˜ ์ƒํƒœ๊ฐ€ ๋ณ€ํ™” ๋˜์—ˆ์„ ๋•Œ ์ด๋ฅผ ๊ฐ์ง€ํ•˜๋Š” ์ค‘์š”ํ•œ ๋…€์„์ด๋‹ค.

setContent()๋ผ๋Š” ํ•จ์ˆ˜๋Š” ์œ„์—์„œ ์„ค๋ช…ํ–ˆ์œผ๋‹ˆ ๋„˜์–ด๊ฐ€๊ณ , ์ด๋ฒˆ์—๋Š” Content๋ผ๋Š” ๋…€์„์„ ๋ณด์ž. ์ด๋…€์„์€ ์ถ”์ƒ ๋ฉ”์†Œ๋“œ๋กœ, createComposition() ์ด๋ผ๋Š” ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ ๋˜์—ˆ์„ ๋•Œ, ๊ฐ€์žฅ ๋จผ์ € ๋ถˆ๋ฆฌ๋Š” ํ•จ์ˆ˜์ด๋‹ค. ์•„๊นŒ ์–ธ๊ธ‰๋˜์—ˆ๋˜ ensureCompsositionCreated() ํ•จ์ˆ˜์—์„œ tree๊ณ„์ธต์˜ ComposeView๊ฐ€ ๋‹ค ๋ถ™์—ˆ๋‹ค๋ฉด, ์ดํ›„์— ์ฆ‰์‹œ Contentํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ์ด๋œ๋‹ค.

@Suppress("DEPRECATION") // Still using ViewGroup.setContent for now
    private fun ensureCompositionCreated() {
        if (composition == null) {
            try {
                creatingComposition = true
                composition = setContent(
                    parentContext ?: findViewTreeCompositionContext() ?: windowRecomposer
                ) {
                    Content() // ์ด๊ณณ์—์„œ ๋ทฐ๊ฐ€ ๋‹ค window์— ๋ถ™๊ฒŒ๋˜๋ฉด ์ฝœ๋ฐฑ์„ ํ˜ธ์ถœํ•œ๋‹ค.
                }
            } finally {
                creatingComposition = false
            }
        }
    }

๊ทธ๋Ÿฌ๋ฉด ์•„๋ž˜ ComposeView์˜ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ๋œ Content๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด์„œ, ๊ธฐ์กด์— ์ƒ์„ฑ๋œ View์— UI์†์„ฑ๊ณผ ๊ฐ™์€ Content๊ฐ€ ๋ถ™๊ฒŒ๋œ๋‹ค.

/**
* The Jetpack Compose UI content for this view.
* Subclasses must implement this method to provide content. Initial composition will
* occur when the view becomes attached to a window or when [createComposition] is called,
* whichever comes first.
*/
@Composable
abstract fun Content()

Content๋Š” ์„ค๋ช…์—์„œ ๋ณด๋Š”๊ฒƒ๊ณผ ๊ฐ™์ด createComposition() ํ•จ์ˆ˜ ํ˜ธ์ถœ ํ›„ View๊ฐ€ Window์— ๋ถ™์€ ์ดํ›„ ์ฆ‰์‹œ ํ˜ธ์ถœ๋œ๋‹ค.

์ตœ์ข…์ ์œผ๋กœ ComponentActivity.setContent(CompositionContext?, @Composable () -> Unit) ํ•จ์ˆ˜์—์„œ ๊ตฌํ˜„๋œ ComposeView ์ธ์Šคํ„ด์Šค๋ฅผ ContentLayout์„ widht/height๋ฅผ wrapContentํฌ๊ธฐ๋กœ ์ •ํ•˜์—ฌ ContentView๋ฅผ Setํ•ด์ฃผ๊ฒŒ ๋œ๋‹ค.

/**
 * Composes the given composable into the given activity. The [content] will become the root view
 * of the given activity.
 *
 * This is roughly equivalent to calling [ComponentActivity.setContentView] with a [ComposeView]
 * i.e.:
 *
 * ```
 * setContentView(
 *   ComposeView(this).apply {
 *     setContent {
 *       MyComposableContent()
 *     }
 *   }
 * )
 * ```
 *
 * @param parent The parent composition reference to coordinate scheduling of composition updates
 * @param content A `@Composable` function declaring the UI contents
 */
public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
  	...
		else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

๊ทธ๋ž˜์„œ, Jetpack Compose๋Š” ์“ธ๋งŒํ•œ๊ฐ€?

์“ธ๋งŒํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ์ „ํ˜€ ์ƒ๊ฐ์ง€๋„ ๋ชปํ•œ ์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์„ ๋„์ž…ํ•  ์ˆ˜ ์žˆ๋˜ ๊ฒƒ์ด ์‹ ์„ ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•˜๊ณ , ์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ๋„ ๊ฒฐ๊ตญ ์œ ํ–‰์ด๋  ๊ฒƒ์ด๊ธฐ๋•Œ๋ฌธ์—, ๊ธฐ์กด์— ์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์‰ฝ๊ฒŒ ์ ‘ํ–ˆ๋˜ ๊ฐœ๋ฐœ์ž๋“ค ex) React, Flutter ๋“ฑ๋“ฑ์„ ์ ‘ํ•œ ๊ฐœ๋ฐœ์ž๋Š” ๋” ์‰ฝ๊ฒŒ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์„ ๋„์ „ํ•ด๋ณด์ง€ ์•Š์„๊นŒ ์‹ถ๋‹ค. ์˜คํžˆ๋ ค ๋ฐ˜๋Œ€๋กœ ๊ธฐ์กด์— XML๋กœ ๋ ˆ์ด์•„์›ƒ ์ฝ”๋“œ๋ฅผ ์งœ๋˜ ๊ฐœ๋ฐœ์ž๋“ค์€ ์ƒ์†Œํ•œ ๋ฐฉ์‹์ด๋ผ ์ ์‘ํ•˜๋Š”๋ฐ ์‹œ๊ฐ„์ด ๊ฝค๋‚˜ ๊ฑธ๋ฆด ๊ฒƒ์ด๋‹ค.

๋‹ค๋งŒ, ์•„์ง๋„ alpha๋ฒ„์ „์ด๋ผ ๋ฐ”๋€” ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ๋„ˆ๋ฌด๋‚˜๋„ ๋งŽ์€์ƒํ™ฉ์ด๋‹ค. ์•ž์œผ๋กœ ์ข‹์€ ์˜ˆ์‹œ๋“ค์ด ๋‚˜์˜ค๊ณ , ์ข€ ๋” ๊ธฐ์กด ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ต์ˆ™ํ•ด ํ• ๋งŒํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ ์ œ๊ณต์ด ๋˜๋ฉด, ์ข€ ๋” ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์œผ๋กœ ๋„˜์–ด๊ฐˆ ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ ์‹ถ๋‹ค.

์ง€๊ธˆ ๋‹น์žฅ์€.. ๋ณธ์ธ์˜ ๊ฒฝ์šฐ ์›น ํ”„๋กœ๊ทธ๋ž˜๋ฐ๋„ ๊ฒฝํ—˜์„ ํ•ด๋ดค๋˜ํ„ฐ๋ผ ์ต์ˆ™ํ•œ ์ฝ”๋“œํŒจํ„ด์ด๊ธดํ•˜์ง€๋งŒ, ๋‹น์žฅ ๊ธฐ์กด์— ์žˆ๋˜ ์ฝ”๋“œ์™€ ๊ณต์กดํ•˜๋ฉด์„œ ์“ฐ๊ธฐ์—๋Š” ์—ฌ๊ฐ„ ๋ถˆํŽธํ• ๊ฒƒ ๊ฐ™๊ธฐ๋„ํ•˜๋‹ค. ์• ์ดˆ์— Flutter์˜ ๊ฒฝ์šฐ ์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์œผ๋กœ ๋ Œ๋”๋ง์„ ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ฐœ๋ฐœ์„ ์„ ์–ธํ˜•์œผ๋กœ ํ•˜๊ฒŒ๋˜๋‹ˆ, ๊ตณ์ด ์ด๋Ÿด๋ฐ”์—๋Š” Flutter๋กœ ์„œ๋น„์Šค๋ฅผ ๋”ฐ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ๋‚ซ์ง€ ์•Š์„๊นŒ๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ , ์ด๋Ÿฌํ•œ ์ƒˆ๋กœ์šด ์‹œ๋„๋Š” ์•ˆ๋“œ๋กœ์ด๋“œ ์ง„์˜์—์„œ ์นจ์ฒด๋˜์—ˆ๋˜ ๋ถ„์œ„๊ธฐ์— ํ™œ๊ธฐ๋ฅผ ๋„์›Œ์ฃผ์—ˆ๊ณ , ๊ฐœ๋ฐœ์ž์˜ ์ทจํ–ฅ์— ๋”ฐ๋ผ ์•ˆ๋“œ๋กœ์ด๋“œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํญ์ด ๋„“์–ด์ง„ ๋ถ€๋ถ„์—์„œ๋Š” ๋งค์šฐ ์ข‹์€๊ฒƒ ๊ฐ™๋‹ค.(๊ณต๋ถ€ํ•  ๊ฒŒ ๋˜ ๋Š˜์—ˆ๊ตฌ๋งŒ..)