
![]() | ![]() |
|---|
μ λ μ΅κ·Όμ μ€λ΅λ°λ₯Ό ꡬννλ©΄μ κ°κ°μ νλ©΄μμ νΉμ λ²νΌ ν΄λ¦ μ μ€λ΅λ°κ° 3μ΄ λμ λ ΈμΆλκ³ , "μ΄λ" λ²νΌμ λλ₯΄λ©΄ νΉμ νλ©΄μΌλ‘ μ΄λνλ κΈ°λ₯μ΄ νμνμ΅λλ€.
νμ§λ§ μ€μ λ‘ κ΅¬νμ μμνμλ§μ μκ°λ³΄λ€ λ§μ λ¬Έμ κ° λ±μ₯νμ΅λλ€.
μ΄ κΈμμλ
"Composeμ Snackbarκ° μ 3μ΄ κ³ μ μκ°μ΄ μ λλμ§?"
"μ μ°μ ν΄λ¦ μ μ€λ΅λ°κ° λ°λ³΅ νΈμΆλλμ§?"
"μ΄λ»κ² 컀μ€ν μ€λ΅λ°λ₯Ό μμ μ μΌλ‘ ꡬννλμ§?"
μμ λν ν΄κ²° κ³Όμ μ κΈ°λ‘ν΄λ³΄λ €κ³ ν©λλ€.

λͺ μΈλ λ€μκ³Ό κ°μ΄ λμ΄ μμμ΄μ.
κΈ°λ₯μ μ λ¦¬ν΄ λ³΄μλ©΄
- κ³ μ ν μ€νΈ
- βμ΄λβ ν μ€νΈ ν΄λ¦μ νμ΄μ§ μ΄λ
- 3μ΄κ° μ€λ΅λ°κ° λμμ Έ μμ΄μΌν¨
λ¨Όμ μ€λ΅λ°λ₯Ό ꡬννκΈ° μν΄μ 곡μλ¬Έμλ₯Ό νμΈν΄ 보μμ΅λλ€.
λ°μμ νλνλμ© λ―μ΄ λ³Όκ²μ..!
SnackBar λ₯Ό ꡬννκΈ° μν΄μλ ν¬κ² 3κ°μ§ μ¬λ£κ° νμνμ΅λλ€.
SnackBar UI(μ€λ΅λ° μ»΄ν¬μ λΈ ν¨μ UI)

