Flutter에서 Lottie 사용하기 — lottie 패키지 실전 가이드
Flutter에서는 순수 Dart로 구현된 lottie 패키지가 표준입니다. 네이티브 브릿지 없이 Flutter 렌더링 파이프라인 위에서 직접 그리므로 플랫폼 간 렌더링이 동일하다는 것이 큰 장점입니다.
저는 본업에서는 네이티브 iOS 앱을 만들지만, 사이드 프로젝트를 Flutter로 진행하면서 이 패키지를 제품 수준까지 써 봤습니다. lottie-ios처럼 네이티브 구현체를 감싼 방식이 아니다 보니 문제가 터지는 지점도 네이티브와는 조금 다릅니다. 기본 사용법과 함께, 그때 직접 겪은 에러와 해결 과정을 뒤쪽에 정리했습니다.
설치
flutter pub add lottie
기본 사용
import 'package:lottie/lottie.dart';
// 에셋에서
Lottie.asset('assets/loading.json', width: 120, repeat: true)
// 네트워크에서
Lottie.network('https://example.com/anim.json')
// .lottie(dotLottie)도 지원
Lottie.asset('assets/bundle.lottie', decoder: customDecoder)
pubspec.yaml에 에셋 등록을 잊지 마세요.
flutter:
assets:
- assets/loading.json
AnimationController로 제어
class _MyState extends State<MyWidget> with TickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(vsync: this);
@override
Widget build(BuildContext context) {
return Lottie.asset(
'assets/success.json',
controller: _controller,
onLoaded: (composition) {
_controller
..duration = composition.duration
..forward(); // 1회 재생
},
);
}
}
컨트롤러를 쥐고 있으면 구간 재생(_controller.animateTo(0.5)), 역재생, 스크롤 연동이 모두 자연스럽게 됩니다.
런타임 색상 변경 — ValueDelegate
Lottie.asset(
'assets/spinner.json',
delegates: LottieDelegates(
values: [
ValueDelegate.color(
const ['**', 'primary_fill', '**'],
value: Theme.of(context).colorScheme.primary,
),
],
),
)
keypath에 들어갈 레이어 이름은 에셋을 Lottie Inspector에서 열어 확인하거나, 디자이너와 네이밍을 사전에 약속하세요.
성능 팁
- frameRate 옵션 — Lottie 위젯은 기본적으로 화면 주사율에 맞춰 매 프레임 리페인트를 시도합니다. 원본이 24fps로 만들어진 파일을 60Hz, 심하면 120Hz ProMotion 화면에서 그대로 돌리면 같은 그림을 두세 번씩 다시 그리는 셈이 됩니다.
FrameRate.composition으로 원본 프레임레이트를 따르게 하면 이 낭비가 사라집니다. 육안으로는 차이가 거의 없는데 CPU 점유만 내려가서, 저는 반복 재생하는 스피너에는 기본으로 걸어 둡니다. - 큰 JSON 파싱 — JSON 디코딩은 기본적으로 메인 아이솔레이트에서 일어나므로, 수백 KB짜리 파일은 첫 로드 시점에 UI 스레드를 잡고 잰크(jank)를 만듭니다.
compute()로 파싱을 분리하거나, 근본적으로는 에셋 용량 자체를 줄이는 것이 낫습니다. 아래 에러 사례에서 실제 겪은 과정을 적어 두었습니다. - renderCache — 벡터 애니메이션은 매 프레임 패스를 다시 그리는 구조인데,
renderCache: RenderCache.raster를 켜면 한 번 그린 프레임을 래스터 이미지로 캐시해 재사용합니다. 반복 재생하는 작은 스피너처럼 프레임 수가 유한한 루프에 특히 효과적입니다. 대신 캐시가 메모리를 먹으므로 풀스크린급 애니메이션에는 켜기 전에 메모리 사용량을 확인하세요. - Impeller — iOS의 Impeller 렌더러에서는 일부 블렌드 모드가 Skia 시절과 다르게 합성될 수 있습니다. 겪은 내용이 있어 아래에 별도 섹션으로 정리했습니다.
자주 겪는 에러와 해결
Unable to load asset
코드는 분명히 맞는데 화면에 Unable to load asset: assets/loading.json 빨간 에러가 뜨는 경우입니다. 제가 겪은 원인은 매번 셋 중 하나였습니다. 첫째, pubspec.yaml에 에셋을 등록하지 않았거나 경로에 오타가 있는 경우. 둘째, 등록은 했는데 hot reload만 하고 앱을 재시작하지 않은 경우 — 에셋 번들은 hot reload로 갱신되지 않으므로 flutter run을 다시 해야 합니다. 셋째가 가장 찾기 어려운데, 파일명 대소문자입니다. macOS 기본 파일시스템은 대소문자를 구분하지 않아서 Loading.json을 loading.json으로 불러도 로컬에서는 멀쩡히 돌아가다가, CI나 실기기 빌드에서만 깨집니다. 저도 이걸로 한참 헤맨 뒤로는 에셋 파일명을 전부 소문자 스네이크 케이스로 통일했습니다.
pubspec 들여쓰기 실수
YAML은 들여쓰기가 곧 문법입니다. assets:가 flutter: 아래 두 칸 들여쓰기로 들어가야 하는데, 실수로 최상위 레벨에 두면 에러 없이 조용히 무시됩니다. 빌드는 성공하고 에셋만 없는 상태가 되니, 위의 Unable to load asset 에러를 다 확인하고도 원인을 못 찾다가 결국 pubspec을 눈으로 들여다보고서야 발견했습니다. 애니메이션 파일이 여러 개라면 - assets/lottie/처럼 폴더 단위로 등록하는 편이 안전한데, 이때 경로 끝의 슬래시를 빼먹으면 역시 동작하지 않습니다.
큰 JSON 첫 로드 잰크
온보딩 화면에 넣은 애니메이션이 첫 진입 순간 화면을 잠깐 멈추게 한 적이 있습니다. DevTools 타임라인을 열어 보니 JSON 파싱이 UI 스레드를 점유하는 구간이 그대로 보였습니다. 급한 대로는 화면 전환 전에 AssetLottie('...').load()로 미리 로드해 캐시에 올려 두는 방식으로 넘겼고, 근본적으로는 디자이너와 함께 에셋을 다시 내보내 용량을 줄여 해결했습니다. 파일이 수백 KB를 넘는다면 코드보다 에셋을 먼저 의심하는 편이 빠릅니다. 줄이는 방법은 파일 용량 줄이는 7가지 방법에 정리해 두었습니다.
Impeller에서 확인할 것
Flutter가 iOS 기본 렌더러를 Skia에서 Impeller로 바꾼 뒤로는, 같은 Lottie 파일이라도 렌더러에 따라 결과가 달라질 수 있다는 점을 전제로 두는 편이 안전합니다. lottie 패키지는 캔버스 API 위에 직접 그리는데, 그 캔버스의 구현이 통째로 바뀌었기 때문입니다. 특히 멀티플라이·스크린 같은 블렌드 모드와 마스크가 겹친 파일에서 색이 다르게 합성되거나 특정 레이어가 이상하게 보이는 이슈가 보고된 적이 있습니다. 저는 정확한 색이 중요한 화면을 주로 만들다 보니, 애니메이션 색이 미묘하게 달라지는 것을 그냥 넘기기 어려워 확인 절차를 정해 두었습니다.
- 먼저 웹 뷰어에서 기준 렌더링을 확인해 둡니다. 문제가 생겼을 때 "원래 어떻게 보여야 하는가"를 비교할 기준이 됩니다.
- 블렌드 모드나 마스크가 들어간 파일은 시뮬레이터가 아니라 실기기에서 확인합니다. 시뮬레이터와 실기기의 GPU 경로가 달라서 시뮬레이터에서는 재현되지 않는 경우가 있었습니다.
- 렌더링이 다르게 보이면 Info.plist에
FLTEnableImpeller를false로 두고 다시 빌드해 비교합니다. Skia에서는 정상이라면 Impeller 쪽 차이라는 확진이 됩니다. - Impeller 문제로 확인됐다면, 제 경험상 Flutter 버전업을 기다리는 것보다 After Effects에서 블렌드 모드를 빼고 일반 알파 합성으로 다시 내보내는 쪽이 훨씬 빨랐습니다.