핵심 요점
Easing(스무딩 함수)은 선형 시간 t ∈ [0,1]을 비선형 애니메이션 값에 매핑하는 수학 함수 f(t)입니다. 애니메이션이 500ms 동안 객체의 위치를 A에서 B로 변경하는 경우, 이징은 각 시간 지점에서 객체가 이 지점들 사이를 어떻게 이동하는지 결정합니다. 선형 이징(f(t)=t)은 일정한 속도를 의미하며 객체가 기계적으로 움직입니다. 비선형 이징은 자연스럽게 느껴집니다. 객체가 가속(ease-in), 감속(ease-out) 또는 부드러운 시작과 끝(ease-in-out)을 갖습니다.
이징 함수는 3차 베지어 곡선으로 설명되며, X축은 정규화된 시간, Y축은 정규화된 진행입니다. 두 개의 제어점(P1, P2)이 곡선 모양을 정의합니다. 표준 곡선은 CSS 사양에 의해 고정됩니다: ease(0.25, 0.1, 0.25, 1.0), ease-in(0.42, 0.0, 1.0, 1.0), ease-out(0.0, 0.0, 0.58, 1.0), ease-in-out(0.42, 0.0, 0.58, 1.0). 이 동일한 값이 iOS의 CAMediaTimingFunction과 Android의 PathInterpolator에서 사용됩니다. Material Design Motion Guidelines에 따르면, 이징 곡선은 컨텍스트와 일치해야 합니다: 나타날 때는 ease-out(빠른 시작), 사라질 때는 ease-in(빠른 끝).
iOS에서 이징은 주로 Core Animation의 CAMediaTimingFunction 클래스를 통해 작동합니다. 이 클래스는 두 개의 제어점(c1x, c1y, c2x, c2y)으로 3차 베지어 곡선을 받아 CAAnimation(CABasicAnimation, CAKeyframeAnimation)에 적용합니다. CAMediaTimingFunction은 5개의 미리 정의된 함수를 지원합니다: linear, easeIn, easeOut, easeInEaseOut 및 default(easeInEaseOut과 동일).
CAMediaTimingFunction은 CALayer 수준에서 작동하며, 애니메이션은 별도의 렌더 서버 프로세스에서 실행되어 복잡한 변환에서도 60fps를 보장합니다. UIView 속성(frame, alpha, transform) 애니메이션의 경우 UIKit은 options 매개변수 .curveEaseInOut과 함께 UIView.animate를 사용하며, 내부적으로 해당 곡선으로 CAMediaTimingFunction도 생성합니다. iOS 17+에서는 사용자 정의 베지어를 위한 UIViewAnimationCurve.custom과 BlockParameters가 추가되어 사용자 정의 CAAnimation을 작성할 필요가 없어졌습니다. CAMediaTimingFunction은 UIKit Dynamics 및 UIScrollView의 감속(UIScrollViewDecelerationRate)에도 사용됩니다.
SwiftUI에서 이징은 Animation 구조체를 통해 .easeIn, .easeOut, .easeInOut, .spring 및 .interpolatingSpring 수식자로 설정됩니다. 애니메이션이 절차적(beginAnimations/commitAnimations)인 UIKit과 달리, SwiftUI는 선언적 접근 방식을 사용합니다: 애니메이션은 변경되는 상태 속성이며 프레임워크가 전환을 애니메이션하는 방법을 결정합니다.
.timingCurve(c0x, c0y, c1x, c1y) 수식자는 3차 베지어 제어점을 직접 받아들이며, 이는 CAMediaTimingFunction의 완전한 유사체입니다. SwiftUI Animation은 미리 정의된 곡선을 정적 속성으로 지원합니다: Animation.easeInOut은 ease-in-out(0.42, 0.0, 0.58, 1.0)과 동일합니다. 스프링 애니메이션의 경우 Animation.spring(response:dampingFraction:blendDuration)이 사용되며, dampingFraction < 1이 감쇠가 있는 스프링 효과를 만듭니다. WWDC 2023에서 Apple은 Animation.keyframes를 발표했습니다. 이는 각 키프레임에서 사용자 정의 이징을 사용한 프레임별 애니메이션으로, 궤적을 완전히 제어할 수 있습니다.
Android에서 이징은 PathInterpolator(API 21+)를 통해 구현됩니다. 이는 3차 베지어 곡선이 있는 Path 객체를 받는 클래스입니다. 제어점은 Path.cubicTo(x1, y1, x2, y2, x3, y3)를 통해 지정되며, (x1,y1)은 첫 번째 제어점, (x2,y2)는 두 번째, (x3,y3)는 끝점(항상 1,1)입니다. 사용자 정의 곡선이 필요한 경우 PathInterpolator가 필수입니다. 표준 시나리오의 경우 AccelerateDecelerateInterpolator(ease-in-out), AccelerateInterpolator(ease-in) 및 DecelerateInterpolator(ease-out)를 사용할 수 있습니다.
Jetpack Compose에서 이징은 androidx.compose.animation 패키지의 Easing 객체를 통해 설정됩니다. FastOutSlowInEasing, LinearOutSlowInEasing, FastOutLinearInEasing 및 CubicBezierEasing을 통한 사용자 정의 Easing을 사용할 수 있습니다. Compose Easing은 animate*AsState, AnimatedVisibility 및 AnimatedContent에 적용됩니다. View 시스템과의 주요 차이점: Compose는 Kotlin 쪽에서 이징을 보간하며(RenderNode가 아님), 사용자 정의 애니메이션에 더 많은 유연성을 제공하지만 120Hz 디스플레이에서 최적화가 필요합니다.
사용자 정의 ease-in-out 곡선을 사용한 기본 레이어 위치 애니메이션입니다. CAMediaTimingFunction은 움직임의 부드러운 시작과 끝을 정의하는 3차 베지어 제어점을 받습니다.
import UIKit
func animateLayerPosition() {
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = CGPoint(x: 0, y: 0)
animation.toValue = CGPoint(x: 300, y: 400)
animation.duration = 0.8
let easing = CAMediaTimingFunction(
controlPoints: 0.42, 0.0, 0.58, 1.0
)
animation.timingFunction = easing
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
layer.add(animation, forKey: "positionAnimation")
}
제어점 (0.42, 0.0, 0.58, 1.0)은 표준 ease-in-out 곡선을 정의합니다. SwiftUI는 Animation.easeInOut에 동일한 점을 사용합니다. ease-in의 경우 (0.42, 0.0, 1.0, 1.0)을, ease-out의 경우 (0.0, 0.0, 0.58, 1.0)을 사용합니다. Core Animation은 렌더 서버에서 60/120fps로 애니메이션을 실행합니다.
사용자 정의 곡선과 스프링 효과를 사용한 SwiftUI의 선언적 애니메이션입니다. 애니메이션 수식자는 isAnimated 변경에 바인딩됩니다.
import SwiftUI
struct EasingExampleView: View {
@State private var isAnimated = false
var body: some View {
VStack {
Circle()
.fill(Color.blue)
.frame(width: 60, height: 60)
.offset(x: isAnimated ? 100 : 0, y: 0)
.animation(
.timingCurve(0.42, 0.0, 0.58, 1.0, duration: 0.6),
value: isAnimated
)
Button("Toggle") {
isAnimated.toggle()
}
}
}
}
.timingCurve(c0x, c0y, c1x, c1y, duration) 수식자는 SwiftUI에서 사용자 정의 이징 애니메이션을 만듭니다. value 매개변수는 애니메이션을 특정 상태에 바인딩합니다. isAnimated가 변경되면 SwiftUI가 자동으로 곡선을 적용합니다. 스프링 애니메이션의 경우 timingCurve 대신 .spring(response: 0.4, dampingFraction: 0.6)을 사용합니다.
PathInterpolator를 통한 Android의 ease-in-out에 해당합니다. 제어점(0.42, 0.0, 0.58, 1.0)은 iOS와 동일한 곡선을 정의합니다.
import android.animation.ObjectAnimator
import android.graphics.Path
import android.view.animation.PathInterpolator
import android.view.View
fun View.fadeInWithEase() {
val path = Path().apply {
cubicTo(0.42f, 0.0f, 0.58f, 1.0f, 1.0f, 1.0f)
}
val interpolator = PathInterpolator(path)
ObjectAnimator.ofFloat(this, "alpha", 0f, 1f).apply {
duration = 500
interpolator = interpolator
start()
}
}
제어점(0.42, 0.0, 0.58, 1.0)이 있는 PathInterpolator는 iOS의 CAMediaTimingFunction ease-in-out과 동일합니다. 크로스 플랫폼 프로젝트의 경우 동일한 제어점을 사용하여 시각적으로 동일한 애니메이션 동작을 보장합니다. Material Design은 Android 특정 인터페이스의 대안으로 FastOutSlowInInterpolator(0.4, 0.0, 0.2, 1.0)를 권장합니다.
Compose에서 이징은 동일한 제어점으로 CubicBezierEasing을 통해 설정됩니다. 결과는 투명도와 오프셋이 있는 부드러운 요소 나타남입니다.
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
@Composable
fun EasingComposable() {
var visible by remember { mutableStateOf(false) }
val easing = CubicBezierEasing(0.42f, 0.0f, 0.58f, 1.0f)
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = tween(durationMillis = 500, easing = easing)
)
Column {
Button(onClick = { visible = !visible }) {
Text("Toggle")
}
Box(
modifier = Modifier
.size(100.dp)
.graphicsLayer(alpha = alpha)
.background(MaterialTheme.colorScheme.primary)
)
}
}
Compose의 CubicBezierEasing은 동일한 4개의 제어점 매개변수를 받습니다. animateFloatAsState는 지정된 곡선으로 alpha 값을 자동으로 보간합니다. 비교: Compose의 FastOutSlowInEasing은 (0.4, 0.0, 0.2, 1.0)과 동일하며, 이는 표준 Material Design 3 곡선입니다. CubicBezierEasing을 통한 사용자 정의 곡선은 브랜드 애니메이션을 완전히 제어할 수 있습니다.
자주 묻는 질문
Easing은 iOS(CAMediaTimingFunction), 웹(CSS ease-in-out) 및 디자인에서 사용되는 스무딩 함수의 일반적인 개념입니다. Interpolator는 Android(TimeInterpolator)에서 이 개념의 구현입니다. 본질적으로 동일한 것이며, 애니메이션 속도의 수학적 곡선입니다. 차이는 이름과 플랫폼 API에만 있습니다. 크로스 플랫폼 프로젝트에서는 easing이라는 용어가 범용적으로 사용됩니다.
iOS는 5개의 미리 정의된 함수(linear, easeIn, easeOut, easeInEaseOut, custom(3차 베지어))가 있는 CAMediaTimingFunction을 제공합니다. SwiftUI에서는 Animation.easeIn, Animation.easeOut, Animation.easeInOut, Animation.spring 및 Animation.interpolatingSpring을 사용할 수 있습니다. 사용자 정의 곡선의 경우 Animation.timingCurve(c0x, c0y, c1x, c1y, duration:)를 사용합니다.
네, 동일한 3차 베지어 곡선을 설정하면 가능합니다. iOS의 CAMediaTimingFunction과 Android의 PathInterpolator는 3차 베지어 제어점(x1, y1, x2, y2)을 받습니다. 예를 들어, ease-in-out(0.42, 0.0, 0.58, 1.0) 곡선은 두 플랫폼에서 동일한 동작을 제공합니다. Material Design은 크로스 플랫폼 애니메이션에 이 접근 방식을 사용합니다.
Material Design Motion Guidelines을 사용하세요: 요소 나타남에는 ease-out(빠른 시작, 부드러운 끝), 사라짐에는 ease-in(빠른 끝), 화면 간 이동에는 ease-in-out을 사용합니다. 대화형 요소(버튼, 카드)에는 스프링 애니메이션을 사용합니다. linear는 부자연스러워 보이므로 피하세요.
끊김(잉크)은 애니메이션이 60fps에 도달하지 못할 때 발생합니다. 확인하세요: iOS에서 CAMediaTimingFunction(타이머 아님)을 사용하고 있나요? Android에서 PathInterpolator(Handler.post 아님)를 사용하고 있나요? 메인 스레드에서의 무거운 계산, 복잡한 레이어 계층 구조 또는 큰 drawable이 프레임 드롭을 유발할 수 있습니다. iOS에서는 복잡한 레이어에 shouldRasterize와 함께 Core Animation을 사용하고, Android에서는 하드웨어 가속과 GPU 렌더링을 사용합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.