μ€μ λ‘ androidx.compose.material3 μμ Snackbar μ»΄ν¬λνΈλ₯Ό μ 곡νκ³ μμ΅λλ€.
μ½λ
@Composable
fun Snackbar(
modifier: Modifier = Modifier,
action: @Composable (() -> Unit)? = null,
dismissAction: @Composable (() -> Unit)? = null,
actionOnNewLine: Boolean = false,
shape: Shape = SnackbarDefaults.shape,
containerColor: Color = SnackbarDefaults.color,
contentColor: Color = SnackbarDefaults.contentColor,
actionContentColor: Color = SnackbarDefaults.actionContentColor,
dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
content: @Composable () -> Unit,
) {
Surface(
modifier = modifier,
shape = shape,
color = containerColor,
contentColor = contentColor,
shadowElevation = SnackbarTokens.ContainerElevation,
) {
val textStyle = SnackbarTokens.SupportingTextFont.value
val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value
CompositionLocalProvider(LocalTextStyle provides textStyle) {
when {
actionOnNewLine && action != null ->
NewLineButtonSnackbar(
text = content,
action = action,
dismissAction = dismissAction,
actionTextStyle = actionTextStyle,
actionContentColor = actionContentColor,
dismissActionContentColor = dismissActionContentColor,
)
else ->
OneRowSnackbar(
text = content,
action = action,
dismissAction = dismissAction,
actionTextStyle = actionTextStyle,
actionTextColor = actionContentColor,
dismissActionColor = dismissActionContentColor,
)
}
}
}
}
νμ§λ§, μ΄λ shadowElevation μ κ³ μ κ°μΌλ‘ μ¬μ©νκ³ μκΈ°μ 컀μ€ν
ν SnackBar λ₯Ό λ§λ€κΈ°μλ λΆμ ν©νλ€κ³ μκ°ν΄ μ§μ μ»΄ν¬μ λΈμ ꡬμ±νμ΅λλ€.
MelonActionSnackbar μ½λ
@Composable
fun Snackbar(
modifier: Modifier = Modifier,
action: @Composable (() -> Unit)? = null,
dismissAction: @Composable (() -> Unit)? = null,
actionOnNewLine: Boolean = false,
shape: Shape = SnackbarDefaults.shape,
containerColor: Color = SnackbarDefaults.color,
contentColor: Color = SnackbarDefaults.contentColor,
actionContentColor: Color = SnackbarDefaults.actionContentColor,
dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
content: @Composable () -> Unit,
) {
Surface(
modifier = modifier,
shape = shape,
color = containerColor,
contentColor = contentColor,
shadowElevation = SnackbarTokens.ContainerElevation,
) {
val textStyle = SnackbarTokens.SupportingTextFont.value
val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value
CompositionLocalProvider(LocalTextStyle provides textStyle) {
when {
actionOnNewLine && action != null ->
NewLineButtonSnackbar(
text = content,
action = action,
dismissAction = dismissAction,
actionTextStyle = actionTextStyle,
actionContentColor = actionContentColor,
dismissActionContentColor = dismissActionContentColor,
)
else ->
OneRowSnackbar(
text = content,
action = action,
dismissAction = dismissAction,
actionTextStyle = actionTextStyle,
actionTextColor = actionContentColor,
dismissActionColor = dismissActionContentColor,
)
}
}
}
}
Snackbar μ λ€μ΄κ° message, μ‘μ
λ²νΌμ ν
μ€νΈμΈ actionLabel, μ€μ μ΄λ€ λμμ ν μ§ action ν¨μλ₯Ό λ°μμ μλλ‘ κ΅¬μ±ν΄λ³΄μμ΄μ.
Scaffoldμμ μ¬μ©λλ Snackbarλ€μ νμνκΈ° μν Hostλ‘,Material μ€νκ³Ό hostStateμ λ°λΌ Snackbarλ₯Ό μ μ ν μμ μ 보μ¬μ£Όκ³ μ¨κΈ°κ³ λ«λ μν μ ν©λλ€.@Composable
fun SnackbarHost(
hostState: SnackbarHostState,
modifier: Modifier = Modifier,
snackbar: @Composable (SnackbarData) -> Unit = { Snackbar(it) },
) {
val currentSnackbarData = hostState.currentSnackbarData
val accessibilityManager = LocalAccessibilityManager.current
LaunchedEffect(currentSnackbarData) {
if (currentSnackbarData != null) {
val duration =
currentSnackbarData.visuals.duration.toMillis(
currentSnackbarData.visuals.actionLabel != null,
accessibilityManager,
)
delay(duration)
currentSnackbarData.dismiss()
}
}
FadeInFadeOutWithScale(
current = hostState.currentSnackbarData,
modifier = modifier,
content = snackbar,
)
}SnackBarHostState μ snackbar μ»΄ν¬μ λΈ ν¨μλ₯Ό λ°μμ μ΄λ₯Ό λμμ£Όλ μν μ ν©λλ€ !SnackBarHostState
μ€λ΅λ°μ μνλ₯Ό κ΄λ¦¬νκ³ , showSnackbar()λ₯Ό ν΅ν΄ μ€λ΅λ°λ₯Ό λμλλ€.
λ³΄ν΅ remember λ₯Ό μ΄μ©ν΄μ μ μ₯ ν ν, Scaffold μ SnackBarHost λ₯Ό ꡬμ±ν λ μ¬μ©ν΄μ.
μ€λ΅λ°λ₯Ό μ‘°μνλ λ° ν΅μ¬μΈ μνλ‘, μ£Όλ‘ μ΄ν΄λ³Ό κ³³μ Mutex() λ₯Ό μ΄μ©ν λ°©μκ³Ό SnackbarData, showSnackbar() μ
λλ€.
Mutex() μ showSnackBar()
Snackbar μμ² μ¦, λμ°λ ν¨μλ SnackBarHostState μΈμ€ν΄μ€κ° κ°μ§κ³ μλ
showSnackBar() λ₯Ό νΈμΆν΄μ λμ°κ² λλλ°μ ..!
λ§μ½, μ¬μ©μκ° μ¬λ¬λ² μ€λ΅λ° μμ²μ νκ² λλ©΄ μ΄λ»κ² λ κΉμ??
μ€λ΅λ°κ° λμμ λκ°κ° λ° κ°λ₯μ±μ μ‘΄μ¬ νμ§ μμκΉμ..??
μ€λ΅λ°λ₯Ό λμ°λ ν¨μμΈ showSnackBar() λ΄λΆ μ½λλ₯Ό κ°λ¨ν 보μλ©΄ β¦
suspend fun showSnackbar(visuals: SnackbarVisuals): SnackbarResult =
mutex.withLock {
try {
return suspendCancellableCoroutine { continuation ->
currentSnackbarData = SnackbarDataImpl(visuals, continuation)
}
} finally {
currentSnackbarData = null
}
}
μ£Όμ κΉκ² λ΄μΌν μ μ mutex.withLock λΆλΆμ
λλ€.
mutex λ λ³΄ν΅ μ¬λ¬ μ½λ£¨ν΄μ΄ 곡μ μμμ λν΄ λ°μ΄ν° λ¬΄κ²°μ± νΉμ νλμ μμ
μ μλ£νλλ‘ λ³΄μ₯ν λ μ¬μ©ν©λλ€
withLock λΈλ‘ μμ μλ μμ
μ μμ ν μννκ² ν©λλ€. μ΄ν μμ
μμ²μ΄ λ€μ΄μ€λ©΄ μ΄μ μμ
μ΄ μλ£λ λκΉμ§ λκΈ°νν μ€νν©λλ€.
μ¦, μ¬μ©μκ° μ°μμ μΌλ‘ ν΄λ¦μ νκ² λλ©΄ λκ°κ° λμμ λ¨λ κ²μ΄ μλ, μμ μ νμ²λΌ μμλκ³ μ€λ΅λ°κ° μ¬λΌμ§λ©΄ λ€μ μ€λ΅λ°λ₯Ό λμμ£Όκ² λ©λλ€ !!
showSnackbar()
μμ λ³Έ showSnackbar() λ₯Ό μ¬μ©νκΈ° μν΄μλ μ€μ λ‘ μΈλΆμμ μ¬μ©ν λλ μλ μ½λλ₯Ό νΈμΆν΄μ μ¬μ©ν©λλ€.
suspend fun showSnackbar(
message: String,
actionLabel: String? = null,
withDismissAction: Boolean = false,
duration: SnackbarDuration = // duration κ³ μ
if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite,
): SnackbarResult =
showSnackbar(SnackbarVisualsImpl(message, actionLabel, withDismissAction, duration))
SnackbarDuration μ Short, Long, Indefinite 3κ°μ§ duration λ§μ μ§μ ν μ μκ² λμ΄ μμ΅λλ€.
κ°κ°μ μκ°μ 4μ΄, 10μ΄, 무ν μΌλ‘λμ΄ μμ΄μ μ ν¬κ° ꡬννκ³ μ νλ 3μ΄λ ν΄λΉ λ°©μμΌλ‘ ꡬν ν μ μμμ΅λλ€ ππ
μ΄μ μ 리λ₯Ό ν΄λ³΄μλ©΄ ..!
μ΄μ ..! μμ 2κ°μ§ λ¬Έμ λ₯Ό ν΄κ²°ν λ°©λ²μ λν΄ μ΄μΌκΈ° ν΄λ³Όκ²μ γ
γ
μ°μ μ λ snackbar μ λν λ‘μ§μ λ°λ‘ λΆλ¦¬νκ³ μΆμκΈ°μ SnackbarController λΌλ ν΄λμ€λ₯Ό λ§λ€μ΄μ£Όμμ΅λλ€ !
- MelonSnackbarController
class MelonSnackbarController( private val coroutineScope: CoroutineScope, ) { val snackbarHostState = SnackbarHostState() var currentRequest by mutableStateOf<MelonSnackbarActionRequest?>(null) private set fun show(request: MelonSnackbarActionRequest) { currentRequest = request coroutineScope.launch { launch { snackbarHostState.showSnackbar( message = request.message, actionLabel = request.actionLabel, withDismissAction = true, duration = SnackbarDuration.Short, ) } } } fun performAction() { currentRequest?.onClick?.invoke() currentRequest = null coroutineScope.launch { snackbarHostState.currentSnackbarData?.dismiss() } } } @Composable fun rememberMelonSnackbarController( scope: CoroutineScope = rememberCoroutineScope(), ): MelonSnackbarController = remember { MelonSnackbarController(scope) }showSnackbarλ suspend ν¨μμ΄κΈ° λλ¬ΈμrememberCoroutineScopeλ₯Ό μ¬μ©ν΄μ λμνλλ‘ νμμ΅λλ€.controllerμ체λ μμ± ν μ΄κΈ°ν λμ§ μκ² νκΈ° μν΄ remember λ‘ κ°μΈ μ£Όμμ΄μ.
λ€μμΌλ‘λ Snackbar μμ²μ μν λͺ¨λΈκ³Ό μΈλΆμμ νΈλ¦¬κ±° νλ λ°©μμΌλ‘ μ¬μ©νκΈ° μν΄ CompositonLoacal μ μ¬μ©νμ΅λλ€.
- MelonSnackbarModel
@Immutable data class MelonSnackbarActionRequest( val message: String, val actionLabel: String, val onClick: () -> Unit, ) val LocalMelonSnackbarTrigger = staticCompositionLocalOf<(MelonSnackbarActionRequest) -> Unit> { error("No MelonSnackbarTrigger provided") }μ€μ μ¬μ©νλ κ³³μμλ snackbarController λ₯Ό νΈμΆνλ λ‘μ§μ trigger λ‘ μ μΈνμ¬ μΈλΆμμ μ¬μ©νκ² νμ΅λλ€.
- μ μΈ & μ¬μ©
snackbarTrigger κ° νΈμΆμ΄ λλ©΄ snackbarRequest κ° μ λ¬λκ³ μ΄λ₯Ό ν΅ν΄ snackbarController.show κ° νΈμΆμ΄ λ©λλ€.// MainScreen μμ μ μΈ val snackbarController = rememberMelonSnackbarController() val snackbarTrigger: (MelonSnackbarActionRequest) -> Unit = remember { { request -> snackbarController.show(request) } } Scaffold( snackbarHost = { SnackbarHost( hostState = snackbarController.snackbarHostState, ) { data -> val request = snackbarController.currentRequest ?: return@SnackbarHost MelonActionSnackbar( message = request.message, actionLabel = request.actionLabel, action = snackbarController::performAction, modifier = Modifier .padding( start = 16.dp, end = 16.dp, bottom = 16.dp, ), ) } }, // μΈλΆ Screen μμ μ¬μ© ex. HomeScreen val snackbarTrigger = LocalMelonSnackbarTrigger.current val snackbarRequest = MelonSnackbarActionRequest( message = "mixup count $count", actionLabel = stringResource(snackbar_move_action_label), onClick = navigateToMixUp, ) snackbarTrigger(snackbarRequest) // <- μ€μ νΈμΆνλ ν¨μ
λ€μ λμμμ 보μλ©΄ ..!
μ€λ΅λ° μ¬λ¬λ² ν΄λ¦μλ§λ€ μ΄μ μ€λ΅λ°λ μ·¨μνκ³ μλ‘μ΄ μ€λ΅λ°λ§ λμνλλ‘ κ΅¬μ±
κΈ°μ‘΄ ꡬ쑰λ μ°μ ν΄λ¦μ ν΄λΉ μ€λ΅λ°μ λν μμ²μ΄ μ¬λΌμ§μ§ μκ³ λκΈ°νλ€κ° κ³μν΄μ μ΄μ νΈμΆμ λ€μ λΆλ¬μμ§λκ²μ λ³Ό μ μλλ°μ..!!

