React Native에서의 Lottie란? (JSON 형태의 애니메이션)

eeennsu·2026년 8월 19일

React Native

목록 보기
84/92

개요

디자이너가 만든 애니메이션을 앱에 그대로 옮기는 일은 생각보다 까다롭다. 로딩 스피너 하나만 해도 회전 속도, 이징, 페이드 타이밍을 코드로 다시 맞춰야 하고, 결과물은 대체로 원본과 미묘하게 다르다.

Lottie는 이 과정을 없애는 방식으로 접근한다. After Effects에서 만든 애니메이션을 Bodymovin 플러그인으로 JSON으로 내보내면, 네이티브 런타임이 그 JSON을 읽어 벡터로 직접 그린다. 개발자가 다시 만들 필요가 없고, 디자이너가 수정하면 JSON 파일만 교체하면 된다.

React Native에서는 lottie-react-native가 iOS의 lottie-ios와 Android의 lottie-android를 감싸서 제공한다. 현재 최신 버전은 7.x대이며 New Architecture를 지원한다.



Lottie를 선택하는 기준

애니메이션을 넣는 방법은 크게 세 가지다. Reanimated로 직접 구현하거나, GIF나 동영상을 재생하거나, Lottie를 쓰는 것이다.

Reanimated는 사용자 입력에 반응해야 할 때 유리하다. 드래그 위치에 따라 카드가 따라오거나, 스크롤 값에 헤더가 연동되는 동작은 Lottie로 표현하기 어렵다. 반대로 복잡한 일러스트가 여러 레이어로 움직이는 애니메이션을 Reanimated로 만드는 건 비효율적이다.

GIF나 동영상은 래스터 이미지라 고해상도 기기에서 뭉개지고, 배경 투명 처리가 번거롭고, 파일이 무겁다. 같은 애니메이션이 GIF로 2MB일 때 Lottie JSON은 수십 KB에 그치는 경우가 흔하다.

Lottie는 정해진 타임라인을 그대로 재생하는 데 최적화되어 있다. 스플래시, 온보딩 일러스트, 로딩 인디케이터, 결제 성공 화면, 빈 목록 화면처럼 "예쁘게 보여주고 끝나는" 구간이 주 무대다. 여기에 재생 구간을 나눠 상태에 따라 다른 부분을 재생하는 정도의 인터랙션까지는 충분히 소화한다.



설치와 New Architecture

RN CLI 프로젝트 기준으로 설치는 단순하다.

npm install lottie-react-native
cd ios && pod install

New Architecture가 켜져 있어도 별도 설정은 필요 없다. 7.x는 Fabric 컴포넌트로 구현되어 있어 newArchEnabled=true 상태에서 그대로 동작한다. 다만 6.x 이하 버전은 Fabric 지원이 불완전해 iOS에서 첫 렌더가 비는 문제가 보고된 적이 있으므로, New Architecture를 쓴다면 7.x 이상으로 올리는 편이 안전하다.

애니메이션 파일은 src/assets/lottie/ 같은 디렉터리에 두고 require로 참조한다. Metro가 기본적으로 .json을 처리하므로 추가 설정은 없다.



기본 사용법

import LottieView from 'lottie-react-native';

export function LoadingIndicator() {
  return (
    <LottieView
      source={require('@/assets/lottie/loading.json')}
      style={{ width: 120, height: 120 }}
      autoPlay
      loop
    />
  );
}

style에 크기를 반드시 지정해야 한다. LottieView는 기본 크기를 갖지 않아서 크기가 없으면 아무것도 보이지 않는다. Lottie가 안 나온다는 문의의 절반 이상이 여기서 나온다.

resizeModecontain, cover, center를 지원하며 기본값은 contain이다. 디자인 시안의 비율과 컨테이너 비율이 다르면 여백이 생기므로, 배경에 꽉 채워야 하는 경우에만 cover를 쓴다.



재생 제어

autoPlay만으로 부족한 경우 ref로 명령형 제어를 한다.

const lottieRef = useRef<LottieView>(null);

lottieRef.current?.play();        // 처음부터 재생
lottieRef.current?.play(30, 90);  // 30~90 프레임 구간만 재생
lottieRef.current?.pause();
lottieRef.current?.resume();
lottieRef.current?.reset();

구간 재생이 실무에서 가장 쓸모 있다. 하나의 JSON에 여러 상태의 애니메이션을 담아두고 프레임 범위로 나눠 쓰는 방식인데, 파일을 여러 개로 쪼개는 것보다 관리가 편하고 상태 전환도 매끄럽다.

좋아요 버튼이 전형적인 예다.

export function LikeButton({ liked, onToggle }: Props) {
  const lottieRef = useRef<LottieView>(null);

  const handlePress = () => {
    if (!liked) {
      lottieRef.current?.play(0, 60); // 채워지는 구간
    } else {
      lottieRef.current?.reset();
    }
    onToggle();
  };

  return (
    <Pressable onPress={handlePress} hitSlop={8}>
      <LottieView
        ref={lottieRef}
        source={require('@/assets/lottie/heart.json')}
        style={{ width: 40, height: 40 }}
        loop={false}
        progress={liked ? 1 : 0}
      />
    </Pressable>
  );
}

progress를 함께 넘긴 이유는 서버 상태가 늦게 도착하거나 화면을 다시 진입했을 때의 초기 표시 때문이다. 애니메이션 없이 이미 좋아요가 눌린 상태를 그려야 하는 순간이 반드시 생긴다.

재생이 끝나는 시점을 잡아야 한다면 onAnimationFinish를 쓴다.

