Android에서 Lottie 사용하기 — XML부터 Compose까지
Android에서 Lottie를 쓰는 두 가지 트랙 — 전통적인 LottieAnimationView와 Jetpack Compose 전용 API — 를 모두 다룹니다.
설치
// build.gradle.kts (View 기반)
dependencies {
implementation("com.airbnb.android:lottie:6.6.0")
}
// Compose를 쓴다면
dependencies {
implementation("com.airbnb.android:lottie-compose:6.6.0")
}
XML — LottieAnimationView
에셋을 src/main/assets/(또는 res/raw/)에 넣고:
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie"
android:layout_width="240dp"
android:layout_height="240dp"
app:lottie_fileName="onboarding_hero.json"
app:lottie_autoPlay="true"
app:lottie_loop="true" />
코드에서 제어할 때:
binding.lottie.apply {
speed = 1.5f
setMinAndMaxFrame(0, 30)
addAnimatorUpdateListener { /* progress */ }
playAnimation()
}
Jetpack Compose — lottie-compose
@Composable
fun LoadingIndicator() {
val composition by rememberLottieComposition(
LottieCompositionSpec.Asset("loading.json")
)
val progress by animateLottieCompositionAsState(
composition,
iterations = LottieConstants.IterateForever,
)
LottieAnimation(
composition = composition,
progress = { progress },
modifier = Modifier.size(120.dp),
)
}
LottieCompositionSpec은 Asset, RawRes, Url, JsonString을 지원하고, .lottie(dotLottie) 파일도 zip으로 로드할 수 있습니다.
동적 색상 변경 — dynamic properties
val dynamicProperties = rememberLottieDynamicProperties(
rememberLottieDynamicProperty(
property = LottieProperty.COLOR,
value = MaterialTheme.colorScheme.primary.toArgb(),
keyPath = arrayOf("**", "Fill 1", "Color"),
)
)
LottieAnimation(composition, { progress },
dynamicProperties = dynamicProperties)
keyPath에 들어갈 레이어 이름은 디자이너와 약속하거나, 에셋을 Lottie Inspector에 넣어 레이어 구조를 확인한 뒤 정하세요.
성능 체크포인트
- 하드웨어 가속 — 기본은 자동 판단이지만, 복잡한 씬에서
renderMode = RenderMode.HARDWARE를 명시하면 개선되는 경우가 있습니다. 단, 일부 블렌드 모드는 소프트웨어에서만 정확합니다. - RecyclerView — 셀 재활용 시
cancelAnimation()을 잊으면 백그라운드에서 계속 돌며 배터리를 먹습니다. - 저사양 기기 — 같은 파일도 보급형 기기에선 프레임 드랍이 납니다. 레이어 수가 과도한 에셋은 검수 단계에서 걸러내는 것이 근본 해결책입니다.
- 초기 로드 지연 — 큰 JSON 파싱은 수십 ms가 걸립니다. 스플래시 등 첫 화면 에셋은
LottieCompositionFactory로 미리 캐시하세요.
XML vs Compose, 실제로 골라야 할 때
새 프로젝트라면 고민할 것 없이 lottie-compose로 통일하면 됩니다. 문제는 실무 코드베이스 대부분이 그렇지 않다는 점입니다. 기존 View 화면과 신규 Compose 화면이 섞인 앱이라면 "어느 API를 표준으로 삼을지"를 먼저 정해 두는 편이 좋습니다. 제가 협업한 프로젝트에서는 다음 기준으로 정리했습니다.
- 화면 단위로 따라갑니다 — 해당 화면이 View면
LottieAnimationView, Compose면lottie-compose. 한 화면 안에서AndroidView로 View 기반 Lottie를 감싸는 혼용은 재생 상태를 양쪽에서 동기화하는 코드가 계속 늘어나 결국 정리 대상이 됐습니다. - 재생 상태를 코드가 소유해야 하면 Compose가 편합니다 —
animateLottieCompositionAsState가 진행률을 State로 노출하므로, 스크롤 위치나 버튼 상태에 애니메이션을 묶는 코드가 눈에 띄게 짧아집니다. - autoPlay에 loop만 도는 장식용이라면 차이가 없습니다 — 이 경우 멀쩡한 XML 화면을 Compose로 바꿀 이유는 없습니다.
두 API 모두 내부적으로 같은 렌더러(LottieDrawable)를 쓰기 때문에 그림 자체는 동일하게 나옵니다. 선택 기준은 "어떻게 그려지는가"가 아니라 "재생 상태를 어디서 관리하는가"입니다.
자주 겪는 에러와 해결
assets 경로 실수 — 조용히 실패하는 로드
가장 흔한 사고입니다. 파일을 res/raw/에 두고 assets용 속성인 lottie_fileName을 쓰거나, 반대로 assets/에 두고 RawRes로 로드하는 경우입니다. assets 방식은 파일명이 문자열이라 컴파일 타임 검증이 없어서 오타가 나도 빌드는 통과하고, 런타임에 가서야 실패합니다. 그래서 저는 새 코드에는 가급적 R.raw 참조를 쓰는 쪽을 권합니다 — 파일이 없으면 빌드가 깨지므로 실수가 배포까지 가지 않습니다.
RecyclerView 재활용 시 애니메이션 잔상
목록 셀에 Lottie를 넣으면 스크롤 중 이전 아이템의 애니메이션이 새 아이템 위에서 이어 도는 잔상을 겪게 됩니다. 원인은 뷰 재활용입니다 — onBindViewHolder에서 새 에셋을 지정해도 이전 재생 상태와 리스너가 뷰에 남아 있기 때문입니다.
override fun onViewRecycled(holder: VH) {
holder.binding.lottie.cancelAnimation()
holder.binding.lottie.progress = 0f
holder.binding.lottie.removeAllAnimatorListeners()
}
재활용 시점에 이렇게 확실히 정리하고, 바인딩 시점에는 setAnimation 후 필요한 셀에서만 playAnimation()을 다시 호출하는 구조로 바꾸면 잔상과 백그라운드 재생 문제가 같이 사라집니다.
하드웨어 가속에서 깨지는 블렌드 모드
스크린·멀티플라이 같은 블렌드 모드가 들어간 에셋은 RenderMode.HARDWARE에서 색이 다르게 나오거나 검게 뭉개질 수 있습니다. 하드웨어 레이어가 일부 블렌드 연산을 지원하지 않기 때문입니다. RenderMode.SOFTWARE로 바꿔 비교했을 때 결과가 달라지면 블렌드 모드가 원인입니다. 다만 소프트웨어 렌더는 CPU 비용이 커서 임시방편에 가깝고, 근본 해결은 디자이너에게 블렌드 모드를 빼고 다시 내보내 달라고 요청하는 쪽입니다. 문제 레이어를 특정할 때는 Lottie Inspector에 파일을 넣어 레이어 구조를 같이 보면 대화가 훨씬 빨라집니다.
저사양 기기 프레임 드랍, 감이 아니라 측정으로
"버벅이는 것 같다"는 느낌만으로는 에셋 탓인지 코드 탓인지 가릴 수 없습니다. 개발자 옵션의 GPU 렌더링 프로파일을 켜서 막대가 16ms 기준선을 넘는지 보거나, adb shell dumpsys gfxinfo <패키지명>으로 janky frames 비율을 뽑아 보세요. 제 경험상 플래그십에서 멀쩡하던 파일이 보급형에서만 프레임 타임이 튀는 경우가 많았고, 그때 병목은 대부분 코드가 아니라 에셋 — 레이어 수와 이펙트 — 이었습니다. 에셋을 가볍게 만드는 방법은 최적화 글에 따로 정리해 뒀습니다. 그리고 판정은 반드시 실제 저가형 기기에서 하세요. 에뮬레이터는 호스트 GPU를 쓰기 때문에 결과가 낙관적으로 나옵니다.