본문 바로가기

React

React에서 Lottie 애니메이션 통합하기

Lottie는 After Effects로 만든 애니메이션을 JSON으로 내보내 웹에서 SVG/Canvas로 부드럽게 재생하는 기술입니다. React에서는 가볍고 선명한 인터랙션을 구현할 때 매우 유용합니다. 이 글은 실무에서 바로 쓰는 통합 방법과 성능/SSR/접근성까지 핵심만 정리합니다.

1. 어떤 라이브러리를 쓸까?

가장 쉬운 선택은 lottie-react입니다. 내부적으로 lottie-web을 감싸 React 친화적 API와 ref 제어, 콜백을 제공합니다. 세밀한 초기화 옵션이 필요하면 lottie-web을 직접 사용할 수 있습니다.

권장: 빠른 개발은 lottie-react, 커스텀 초기화(assetsPath 등)나 독자 렌더러 튜닝이 필요하면 lottie-web 직접 사용을 고려합니다.

2. 설치

// 옵션 1: lottie-react (권장)
npm i lottie-react

// 옵션 2: lottie-web 직접 사용
npm i lottie-web

3. lottie-react로 기본 통합

가장 단순한 예제입니다. JSON은 정적 import 또는 public 경로를 사용할 수 있습니다.

import React from 'react'
import Lottie from 'lottie-react'
import rocket from './assets/rocket.json'

export default function Hero() {
  return (
    <div style={{ width: 240 }}>
      <Lottie animationData={rocket} loop autoplay />
    </div>
  )
}

원격 JSON을 fetch해서 쓰고 싶다면 상태로 로드한 뒤 animationData에 전달합니다.

import React, { useEffect, useState } from 'react'
import Lottie from 'lottie-react'

export default function RemoteLottie() {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetch('https://assets10.lottiefiles.com/packages/lf20_rocket.json')
      .then(r => r.json())
      .then(setData)
  }, [])
  if (!data) return null
  return <Lottie animationData={data} loop autoplay />
}

4. 재생 제어(Play/Pause/Speed/Segments)

ref로 미세 제어가 가능합니다. 호버 재생, 구간 재생 등 UX를 쉽게 만들 수 있습니다.

import React, { useRef } from 'react'
import Lottie from 'lottie-react'
import check from './check.json'

export default function ControlDemo() {
  const lottieRef = useRef(null)
  return (
    <div>
      <Lottie
        lottieRef={lottieRef}
        animationData={check}
        loop={false}
        autoplay={false}
        onComplete={() => console.log('완료')}
        style={{ width: 160 }}
      />
      <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
        <button onClick={() => lottieRef.current?.play()}>Play</button>
        <button onClick={() => lottieRef.current?.pause()}>Pause</button>
        <button onClick={() => lottieRef.current?.stop()}>Stop</button>
        <button onClick={() => lottieRef.current?.setSpeed(2)}>2x</button>
        <button onClick={() => lottieRef.current?.playSegments([0, 45], true)}>0~45</button>
      </div>
    </div>
  )
}

5. lottie-web 직접 사용

초기화 옵션을 세밀하게 제어하거나 assetsPath가 필요한 이미지 기반 Lottie에 유용합니다.

import React, { useEffect, useRef } from 'react'
import lottie from 'lottie-web'

export default function BareLottie() {
  const boxRef = useRef(null)
  useEffect(() => {
    if (!boxRef.current) return
    const anim = lottie.loadAnimation({
      container: boxRef.current,
      renderer: 'svg', // 'canvas' 또는 'html' 가능
      loop: false,
      autoplay: false,
      path: '/animations/like.json',
      // animationData: json 객체로도 가능
      assetsPath: '/animations/images/',
      rendererSettings: { preserveAspectRatio: 'xMidYMid meet' }
    })
    return () => anim.destroy()
  }, [])
  return <div ref={boxRef} style={{ width: 200, height: 200 }} />
}

6. 지연 로딩과 번들 최적화

애니메이션은 무겁습니다. 화면에 보일 때만 로드하고, JSON은 분리하세요.

import React, { useEffect, useRef, useState, Suspense } from 'react'
const Lottie = React.lazy(() => import('lottie-react'))

