Jetpack Compose에서 Lottie 재생 제어하기

animateLottieCompositionAsState vs LottieAnimatable 차이와 선택 기준

Jetpack Compose에서 Lottie 애니메이션을 붙일 때 자주 쓰는 방식은 두 가지다.

  • animateLottieCompositionAsState: 선언형(상태 기반)으로 자동 재생
  • LottieAnimatable: 명령형(명령 호출)으로 수동 제어

둘 다 같은 라이브러리(Lottie Compose) 안에 있지만, 재생 제어 모델이 완전히 다르다.
특히 “토글로 잠깐 보여주는 애니메이션”이나 “항상 0프레임부터 다시 재생” 같은 요구가 있으면 선택이 갈린다.


1) animateLottieCompositionAsState: 선언형, 자동 재생

핵심 개념

animateLottieCompositionAsStateCompose의 상태 변화에 맞춰 진행(progress)을 계산해 주는 형태다.
보통 isPlaying, iterations 같은 파라미터를 주고, 그 결과로 나온 progressLottieAnimation에 전달한다.

  • 장점: 코드가 짧고 선언형 흐름에 잘 맞는다
  • 단점: 재생을 “명령”으로 다시 시작시키기 어렵다
    동일 composition을 재사용하면서 show만 토글하면, 이전 진행 상태가 남아서 “끝난 상태”에 머무르는 상황이 생길 수 있다

예시 코드: 토글에 따라 자동 재생

@Composable
fun ExampleAutoPlayLottie(
    show: Boolean,
    modifier: Modifier = Modifier
) {
    if (!show) return

    val composition by rememberLottieComposition(
        LottieCompositionSpec.RawRes(R.raw.example)
    )

    val progress by animateLottieCompositionAsState(
        composition = composition,
        isPlaying = true,
        iterations = 1
    )

    LottieAnimation(
        composition = composition,
        progress = { progress },
        modifier = modifier
    )
}

