Jetpack Compose Animations: 앱을 살아있게 만드는 4가지 기술

발행: (2026년 3월 2일 오전 09:18 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

Animation은 현대 Android 앱의 심장박동과 같습니다. 정적인 UI를 유동적이고 반응성이 뛰어난 경험으로 바꾸어 사용자에게 자연스럽고 즐거운 느낌을 제공합니다. Jetpack Compose는 Google의 최신 선언형 UI 프레임워크로, 몇 줄의 코드만으로 부드러운 애니메이션을 만들 수 있는 강력한 내장 API를 제공합니다.

이 가이드에서는 Compose에서 앱의 느낌을 평범함에서 뛰어남으로 끌어올릴 수 있는 네 가지 핵심 애니메이션 기법을 단계별로 살펴보겠습니다. 각 기법마다 바로 프로젝트에 적용할 수 있는 실용적인 코드 예제가 함께 제공됩니다.

1. animateFloatAsState: 부드러운 값 변화를 위한 기본

animateFloatAsState는 Compose에서 가장 많이 사용되는 애니메이션 API일 가능성이 높습니다. 현재 상태에서 목표값으로 float 값을 부드럽게 애니메이션하여 투명도 페이드, 스케일 변화, 회전 등에 적합합니다.

사용 사례: 탭 시 애니메이션 버튼 투명도

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha

@Composable
fun AnimatedOpacityButton() {
    var isPressed by remember { mutableStateOf(false) }

    // Animate opacity from 1.0 to 0.5 when pressed
    val alpha by animateFloatAsState(
        targetValue = if (isPressed) 0.5f else 1.0f,
        label = "button_alpha"
    )

    Button(
        onClick = { isPressed = !isPressed },
        modifier = Modifier.alpha(alpha)
    ) {
        Text("Tap me")
    }
}

작동 원리

  • 부드러운 전환 – 기본적으로 300 ms 동안 애니메이션이 실행되어 자연스러운 페이드 효과를 만듭니다.
  • 상태 기반 – 상태 조건이 변경될 때마다 애니메이션이 자동으로 트리거됩니다.
  • 성능 – 저수준 그래픽 API를 사용해 부드러운 60 fps 애니메이션을 제공합니다.

애니메이션 속도 커스터마이징

animationSpec을 사용해 애니메이션 지속 시간을 제어할 수 있습니다:

val alpha by animateFloatAsState(
    targetValue = if (isPressed) 0.5f else 1.0f,
    animationSpec = tween(durationMillis = 500), // Slower fade
    label = "button_alpha"
)

2. AnimatedVisibility: Show/Hide with Polish

animateFloatAsState가 값 변화를 처리하는 반면, AnimatedVisibility는 컴포저블을 뷰 계층에서 들어오고 나가게 하면서 우아한 진입/퇴장 애니메이션을 제공합니다.

Use Case: Animated Error Message

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun LoginForm() {
    var showError by remember { mutableStateOf(false) }
    var email by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") }
        )

        // Error message with slide + fade animation
        AnimatedVisibility(
            visible = showError,
            enter = slideInVertically() + fadeIn(),
            exit = slideOutVertically() + fadeOut()
        ) {
            Text(
                "Invalid email format",
                color = Color.Red,
                modifier = Modifier
                    .background(Color(0xFFFFEBEE))
                    .padding(12.dp)
            )
        }

        Button(onClick = {
            showError = email.isEmpty() || !email.contains("@")
        }) {
            Text("Sign In")
        }
    }
}

Animation Combinations

Compose에서는 여러 애니메이션을 조합할 수 있습니다:

enter = slideInVertically() + fadeIn() + expandVertically()
exit  = slideOutVertically() + fadeOut() + shrinkVertically()

이렇게 하면 오류 메시지가 한 번에 슬라이드 인, 페이드 인, 그리고 확장되는 세련된 효과를 만들 수 있습니다.

3. animateContentSize: 부드러운 레이아웃 변경

컴포저블의 크기가 콘텐츠 업데이트로 인해 변할 때, animateContentSize는 즉시 점프하는 대신 레이아웃 변화를 애니메이션으로 처리합니다.

사용 사례: 설명 텍스트 확장

import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp

@Composable
fun ExpandableText(title: String, fullText: String) {
    var isExpanded by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .clickable { isExpanded = !isExpanded }
            .animateContentSize(animationSpec = tween(durationMillis = 400))
            .padding(16.dp)
    ) {
        Text(
            text = title,
            style = MaterialTheme.typography.headlineSmall
        )

        Text(
            text = fullText,
            maxLines = if (isExpanded) Int.MAX_VALUE else 3,
            overflow = TextOverflow.Ellipsis
        )
    }
}

이 스니펫들을 자유롭게 복사해서 프로젝트에 적용하고, 파라미터를 실험해 보면서 앱의 디자인 언어에 맞게 조정해 보세요.

마법

maxLines3에서 MAX_VALUE로 변경될 때, 컬럼의 높이가 부드럽게 확장됩니다. 갑작스러운 점프 없이—우아한 성장만이 있습니다.

Source:

4. updateTransition + Crossfade: 복합 상태 애니메이션

여러 속성이 동시에 변하는 보다 복잡한 애니메이션의 경우, updateTransition은 모든 애니메이션을 하나의 논리적 단위로 조정합니다.

사용 사례: 스피너 회전이 포함된 로딩 상태

import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

enum class LoadingState {
    Idle, Loading, Success, Error
}

@Composable
fun LoadingIndicator(state: LoadingState) {
    val transition = updateTransition(targetState = state, label = "loading_transition")

    val rotation by transition.animateFloat(label = "rotation") {
        if (it == LoadingState.Loading) 360f else 0f
    }

    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Crossfade(targetState = state, label = "content_crossfade") { loadingState ->
            when (loadingState) {
                LoadingState.Idle -> Text("Ready to load")
                LoadingState.Loading -> CircularProgressIndicator(
                    modifier = Modifier
                        .size(40.dp)
                        .rotate(rotation)
                )
                LoadingState.Success -> Text("Loaded!", color = Color.Green)
                LoadingState.Error -> Text("Error occurred", color = Color.Red)
            }
        }
    }
}

updateTransition이 강력한 이유

  • 조정(Orchestration) – 하나의 상태 변화에 묶인 모든 애니메이션이 동기화되어 실행됩니다.
  • 일관성(Consistency) – 애니메이션 간 충돌을 방지합니다.
  • 성능(Performance) – 애니메이션 값이 변할 때만 재구성하고, 매 프레임마다 재구성하지 않습니다.

보너스: Crossfade로 우아한 콘텐츠 전환

Crossfade는 이전 컴포저블을 서서히 사라지게 하고 새로운 컴포저블을 서서히 나타나게 하여 두 컴포저블 사이의 전환을 애니메이션합니다.

@Composable
fun TabContent(selectedTab: Int) {
    Crossfade(targetState = selectedTab, label = "tab_switch") { tab ->
        when (tab) {
            0 -> HomeScreen()
            1 -> ProfileScreen()
            else -> SettingsScreen()
        }
    }
}

복잡한 진입/퇴장 로직이 필요하지 않으며, Crossfade가 시각적 우아함을 자동으로 처리합니다.

성능 팁

  • label 파라미터 사용 – Android Studio의 Animation Inspector에서 디버깅에 도움이 됩니다.
  • 단순 값에는 animateFloatAsState 선호 – 최적화되고 가볍습니다.
  • 초기 컴포지션 중 애니메이션 피하기 – 애니메이션이 시작되기 전에 상태가 안정되어야 합니다.
  • 실제 디바이스에서 테스트 – 에뮬레이터는 애니메이션 성능을 정확히 나타내지 못합니다.

모두 합쳐 보기

각 기법은 특정 애니메이션 문제를 해결하며, 네 가지를 모두 마스터하면 Compose 앱이 진정으로 즐거운 느낌을 줄 것입니다.

기법이상적인 사용
animateFloatAsState빠른 상태 피드백 (버튼 클릭, 토글)
AnimatedVisibility화면 및 다이얼로그 진입/퇴장
animateContentSize유기적인 콘텐츠 성장 (리스트 확장, 텍스트)
updateTransition복잡하고 동기화된 상태 변화

다음 단계

모든 8개의 템플릿은 애니메이션을 위한 깔끔한 Compose UI를 사용합니다:

다음 기능에서 animateFloatAsState를 구현하는 것으로 시작하고, 앱의 복잡성이 증가함에 따라 다른 기술들을 차례로 탐색하세요.

즐거운 애니메이션!

0 조회
Back to Blog

관련 글

더 보기 »