iOS에서 Lottie 사용하기 — SwiftUI 시대의 lottie-ios 가이드
iOS 앱에서 Lottie를 도입하는 방법을 SwiftUI 중심으로 정리합니다. lottie-ios 4.x는 SwiftUI 전용 LottieView를 제공해서 UIKit 래핑 없이 바로 쓸 수 있습니다.
설치 — Swift Package Manager
Xcode에서 File → Add Package Dependencies를 열고 아래 주소를 추가합니다.
https://github.com/airbnb/lottie-ios
Package.swift를 쓰는 모듈 구조라면:
dependencies: [
.package(url: "https://github.com/airbnb/lottie-ios", from: "4.5.0")
]
lottie-spm(XCFramework 배포) 패키지를 쓰면 빌드 시간도 줄일 수 있습니다.SwiftUI에서 재생하기
import Lottie
import SwiftUI
struct OnboardingView: View {
var body: some View {
LottieView(animation: .named("onboarding_hero"))
.playing(loopMode: .loop)
.frame(width: 240, height: 240)
}
}
재생 제어가 필요하면 playbackMode를 상태로 관리합니다.
struct SuccessView: View {
@State private var mode: LottiePlaybackMode = .paused
var body: some View {
LottieView(animation: .named("success_check"))
.playbackMode(mode)
.animationDidFinish { _ in /* 완료 처리 */ }
.onAppear {
mode = .playing(.fromProgress(0, toProgress: 1, loopMode: .playOnce))
}
}
}
dotLottie(.lottie) 로드
LottieView {
try await DotLottieFile.named("bundle_animations")
}
.playing(loopMode: .autoReverse)
비동기 로더 클로저를 받으므로 네트워크에서 받아오는 패턴도 자연스럽게 처리됩니다.
다크모드 색상 대응 — ValueProvider
Lottie 색을 코드로 바꾸는 정석은 ValueProvider입니다. 디자이너에게 레이어 이름(keypath)을 약속받는 것이 핵심입니다.
LottieView(animation: .named("spinner"))
.playing(loopMode: .loop)
.valueProvider(
ColorValueProvider(UIColor.label.lottieColorValue),
for: AnimationKeypath(keypath: "**.Fill 1.Color")
)
keypath의 **는 와일드카드입니다. 어떤 레이어 이름이 들어 있는지 모르겠다면 에셋을 Lottie Inspector에 넣어 레이어 구성을 먼저 확인하세요.
실무 주의점
- 렌더링 엔진 — 4.x의 기본은 Core Animation 엔진입니다. 대부분 더 빠르지만, 일부 표현식 기반 파일은 메인 스레드 엔진으로 폴백됩니다. 콘솔 경고를 확인하세요.
- 목록 셀에서의 재사용 — 셀마다 애니메이션 인스턴스를 새로 만들면 스크롤이 버벅입니다. 캐시(
DefaultAnimationCache)를 활용하세요. - 앱 용량 — 에셋이 늘수록 IPA가 커집니다. 수백 KB짜리 JSON이 섞여 있지 않은지 주기적으로 검수하는 습관이 필요합니다.
- 런타임 차이 — 웹 미리보기에서 멀쩡하던 파일이 iOS에서 다르게 나올 수 있습니다. 지원 기능 경계가 다르기 때문입니다. 최종 확인은 반드시 실기기에서.
목록 셀 캐싱, 실제로 적용해 보면
셀 캐싱은 말로 들으면 당연한 이야기인데, 직접 겪기 전에는 체감이 잘 안 됩니다. 제가 만드는 앱의 기록 목록에는 동기화 상태를 보여주는 작은 애니메이션이 셀마다 들어가는데, 처음 구현에서는 셀 구성 시점마다 LottieAnimation.named()를 호출하는 구조였습니다. 구형 기기에서 스크롤이 미묘하게 걸리는 느낌이 들어 Instruments로 찍어 보니 JSON 파싱과 뷰 생성 비용이 스크롤 중에 반복되고 있었습니다.
다행히 .named()는 기본적으로 DefaultAnimationCache.sharedCache를 사용하므로 같은 이름의 에셋이라면 두 번째 호출부터는 파싱을 건너뜁니다. 문제는 URL에서 받아오거나 커스텀 로더를 쓰는 경로인데, 이때는 캐시가 자동으로 적용되지 않으니 animationCache 파라미터를 직접 지정해야 합니다. 제 경우에는 화면 진입 시점에 애니메이션 객체를 한 번만 만들어 뷰모델이 들고 있게 하고, 셀에는 참조만 내려주는 구조로 바꾸면서 재사용 시점의 비용을 없앴습니다. 셀 개수가 많다면 애니메이션 자체를 꼭 셀마다 재생해야 하는지도 한번 의심해 볼 만합니다. 화면에 보이는 셀만 재생하고 나머지는 정지 프레임으로 두는 것만으로도 부담이 크게 줄었습니다.
3.x에서 4.x로 올릴 때 확인할 것
제가 유지보수하던 프로젝트는 꽤 오래 3.x에 머물러 있었는데, 4.x로 올리면서 가장 크게 체감한 변화는 렌더링 엔진이었습니다. 4.x부터는 Core Animation 엔진이 기본이 되면서 프레임 갱신이 메인 스레드를 거의 쓰지 않게 됩니다. 스크롤 중에도 애니메이션이 끊기지 않는 건 이 덕분인데, 반대로 모든 파일이 이 엔진에서 도는 것은 아니라는 점이 함정입니다.
After Effects 표현식이나 일부 매트 기능이 들어간 파일은 Core Animation 엔진이 지원하지 못해서 런타임에 메인 스레드 엔진으로 자동 폴백됩니다. 이때 콘솔에 경고 로그가 출력되는데, 저는 처음에 이 경고를 무심코 넘겼다가 저사양 기기에서만 프레임이 떨어진다는 제보를 받고 나서야 원인을 찾았습니다. 시뮬레이터나 최신 기기에서는 폴백돼도 티가 안 나기 때문에 놓치기 쉽습니다. 대처는 두 갈래였습니다. 디자이너에게 표현식을 키프레임으로 구워 다시 내보내 달라고 요청해 폴백 자체를 없애거나, 폴백이 의도된 파일이라면 해당 뷰에만 엔진을 명시해서 경고를 정리하는 것입니다.
// 전역 기본값 (기본은 .automatic — 가능하면 Core Animation)
LottieConfiguration.shared = LottieConfiguration(renderingEngine: .automatic)
// 특정 뷰만 메인 스레드 엔진으로 고정
LottieView(animation: .named("legacy_chart"))
.configuration(LottieConfiguration(renderingEngine: .mainThread))
API 이름 변경도 있습니다. 3.x의 Animation과 AnimationView는 SwiftUI 타입과의 충돌을 피하려고 LottieAnimation, LottieAnimationView로 바뀌었습니다. 컴파일 에러가 한꺼번에 쏟아져서 놀랄 수 있는데 대부분 기계적인 치환으로 끝납니다. 제 기준으로 마이그레이션 코드 수정 자체는 반나절이면 충분했고, 오히려 폴백 경고가 나오는 에셋을 골라내는 데 시간이 더 걸렸습니다. 올리기 전에 프로젝트의 모든 Lottie 파일을 한 번씩 재생하면서 콘솔을 지켜보는 과정을 마이그레이션 체크리스트에 넣어 두길 권합니다.
자주 겪는 에러와 해결
.named()가 nil을 반환할 때
코드는 멀쩡한데 화면에 아무것도 안 나오는 경우의 대부분이 이것이었습니다. JSON 파일을 프로젝트에 드래그해 넣을 때 Target Membership 체크를 놓친 경우입니다. 특히 위젯이나 앱 익스텐션처럼 타겟이 여러 개인 프로젝트에서 본체 앱에만 추가해 두고 익스텐션에서 로드하려다 빈 화면을 보는 일이 잦습니다. 파일 인스펙터에서 타겟 체크박스를 먼저 확인하세요. 로컬 SPM 모듈로 분리된 구조라면 리소스가 별도 번들로 들어가므로 .named("name", bundle: .module)처럼 번들을 명시해야 합니다. 기본값이 .main이라서 모듈 안에서는 조용히 nil이 됩니다.
SPM 캐시가 꼬였을 때
버전을 올렸는데 분명히 존재하는 심볼을 못 찾는다고 하거나, 패키지 해석이 예전 버전에 붙잡혀 있는 경우가 있습니다. 저는 File → Packages → Reset Package Caches를 먼저 시도하고, 그래도 안 되면 DerivedData를 지우고 다시 해석합니다. CI에서만 재현되는 케이스라면 러너에 남은 캐시가 원인일 때가 많아서, 저희 빌드 스크립트에는 캐시 정리 단계를 옵션으로 넣어 두었습니다.
프리뷰에서만 안 보일 때
실기기와 시뮬레이터에서는 잘 나오는데 SwiftUI 프리뷰에서만 빈 화면인 경우도 겪었습니다. 프리뷰는 별도 프로세스에서 돌기 때문에 번들 경로가 앱 실행 때와 다르게 잡힐 수 있고, 로컬 패키지의 리소스 처리도 미묘하게 다릅니다. 저는 프리뷰 전용으로 원인을 오래 파기보다는, 프리뷰가 안 보이면 시뮬레이터로 확인하는 쪽으로 기대치를 낮췄습니다. 다만 에셋 자체가 의심될 때는 파일을 뷰어에 넣어 재생 여부를 먼저 확인하면 번들 문제인지 파일 문제인지 빠르게 갈라낼 수 있습니다. 그래도 안 움직이는 파일이라면 단계별 디버깅 체크리스트를 참고하세요.