λ°λΌμ λͺ
μμ μΌλ‘ μ΄λ₯Ό ν΄μ νκ² νλ λ°©λ²μ μ¬μ©νμ΄μ !
λ°©λ²μ snackbarHostState μ currentSnackbarData μ dissmiss ν¨μλ₯Ό μ¬μ©νλ©΄λλλ°μ ..!
μλλ λ§€λ² νΈμΆμ λ§λ€ μ΄μ snackbar λ₯Ό λͺ
μμ μΌλ‘ ν΄μ ν΄μ£Όλ μ½λμ
λλ€.
fun show(request: MelonSnackbarActionRequest) {
snackbarHostState.currentSnackbarData?.dismiss() // λͺ
μμ μΌλ‘ Dismiss
currentRequest = request
coroutineScope.launch {
launch {
snackbarHostState.showSnackbar(
message = request.message,
actionLabel = request.actionLabel,
withDismissAction = true,
duration = SnackbarDuration.Short,
)
}
}
}
snackbar duration κ°μ 3μ΄λ‘ μ§μ
κ·Έλ¬λ©΄ !! μ΄μ μκ°μ μ‘°μ ν΄λ΄μΌ κ² μ£ ?? μμ λ³Έκ²κ³Ό κ°μ΄ Duration μ Short λ‘ μ§μ ν κ²½μ°, κ°μ₯ μ§§μμΌ 4μ΄μ μκ°μ μ§μ ν μ μμμ΄μ
λ°λΌμ Snackbar λ₯Ό λͺ μμ μΌλ‘ 3μ΄μ μκ°μ΄ μ§λκ² λλ©΄ νμ μμμ΄ κ±Έλ¦¬κ² νμ¬ μ€λ΅λ°κ° dismiss λλ λ‘μ§μ΄ νμνμ΅λλ€
ν΄λΉ λ°©μμ ꡬννκΈ° μν΄μ corutineScope λ΄λΆμ delay λ₯Ό λ΄λΉνλ launch λΈλ‘μ λ§λ€κ³ delay κ° μ§λλ©΄ dismiss νκ² νμμ΄μ
μ½λ
private const val SNACKBAR_AUTO_DISMISS_MS = 3000L
fun show(request: MelonSnackbarActionRequest) {
currentRequest = request
coroutineScope.launch {
snackbarHostState.currentSnackbarData?.dismiss()
launch {
snackbarHostState.showSnackbar(
message = request.message,
actionLabel = request.actionLabel,
withDismissAction = true,
duration = SnackbarDuration.Indefinite, // 무νμΌλ‘ μ€μ
)
}
launch {
delay(SNACKBAR_AUTO_DISMISS_MS) // 3μ΄ λλ μ΄
snackbarHostState.currentSnackbarData?.dismiss()
}
}
}

