# Jetpack Compose: 고급 애니메이션 단계별 가이드 > Compose 고급 애니메이션 완벽 가이드: 전환, AnimatedVisibility, Animatable, 제스처, 부드러운 Android 인터페이스를 위한 성능. - Published: 2026-02-08 - Updated: 2026-04-28 - Author: SharpSkill - Tags: jetpack compose, android, animations, kotlin, ui - Reading time: 12 min --- 애니메이션은 기능적인 애플리케이션을 기억에 남는 사용자 경험으로 바꿉니다. Jetpack Compose는 부드러운 인터페이스 제작을 크게 단순화하는 강력한 선언형 애니메이션 API를 제공합니다. 이 가이드는 성능이 뛰어나고 유지보수가 쉬운 애니메이션을 구축하기 위한 고급 기법을 다룹니다. > **사전 준비** > > 이 튜토리얼은 Compose의 기본 개념(리컴포지션, 상태, Modifier)에 대한 이해를 전제로 합니다. 기초를 다지려면 먼저 Jetpack Compose 면접 질문 가이드를 참고하시기 바랍니다. ## Compose 애니메이션 API의 기본 Compose는 애니메이션을 위한 여러 단계의 API를 제공합니다. 어떤 API를 선택할지는 원하는 제어 수준과 애니메이션의 복잡도에 따라 달라집니다. API는 크게 세 가지 범주로 나뉩니다. 고수준 애니메이션(`AnimatedVisibility`, `AnimatedContent`), 상태 기반 애니메이션(`animate*AsState`), 저수준 애니메이션(`Animatable`, `Transition`)입니다. ```kotlin // AnimationLevels.kt // Overview of the three animation API levels @Composable fun AnimationApiOverview() { // High level: simple predefined animations AnimatedVisibility(visible = isVisible) { Text("Animated content") } // Intermediate level: state-driven animation val alpha by animateFloatAsState( targetValue = if (isSelected) 1f else 0.5f, label = "alpha" ) // Low level: full control over animation val animatable = remember { Animatable(0f) } LaunchedEffect(targetValue) { animatable.animateTo(targetValue) } } ``` 적절한 API 단계를 선택하는 것은 필요한 유연성을 유지하면서도 가독성 있는 코드를 작성하는 데 매우 중요합니다. ## AnimatedVisibility: 우아한 등장과 퇴장 애니메이션 `AnimatedVisibility`는 요소의 등장과 사라짐을 애니메이션화하기에 이상적인 시작점입니다. 이 API는 콘텐츠의 컴포지션과 디컴포지션을 자동으로 처리합니다. `enter`와 `exit` 매개변수는 애니메이션 동작을 정의하는 전환의 조합을 받습니다. 이러한 전환은 `+` 연산자로 결합할 수 있습니다. ```kotlin // AnimatedVisibilityExample.kt @Composable fun ExpandableCard( title: String, content: String, modifier: Modifier = Modifier ) { var isExpanded by remember { mutableStateOf(false) } Card( modifier = modifier.clickable { isExpanded = !isExpanded } ) { Column(modifier = Modifier.padding(16.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text(text = title, style = MaterialTheme.typography.titleMedium) Icon( imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, contentDescription = null ) } // Expansion animation with fade + slide AnimatedVisibility( visible = isExpanded, enter = fadeIn(animationSpec = tween(300)) + expandVertically(animationSpec = tween(300)), exit = fadeOut(animationSpec = tween(200)) + shrinkVertically(animationSpec = tween(200)) ) { Text( text = content, modifier = Modifier.padding(top = 12.dp), style = MaterialTheme.typography.bodyMedium ) } } } } ``` `AnimatedVisibility` 내부의 콘텐츠는 `visible = true`일 때만 컴포지션되므로, 확장 가능한 항목이 많은 리스트에서 성능이 최적화됩니다. > **전환 결합하기** > > 사용 가능한 전환에는 `fadeIn/fadeOut`, `slideIn/slideOut`, `expandIn/shrinkOut`, `scaleIn/scaleOut` 등이 있습니다. 자유롭게 결합하여 맞춤 효과를 만들 수 있습니다. ## animate*AsState: 상태 주도 애니메이션 `animate*AsState` 계열의 함수는 원시 값의 변화를 자동으로 애니메이션화합니다. Compose에서 단순한 애니메이션을 구현하는 가장 관용적인 접근 방식입니다. 각 데이터 타입에는 전용 함수가 있습니다. `animateColorAsState`, `animateFloatAsState`, `animateDpAsState`, `animateIntAsState` 등입니다. ```kotlin // AnimateAsStateExample.kt @Composable fun InteractiveButton( isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier ) { // Animated background color val backgroundColor by animateColorAsState( targetValue = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, animationSpec = tween(durationMillis = 250), label = "backgroundColor" ) // Animated elevation val elevation by animateDpAsState( targetValue = if (isSelected) 8.dp else 2.dp, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ), label = "elevation" ) // Animated text size val textSize by animateFloatAsState( targetValue = if (isSelected) 18f else 14f, label = "textSize" ) Surface( modifier = modifier.clickable(onClick = onClick), color = backgroundColor, shadowElevation = elevation, shape = RoundedCornerShape(12.dp) ) { Text( text = if (isSelected) "Selected" else "Select", modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp), fontSize = textSize.sp ) } } ``` `animationSpec` 매개변수는 애니메이션의 시간적 동작을 제어합니다. 가장 많이 사용되는 두 가지 spec은 `tween`(이징이 적용된 고정 지속 시간)과 `spring`(바운스가 있는 사실적인 물리)입니다. ## Transition: 여러 애니메이션 조율하기 여러 속성을 협조적으로 애니메이션화해야 할 때 `updateTransition`은 중앙 집중식 제어를 제공합니다. 이 API는 모든 애니메이션이 동기화 상태를 유지하도록 보장합니다. 패턴은 enum 상태를 정의한 뒤, 그 상태에 의존하는 각 속성에 대해 애니메이션을 만드는 것입니다. ```kotlin // TransitionExample.kt // Card state: defines visual behavior enum class CardState { Collapsed, Expanded, Selected } @Composable fun AnimatedStateCard( cardState: CardState, modifier: Modifier = Modifier ) { // Central transition coordinating all animations val transition = updateTransition( targetState = cardState, label = "cardTransition" ) // Card height based on state val cardHeight by transition.animateDp( transitionSpec = { spring(stiffness = Spring.StiffnessLow) }, label = "height" ) { state -> when (state) { CardState.Collapsed -> 80.dp CardState.Expanded -> 200.dp CardState.Selected -> 160.dp } } // Border color based on state val borderColor by transition.animateColor( transitionSpec = { tween(300) }, label = "borderColor" ) { state -> when (state) { CardState.Collapsed -> Color.Transparent CardState.Expanded -> MaterialTheme.colorScheme.outline CardState.Selected -> MaterialTheme.colorScheme.primary } } // Corner radius based on state val cornerRadius by transition.animateDp( label = "cornerRadius" ) { state -> when (state) { CardState.Collapsed -> 8.dp CardState.Expanded -> 16.dp CardState.Selected -> 24.dp } } Card( modifier = modifier .height(cardHeight) .border(2.dp, borderColor, RoundedCornerShape(cornerRadius)), shape = RoundedCornerShape(cornerRadius) ) { // Card content } } ``` ## Animatable: 애니메이션 완전 제어 `Animatable`은 완전한 프로그래밍적 제어를 제공하는 저수준 API입니다. 이 접근 방식은 중단 가능한 애니메이션, 제스처, 복잡한 시나리오에 필요합니다. `animate*AsState`와 달리, `Animatable`은 진행 중인 애니메이션을 완료될 때까지 기다리지 않고 멈추거나 되돌리거나 수정할 수 있습니다. ```kotlin // AnimatableExample.kt @Composable fun SwipeableCard( onDismiss: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit ) { // Horizontal offset controlled by Animatable val offsetX = remember { Animatable(0f) } val scope = rememberCoroutineScope() // Swipe threshold to trigger dismissal val dismissThreshold = 300f Box( modifier = modifier .offset { IntOffset(offsetX.value.roundToInt(), 0) } .pointerInput(Unit) { detectHorizontalDragGestures( onDragEnd = { scope.launch { if (abs(offsetX.value) > dismissThreshold) { // Animate out then callback val target = if (offsetX.value > 0) 1000f else -1000f offsetX.animateTo( targetValue = target, animationSpec = tween(200) ) onDismiss() } else { // Return to initial position with spring offsetX.animateTo( targetValue = 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy ) ) } } }, onHorizontalDrag = { _, dragAmount -> scope.launch { // snapTo for instant finger tracking offsetX.snapTo(offsetX.value + dragAmount) } } ) } ) { content() } } ``` `Animatable`의 핵심 메서드는 `animateTo()`(목표값까지 애니메이션), `snapTo()`(즉시 변경), `stop()`(중단)입니다. ## AnimatedContent: 콘텐츠 전환 `AnimatedContent`는 서로 다른 콘텐츠 사이의 전환을 애니메이션화합니다. 이 API는 표시되는 UI를 완전히 바꾸는 상태 변화에 적합합니다. `targetState` 키는 전환이 언제 일어날지 결정합니다. `transitionSpec`은 나가는 콘텐츠와 들어오는 콘텐츠가 어떻게 상호작용할지 정의합니다. ```kotlin // AnimatedContentExample.kt @Composable fun CounterWithAnimation( count: Int, modifier: Modifier = Modifier ) { AnimatedContent( targetState = count, modifier = modifier, transitionSpec = { // Determine animation direction val direction = if (targetState > initialState) { // New number enters from top slideInVertically { height -> -height } + fadeIn() togetherWith slideOutVertically { height -> height } + fadeOut() } else { // New number enters from bottom slideInVertically { height -> height } + fadeIn() togetherWith slideOutVertically { height -> -height } + fadeOut() } direction.using(SizeTransform(clip = false)) }, label = "counter" ) { targetCount -> Text( text = "$targetCount", style = MaterialTheme.typography.displayLarge, fontWeight = FontWeight.Bold ) } } ``` > **AnimatedContent의 성능** > > `AnimatedContent` 내부의 콘텐츠는 `targetState`가 바뀔 때마다 리컴포지션됩니다. 복잡한 콘텐츠라면 비용이 큰 요소를 메모이제이션하거나 적절한 키 전략을 사용하시기 바랍니다. ## rememberInfiniteTransition으로 무한 애니메이션 반복되는 애니메이션(로딩 인디케이터, 펄스 효과 등)에는 `rememberInfiniteTransition`이 별도의 사이클 관리 없이도 사용할 수 있는 전용 API를 제공합니다. ```kotlin // InfiniteTransitionExample.kt @Composable fun PulsingDot( color: Color = MaterialTheme.colorScheme.primary, modifier: Modifier = Modifier ) { val infiniteTransition = rememberInfiniteTransition(label = "pulse") // Looping scale animation val scale by infiniteTransition.animateFloat( initialValue = 0.8f, targetValue = 1.2f, animationSpec = infiniteRepeatable( animation = tween(600, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Reverse ), label = "scale" ) // Synchronized opacity animation val alpha by infiniteTransition.animateFloat( initialValue = 0.5f, targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(600, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Reverse ), label = "alpha" ) Box( modifier = modifier .size(24.dp) .scale(scale) .alpha(alpha) .background(color = color, shape = CircleShape) ) } ``` ## LazyColumn 리스트 애니메이션 리스트 항목 애니메이션은 특별한 주의가 필요합니다. `animateItem()` Modifier(이전 명칭은 `animateItemPlacement`)는 재정렬을 자동으로 애니메이션화합니다. ```kotlin // ListAnimationExample.kt @Composable fun AnimatedTaskList( tasks: List, onToggle: (Task) -> Unit, onDelete: (Task) -> Unit, modifier: Modifier = Modifier ) { LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp) ) { items( items = tasks, key = { it.id } // Stable key required for animateItem ) { task -> var isVisible by remember { mutableStateOf(true) } // Exit animation before deletion AnimatedVisibility( visible = isVisible, exit = shrinkVertically() + fadeOut() ) { TaskItem( task = task, onToggle = { onToggle(task) }, onDelete = { isVisible = false // Delay to let animation complete }, modifier = Modifier.animateItem( fadeInSpec = tween(300), fadeOutSpec = tween(300), placementSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ) ) ) } // Trigger deletion after animation LaunchedEffect(isVisible) { if (!isVisible) { delay(300) onDelete(task) } } } } } ``` > **리스트 애니메이션을 위한 안정적인 key** > > 안정적인 `key` 매개변수가 없으면 `animateItem`은 리컴포지션 사이에서 항목을 추적할 수 없습니다. 리스트 인덱스 대신 고유한 ID를 사용하시기 바랍니다. ## Canvas 애니메이션 맞춤 시각 효과에는 `Canvas`와 애니메이션 값을 결합하면 완전한 유연성을 얻을 수 있습니다. ```kotlin // CanvasAnimationExample.kt @Composable fun AnimatedProgressRing( progress: Float, // 0f to 1f modifier: Modifier = Modifier ) { // Progress animation with spring for natural feel val animatedProgress by animateFloatAsState( targetValue = progress, animationSpec = spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessVeryLow ), label = "progress" ) // Continuous rotation animation val infiniteTransition = rememberInfiniteTransition(label = "rotation") val rotation by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( animation = tween(2000, easing = LinearEasing) ), label = "rotation" ) val primaryColor = MaterialTheme.colorScheme.primary val trackColor = MaterialTheme.colorScheme.surfaceVariant Canvas( modifier = modifier .size(120.dp) .rotate(rotation) ) { val strokeWidth = 12.dp.toPx() val radius = (size.minDimension - strokeWidth) / 2 // Background circle (track) drawCircle( color = trackColor, radius = radius, style = Stroke(width = strokeWidth, cap = StrokeCap.Round) ) // Animated progress arc drawArc( color = primaryColor, startAngle = -90f, sweepAngle = animatedProgress * 360f, useCenter = false, style = Stroke(width = strokeWidth, cap = StrokeCap.Round), topLeft = Offset(strokeWidth / 2, strokeWidth / 2), size = Size(radius * 2, radius * 2) ) } } ``` ## 애니메이션 성능 최적화 잘못 최적화된 애니메이션은 잰크(jank)를 유발하고 배터리를 소모시킵니다. 60 FPS를 유지하기 위한 모범 사례를 소개합니다. 첫 번째 규칙은 애니메이션 도중 할당을 피하는 것입니다. 리컴포지션을 유발하는 Modifier 대신 `graphicsLayer`를 사용해야 합니다. ```kotlin // PerformanceOptimization.kt @Composable fun OptimizedAnimatedCard( isExpanded: Boolean, modifier: Modifier = Modifier ) { val scale by animateFloatAsState( targetValue = if (isExpanded) 1.1f else 1f, label = "scale" ) val alpha by animateFloatAsState( targetValue = if (isExpanded) 1f else 0.8f, label = "alpha" ) Card( modifier = modifier // ✅ graphicsLayer: GPU modifications without recomposition .graphicsLayer { scaleX = scale scaleY = scale this.alpha = alpha } // ❌ Avoid: .scale(scale).alpha(alpha) // These modifiers trigger recompositions ) { Text("Card content") } } // Example with remembered lambda to avoid allocations @Composable fun OptimizedClickableItem( onClick: () -> Unit, content: @Composable () -> Unit ) { // ✅ Stable remembered lambda val interactionSource = remember { MutableInteractionSource() } Box( modifier = Modifier .clickable( interactionSource = interactionSource, indication = ripple(), onClick = onClick ) ) { content() } } ``` 두 번째 핵심 포인트는 리스트 안의 애니메이션입니다. 동시 애니메이션 수를 제한하고 파생 계산에는 `derivedStateOf`를 사용해야 합니다. ```kotlin // ListPerformance.kt @Composable fun PerformantAnimatedList( items: List, modifier: Modifier = Modifier ) { // Calculate once whether the list is empty val isEmpty by remember { derivedStateOf { items.isEmpty() } } LazyColumn(modifier = modifier) { items( items = items, key = { it.id } ) { item -> // Lightweight animation only on initial appearance var hasAppeared by remember { mutableStateOf(false) } LaunchedEffect(Unit) { hasAppeared = true } val alpha by animateFloatAsState( targetValue = if (hasAppeared) 1f else 0f, animationSpec = tween(200), label = "itemAlpha" ) ItemCard( item = item, modifier = Modifier.graphicsLayer { this.alpha = alpha } ) } } } ``` ## 결론 Jetpack Compose의 애니메이션은 사용 편의성과 고급 제어 사이의 균형을 제공합니다. 기억해야 할 핵심 포인트는 다음과 같습니다. - ✅ 복잡도에 따라 적절한 API 단계를 선택하기 (AnimatedVisibility → animate*AsState → Animatable) - ✅ 관련된 여러 애니메이션을 조율할 때는 `updateTransition` 사용하기 - ✅ 자연스러운 애니메이션에는 `spring`, 정확한 지속 시간에는 `tween` 선호하기 - ✅ 리스트 애니메이션에는 항상 안정적인 `key` 매개변수 제공하기 - ✅ 불필요한 리컴포지션을 피하기 위해 `graphicsLayer`로 최적화하기 - ✅ 성능을 검증하기 위해 실제 기기에서 애니메이션 테스트하기 Compose 애니메이션을 능숙하게 다루는 것은 프로페셔널한 Android 애플리케이션을 차별화하는 요소입니다. 이러한 기법과 성능에 대한 주의가 결합되면 부드럽고 매력적인 사용자 경험을 만들 수 있습니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/android/jetpack-compose-advanced-animations