export default function LazyInView() {
  const hostRef = useRef(null)
  const [inView, setInView] = useState(false)
  const [data, setData] = useState(null)

  useEffect(() => {
    const obs = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { threshold: 0.2 })
    if (hostRef.current) obs.observe(hostRef.current)
    return () => obs.disconnect()
  }, [])

  useEffect(() => {
    if (!inView || data) return
    import('./heavy.json').then(m => setData(m.default)) // JSON 분리 로딩
  }, [inView, data])

  return (
    <div ref={hostRef} style={{ minHeight: 220 }}>
      {inView && data ? (
        <Suspense fallback={null}>
          <Lottie animationData={data} loop autoplay />
        </Suspense>
      ) : null}
    </div>
  )
}

팁: 여러 페이지에서 공용으로 쓰는 JSON은 CDN 또는 public 폴더로 옮기고 path로 스트리밍하면 초기 번들을 가볍게 유지할 수 있습니다.

7. Next.js/SSR 대응

서버에서 window가 없어 오류가 날 수 있습니다. 클라이언트 전용으로 로드하세요.

// pages/ 또는 app/ 어디서나
import dynamic from 'next/dynamic'
const Lottie = dynamic(() => import('lottie-react'), { ssr: false })

export default function Hero() {
  return <Lottie animationData={require('../public/rocket.json')} loop autoplay />
}

lottie-web을 직접 쓸 때는 useEffect 안에서만 loadAnimation을 호출하고, 컴포넌트 언마운트 시 destroy를 호출합니다.

8. 접근성과 사용자 설정(감소된 동작)

정보 전달 목적이라면 대체 텍스트를 제공하고, 장식용이면 스크린리더에 숨깁니다. 또한 사용자 설정(prefers-reduced-motion)을 존중하세요.

import React, { useEffect, useRef, useState } from 'react'
import Lottie from 'lottie-react'
import wave from './wave.json'

export default function A11yLottie() {
  const [reduced, setReduced] = useState(false)
  const ref = useRef(null)
  useEffect(() => {
    const m = window.matchMedia('(prefers-reduced-motion: reduce)')
    const onChange = () => setReduced(m.matches)
    setReduced(m.matches)
    m.addEventListener('change', onChange)
    return () => m.removeEventListener('change', onChange)
  }, [])
  return (
    <Lottie
      lottieRef={ref}
      animationData={wave}
      autoplay={!reduced}
      loop={!reduced}
      aria-label='파도 애니메이션'
      role='img'
    />
  )
}

장식용이면 aria-hidden='true'를 부여합니다.

9. 성능 최적화 체크리스트

- JSON 용량을 줄이세요: 프레임레이트 낮추기, 불필요한 레이어 제거, 텍스트를 셰이프로 변환, LottieFiles Optimizer 활용을 권장합니다.

- 렌더러 선택: 대부분 svg가 선명하고 가벼우나, 수천 개의 벡터가 움직일 때는 canvas가 더 빠를 수 있습니다.

- 재생 범위 제한: 최초에는 정지(goToAndStop) 상태로 썸네일만 보여주고, 사용자 상호작용 시 playSegments로 필요한 구간만 재생합니다.

- 메모이제이션: animationData 객체를 매 렌더마다 새로 만들지 말고 import 또는 useMemo로 고정합니다.

- 오프스크린 중지: 탭 비활성, 뷰포트 밖에서는 pause해 CPU를 아낍니다(IntersectionObserver 연계).

10. 자주 만나는 이슈와 해결

- 크기 깨짐: 부모 컨테이너에 명확한 width/height를 주고 preserveAspectRatio를 설정합니다.

- 이미지 에셋 경로: AE에서 이미지 기반 Lottie는 assets 폴더를 사용합니다. lottie-web은 assetsPath 옵션으로 절대 경로를 지정하세요.

- 무한 루프 종료 감지: onLoopComplete 콜백에서 loop를 false로 바꾸거나 ref.stop()을 호출합니다.

- 큰 JSON의 초기 깜빡임: lazy 로딩 + skeleton을 쓰고 autoplay를 지연합니다.

11. 마무리

lottie-react로 시작해 빠르게 결과를 내고, 필요 시 lottie-web로 세밀 제어를 더하는 전략이 실무에 적합합니다. 지연 로딩, 접근성, SSR까지 고려하면 가볍고 매끄러운 인터랙션을 안정적으로 제공할 수 있습니다.