ν΄λΉ μ½λλ‘ μ€ν ν΄λ³΄μλλ° λκ° μ΄μνμ£ ..?
λΆλͺ
3μ΄κ° μ§λκ³ λμ snackbar κ° μ¬λΌμ ΈμΌνλλ° μ λ°λ‘ μ¬λΌμ§λ κ±ΈκΉμ?????
λ¬Έμ λ μ½λ£¨ν΄μ μμμ΅λλ€. κΈ°μ‘΄μ μ½λ£¨ν΄μ΄ κ³μν΄μ λμνκ³ μμκΈ° λλ¬ΈμΈλ°μ
μ 리νμλ©΄...
- showSnackbar μν x 15
- λ§€ show λ§λ€ delay μ½λ£¨ν΄ μν
- κ° μ½λ£¨ν΄ λ³λ‘ delay 3μ΄κ° μ§λλ©΄ dismiss λ₯Ό νΈμΆ
- νμ¬μ μ€λ΅λ°κ° dismiss λλ€
λ€ μ΄μ κΉ‘κΉ‘μ΄μ μ½μ§μ΄μμ΅λλ€
κ·ΈλΌ μ΄ λ¬Έμ λ₯Ό μ΄λ»κ² ν΄κ²° νλλ©΄ ~
μ½λ£¨ν΄μ job.cancle()μ μ΄μ©ν΄μ μ€λ΅λ° μμ²μ λν΄μ 1λ1λ‘ μ·¨μν΄μ£Όλ λ°©μμ μ¬μ©νμ΄μ.
μ½λ
fun show(request: MelonSnackbarActionRequest) {
coroutineScope.launch {
snackbarHostState.currentSnackbarData?.dismiss()
val job = launch {
snackbarHostState.showSnackbar(
message = request.message,
actionLabel = request.actionLabel,
withDismissAction = true,
duration = SnackbarDuration.Indefinite,
)
}
launch {
delay(SNACKBAR_AUTO_DISMISS_MS)
job.cancel() // 3μ΄κ° μ§λλ©΄ μμ
μ·¨μ
}
}
}