<LottieView
  source={require('@/assets/lottie/payment-success.json')}
  style={styles.animation}
  autoPlay
  loop={false}
  onAnimationFinish={() => navigation.replace('OrderDetail', { orderId })}
/>

loop가 켜져 있으면 이 콜백은 호출되지 않는다. 그리고 일부 7.x 초기 버전에서 마운트 직후 콜백이 한 번 잘못 발화되는 이슈가 있었으므로, 화면 전환처럼 되돌릴 수 없는 동작을 붙일 때는 실제 기기에서 확인하고 필요하면 플래그로 한 번만 실행되게 막는다.



색상 커스터마이징

다크 모드나 테마 대응 때문에 애니메이션 색상만 바꿔야 할 때가 있다. 매번 디자이너에게 색상별 파일을 요청하는 대신 colorFilters로 특정 레이어의 색을 덮어쓸 수 있다.

<LottieView
  source={require('@/assets/lottie/spinner.json')}
  style={{ width: 48, height: 48 }}
  colorFilters={[{ keypath: 'Shape Layer 1', color: theme.colors.primary }]}
  autoPlay
  loop
/>

keypath는 After Effects의 레이어 이름과 정확히 일치해야 한다. 디자이너에게 레이어 이름을 의미 있게 지어달라고 미리 요청해두면 나중에 훨씬 편하다. 다만 이 기능은 iOS와 Android의 렌더러 구현 차이 때문에 결과가 완전히 같지 않은 경우가 있어, 양쪽 모두에서 확인이 필요하다.



주의사항

After Effects 기능이 전부 지원되지는 않는다. 표현식, 일부 마스크, 블렌드 모드, 특정 이펙트는 무시되거나 다르게 그려진다. 시안이 확정되기 전에 디자이너와 지원 범위를 맞춰두는 게 낫다. 작업이 끝난 뒤 "이 부분이 안 나온다"를 발견하면 재작업 비용이 크다.

JSON 파일 크기를 확인한다. 벡터로만 구성된 애니메이션은 대체로 가볍지만, 비트맵 이미지가 포함되면 base64로 인코딩되어 파일이 수 MB까지 부풀 수 있다. 번들 크기와 파싱 시간에 직접 영향을 주므로, 이미지가 포함된 파일을 받았다면 벡터로 다시 작업할 수 있는지 물어보는 편이 좋다.

리스트 안에서 여러 개를 동시에 재생하지 않는다. FlatList의 각 아이템마다 Lottie가 루프를 돌면 프레임 드랍이 눈에 띄게 발생한다. 화면에 보이는 항목만 재생하도록 viewabilityConfig와 연동하거나, 리스트에서는 정적 이미지를 쓰고 상세 화면에서만 애니메이션을 재생하는 방식을 고려한다.

백그라운드 전환 후 복귀를 확인한다. 앱이 백그라운드로 갔다 돌아오면 애니메이션이 멈춘 채로 남는 경우가 있다. 로딩 오버레이처럼 계속 돌아야 하는 요소라면 AppState를 구독해 다시 resume()을 호출한다.

모션 감소 설정을 존중한다. OS에서 모션 줄이기를 켠 사용자에게는 애니메이션이 불편할 수 있다. AccessibilityInfo.isReduceMotionEnabled()로 확인해 정적인 대체 화면을 보여주는 처리를 넣어두면 좋다.



트러블슈팅

아무것도 보이지 않을 때style의 크기부터 확인한다. 크기가 있는데도 안 보인다면 JSON 파일 자체가 올바른지, 애니메이션의 색상이 배경과 같지는 않은지 본다.

.lottie 파일이 로드되지 않을 때.json으로 바꿔본다. dotLottie 형식은 내부적으로 zip이라 별도 처리가 필요한데, Android의 New Architecture 환경에서 zip을 JSON으로 파싱하려다 실패하는 버그가 보고된 적이 있다. 압축률이 절실하지 않다면 .json이 무난하다.

네이티브 로그를 먼저 본다. JS 콘솔에는 아무 메시지가 없어도 Android의 Logcat이나 Xcode 콘솔에는 파싱 실패 이유가 남는 경우가 많다. 소수점이 들어가면 안 되는 값에 소수가 들어간 것처럼 파일 자체가 규격에 어긋난 상황도 여기서 확인된다.

원격 URL로 불러올 때는 캐싱을 직접 처리해야 한다. source에 URL 문자열을 넘기면 동작하지만 매번 다시 받아오므로, 자주 쓰는 애니메이션은 번들에 포함하고 자주 바뀌는 것만 원격으로 두는 식으로 나눈다.



정리

Lottie는 디자이너의 결과물을 그대로 앱에 옮기기 위한 도구다. 정해진 타임라인을 재생하는 영역에서는 직접 구현보다 압도적으로 효율적이고, 사용자 입력에 실시간으로 반응해야 하는 영역에서는 Reanimated가 여전히 맞다. 이 경계를 구분하는 것이 도입의 출발점이다.

실무에서 자주 마주치는 지점은 세 가지로 압축된다. style에 크기를 지정하는 것, 하나의 파일을 프레임 구간으로 나눠 상태별로 재생하는 것, 그리고 파일 크기와 동시 재생 개수를 성능 관점에서 관리하는 것이다.

New Architecture 환경이라면 7.x 이상을 쓰고, dotLottie 대신 JSON을 기본으로 두면 대부분의 문제를 미리 피할 수 있다.

profile
이력서 https://resume.eunsu.pro

0개의 댓글