`

이 방식에서 자주 생기는 상황

showtrue -> false -> true로 빠르게 토글할 때 사용자는 “다시 처음부터 재생”을 기대한다.
하지만 실제로는 다음과 같은 문제가 생길 수 있다.

  • composition이 remember로 유지되어 동일 인스턴스를 재사용
  • progress 상태도 기대와 다르게 이어지는 경우가 있음
  • 결과적으로 “이미 끝난 progress(1.0) 상태”에서 멈춰 보여서 애니메이션이 안 보이는 것처럼 느껴질 수 있음

이 문제는 “항상 0부터 재생” 같은 요구가 있을 때 특히 치명적이다.

개선 시도: key로 강제 재시작

상태 기반 방식에서도 재시작을 유도할 수는 있다. 대표적으로 key(show) 또는 별도 restartKey를 둔다.

@Composable
fun ExampleAutoPlayWithRestartKey(
    show: Boolean,
    restartKey: Int,
    modifier: Modifier = Modifier
) {
    if (!show) return

    key(restartKey) {
        val composition by rememberLottieComposition(
            LottieCompositionSpec.RawRes(R.raw.example)
        )

        val progress by animateLottieCompositionAsState(
            composition = composition,
            isPlaying = true,
            iterations = 1
        )

        LottieAnimation(
            composition = composition,
            progress = { progress },
            modifier = modifier
        )
    }
}

다만 이 방식은 “재시작 트리거 키를 별도로 관리”해야 하고, 재시작 타이밍이 복잡해질수록 설계가 흔들릴 수 있다.


2) LottieAnimatable: 명령형, 수동 제어

핵심 개념

LottieAnimatable은 “상태가 바뀌면 알아서”가 아니라, 내가 원할 때 animate를 호출하는 방식이다.
즉, 애니메이션 재생이 명령형이며, 이 특성 때문에 재생 재시작이 매우 확실하다.

  • 장점: animate()를 호출할 때마다 원하는 시점에 재생을 시작할 수 있고, 보통 0부터 재생을 강제하기 쉬움
  • 단점: 코드는 조금 더 길어지고, LaunchedEffect 등 코루틴 기반 제어가 필요하다

예시 코드: show가 true가 될 때마다 0부터 재생

@Composable
fun ExampleAnimatableLottie(
    show: Boolean,
    modifier: Modifier = Modifier
) {
    if (!show) return

    val composition by rememberLottieComposition(
        LottieCompositionSpec.RawRes(R.raw.example)
    )

    val animatable = rememberLottieAnimatable()

    LaunchedEffect(show, composition) {
        if (composition == null) return@LaunchedEffect

        // 재생을 항상 처음부터 강제하고 싶다면 reset 후 animate
        animatable.snapTo(progress = 0f)

        animatable.animate(
            composition = composition,
            iterations = 1
        )
    }

    LottieAnimation(
        composition = composition,
        progress = { animatable.progress },
        modifier = modifier
    )
}

이 방식은 show가 다시 true가 될 때마다:

  • snapTo(0f)로 0프레임 세팅
  • animate() 호출로 재생 시작

즉, “간헐적으로 안 뜸”처럼 보이는 재시작 이슈에 훨씬 강하다.

빠른 토글 대응: 이전 재생 취소하고 재시작

토글이 매우 빠르면 이전 애니메이션이 실행 중일 수 있다. 이때는 cancel하고 재시작하는 설계가 안정적이다.

@Composable
fun ExampleAnimatableRestartSafe(
    trigger: Int,
    modifier: Modifier = Modifier
) {
    val composition by rememberLottieComposition(
        LottieCompositionSpec.RawRes(R.raw.example)
    )

    val animatable = rememberLottieAnimatable()

LaunchedEffect(trigger, composition) {
    // composition 로딩이 끝나기 전이면 재생을 시도하지 않음
    if (composition == null) return@LaunchedEffect

    /*
     * 재시작이 목적이라면 resetToBeginning() 하나로 충분함.
     *
     * - resetToBeginning():
     *   진행 중인 애니메이션을 중단하고(progress/내부 상태 포함) 시작 지점(0f)으로 초기화한다.
     *   "항상 0부터 다시 재생" 의도를 가장 정확하게 표현하는 선택.
     *
     * - snapTo(0f):
     *   애니메이션을 재생하지 않고 progress 값을 즉시 0f로 "점프"시키는 함수.
     *   특정 프레임에 고정하거나(프리뷰/슬라이더/제스처 매핑) 수동으로 progress를 조절할 때 사용.
     *
     * 따라서 "재시작" 케이스에서는 resetToBeginning()을 쓰고 snapTo(0f)는 제거하는 게 맞다.
     */
    animatable.resetToBeginning()

    // reset된 상태에서 재생을 시작한다 (항상 0부터 시작)
    animatable.animate(
        composition = composition,
        iterations = 1
    )
}

    LottieAnimation(
        composition = composition,
        progress = { animatable.progress },
        modifier = modifier
    )
}

여기서 trigger는 “재생을 다시 시작하고 싶은 이벤트”를 나타내는 정수 카운터 같은 값이다.
예를 들어 버튼 클릭 때마다 trigger++ 해주면 매번 정확히 재생된다.


3) 어떤 상황에서 무엇을 선택할까

animateLottieCompositionAsState가 잘 맞는 경우

  • 화면에 항상 떠 있고, 자연스럽게 계속 재생됨
  • 단순 반복 애니메이션
  • 재시작 타이밍에 대한 요구가 낮음
  • 선언형 코드가 더 중요한 경우

LottieAnimatable이 더 안정적인 경우

  • “보일 때마다 항상 0부터 재생”이 필수
  • 토글/이벤트 기반으로 짧게 재생하는 효과 연출
  • 동일 composition을 재사용하면서 재생 상태가 남아 문제가 되는 경우
  • 간헐적으로 재생이 안 되는 이슈를 근본적으로 줄이고 싶을 때

4) 정리

  • animateLottieCompositionAsState는 상태 변화에 기반한 선언형 자동 재생이 장점이지만,
    재시작 타이밍을 정확히 통제하기 어려울 수 있다.
  • LottieAnimatable은 명령형 제어로 코드가 길어질 수 있지만,
    animate() 호출 단위로 재생을 확실히 시작할 수 있어 재시작 이슈에 강하다.

“정확한 재생 제어가 필요하면 LottieAnimatable이 더 안정적”이라는 결론은,
토글 기반의 짧은 효과 애니메이션이나 반복적으로 다시 시작해야 하는 UI에서 특히 유효하다.

Leave a Comment