μ .. μλλκ±° κ°μ£ ..??
κ·Έλμ ! μ΄μ ν΄κ²°λκ±° μλλ λΌκ³ μκ°νμ€ μ λ μμ§λ§ ν΄λΉ ꡬ쑰λ λ¬Έμ μ μ νλ κ°μ§κ³ μμ΅λλ€.
μ΄μ μ dismiss λ μ½λ£¨ν΄ μ¦, μ΄μ μ delay κ° κ³μ λμνκ³ μκΈ°μ λ¬Έμ κ° λΌμ.
μ κ° μν건 μ€λ΅λ°κ° μλ‘ νΈμΆμ΄λλ©΄ μ΄μ μ μμ
μ΄ μμ ν μ·¨μ λκΈ°λ₯Ό λ°λ¬μ΄μ.
λ°λΌμ μ΄μ job μ μ°Έμ‘°νμ¬ μλ‘μ΄ μμ
μ΄ λ€μ΄μ€λ©΄ cancle ν΄μ£Όλ λ‘μ§μ ꡬμ±νμ΅λλ€.
μ΅μ’ μ½λ
private const val SNACKBAR_AUTO_DISMISS_MS = 3000L
class MelonSnackbarController(
private val coroutineScope: CoroutineScope,
) {
val snackbarHostState = SnackbarHostState()
var currentRequest by mutableStateOf<MelonSnackbarActionRequest?>(null)
private set
private var actionJob: Job? = null // μ΄μ job μ μ°Έμ‘°ν λ³μ
private var timerJob: Job? = null // μ΄μ job μ μ°Έμ‘°ν λ³μ
fun show(request: MelonSnackbarActionRequest) {
currentRequest = request
coroutineScope.launch {
clearCurrentSnackbar()
val job =
launch {
snackbarHostState.showSnackbar(
message = request.message,
actionLabel = request.actionLabel,
withDismissAction = true,
)
}
actionJob = job
timerJob =
launch {
delay(SNACKBAR_AUTO_DISMISS_MS)
clearCurrentSnackbar()
currentRequest = null
}
}
}
// μ 체 job cancel λ° λ¦¬μμ€ μ κ±°
private fun clearCurrentSnackbar() {
actionJob?.cancel()
timerJob?.cancel()
actionJob = null
timerJob = null
}
fun performAction() {
currentRequest?.onClick?.invoke()
currentRequest = null
clearCurrentSnackbar()
}
}
μμ ꡬνν λ°©μμ Job μ μ§μ λ€κ³ μμΌλ©΄μ
μ΄μ μ€λ΅λ° Job/νμ΄λ¨Έλ₯Ό μ·¨μνκ³ μ μ€λ΅λ°λ₯Ό λμ°λ λ°©μμ΄μλλ°μ !
μΆνμ ν΄λΉ μ½λλ₯Ό κ°μ νλ€κ³ νλ€λ©΄ Flow + collectLatest λ‘ flow κΈ°λ°μ μ²λ¦¬λ‘ μμ²μλ§λ€ λ§€λ² λ§μ§λ§ μμ²λ§μ μ²λ¦¬ν΄μ ꡬνν μλ μμ κ±° κ°μ΅λλ€ !
λ, νμ¬ κ΅¬μ‘°λ λ§€λ² μ€λ΅λ°λ₯Ό μλ‘ λΆλ¬μ€λλ° ν΄λΉ λ°©μ λ§κ³ μ€λ΅λ°κ° λ μλ λμμλ μλ‘μ΄ μ€λ΅λ°κ° μ λ¨κ² νλΌλ μꡬμ¬νμ΄ μμ μλ μμ΅λλ€.
κ·Έλ°κ²½μ°μλ Channel μ BufferOverFlow μ μ± μ μ μ©ν΄ μ¬μ©νκ±°λ μ΄μ μ μμ² κ°μ λΉκ΅ν΄ μ΄λ₯Ό λ§λ λ°©μμ΄ μκ² μ΅λλ€ γ γ ..
κΈ μ½μ΄ μ£Όμ μ κ°μ¬ν©λλ€ ~!!
λ€μμ λ§ν¬λ₯Ό μ°Έκ³ νμ΅λλ€.
compose snackbar 곡μ λ¬Έμ
compose custom snackbar μν°ν΄