Animated API는 React Native 내장 애니메이션 시스템으로, 60fps 부드러운 애니메이션을 네이티브 드라이버로 처리합니다.

모바일 앱에서 애니메이션은 선택이 아니라 필수입니다. 부드러운 전환과 피드백이 사용자 경험을 크게 좌우합니다.


기본 개념

TSX
import { useRef, useEffect } from 'react';
import { Animated, View, StyleSheet } from 'react-native';

function FadeIn() {
  // 1. Animated.Value 생성
  const opacity = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    // 2. 애니메이션 실행
    Animated.timing(opacity, {
      toValue: 1,           // 목표 값
      duration: 500,        // 지속 시간 (ms)
      useNativeDriver: true, // 네이티브 드라이버 사용 (성능 향상)
    }).start();             // 시작
  }, []);

  return (
    // 3. Animated.View에 스타일 적용
    <Animated.View style={[styles.box, { opacity }]}>
      <Text>페이드인 애니메이션</Text>
    </Animated.View>
  );
}

useNativeDriver

TSX
// useNativeDriver: true → 네이티브 스레드에서 애니메이션 처리 (60fps 보장)
// 지원: opacity, transform (translateX, translateY, scale, rotate 등)
// 미지원: width, height, backgroundColor, margin, padding 등 레이아웃 속성

// 레이아웃 애니메이션은 useNativeDriver: false 사용
Animated.timing(width, {
  toValue: 200,
  duration: 300,
  useNativeDriver: false, // 레이아웃 속성은 false
}).start();

애니메이션 종류

timing — 시간 기반

TSX
Animated.timing(value, {
  toValue: 1,
  duration: 300,
  easing: Easing.ease, // 가속도 곡선
  useNativeDriver: true,
}).start();

// Easing 옵션
import { Easing } from 'react-native';
// Easing.linear   — 일정 속도
// Easing.ease     — 기본 가감속
// Easing.bounce   — 바운스 효과
// Easing.elastic(1) — 탄성 효과
// Easing.bezier(0.25, 0.1, 0.25, 1) — 커스텀 곡선

spring — 스프링 물리

TSX
Animated.spring(value, {
  toValue: 1,
  friction: 5,      // 마찰 (높을수록 빨리 멈춤)
  tension: 40,      // 장력 (높을수록 빠르게 이동)
  useNativeDriver: true,
}).start();

// 또는 bounciness/speed 방식
Animated.spring(value, {
  toValue: 1,
  bounciness: 8,    // 탄성 정도
  speed: 12,        // 속도
  useNativeDriver: true,
}).start();

decay — 감속

TSX
// 초기 속도에서 점점 감속 (관성 스크롤 효과)
Animated.decay(value, {
  velocity: 0.5,    // 초기 속도
  deceleration: 0.997, // 감속률
  useNativeDriver: true,
}).start();

transform 애니메이션

TSX
function SlideAndScale() {
  const translateY = useRef(new Animated.Value(-100)).current;
  const scale = useRef(new Animated.Value(0.5)).current;

  useEffect(() => {
    Animated.parallel([
      Animated.spring(translateY, {
        toValue: 0,
        useNativeDriver: true,
      }),
      Animated.spring(scale, {
        toValue: 1,
        useNativeDriver: true,
      }),
    ]).start();
  }, []);

  return (
    <Animated.View style={{
      transform: [
        { translateY },
        { scale },
      ],
    }}>
      <Text>슬라이드 + 스케일</Text>
    </Animated.View>
  );
}

복합 애니메이션

TSX
// parallel — 동시 실행
Animated.parallel([
  Animated.timing(opacity, { toValue: 1, duration: 300, useNativeDriver: true }),
  Animated.timing(translateY, { toValue: 0, duration: 300, useNativeDriver: true }),
]).start();

// sequence — 순차 실행
Animated.sequence([
  Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }),
  Animated.timing(scale, { toValue: 1.2, duration: 150, useNativeDriver: true }),
  Animated.timing(scale, { toValue: 1, duration: 150, useNativeDriver: true }),
]).start();

// stagger — 시차를 두고 순차 실행
Animated.stagger(100, [
  Animated.timing(item1, { toValue: 1, duration: 300, useNativeDriver: true }),
  Animated.timing(item2, { toValue: 1, duration: 300, useNativeDriver: true }),
  Animated.timing(item3, { toValue: 1, duration: 300, useNativeDriver: true }),
]).start();

// loop — 반복
Animated.loop(
  Animated.sequence([
    Animated.timing(value, { toValue: 1, duration: 500, useNativeDriver: true }),
    Animated.timing(value, { toValue: 0, duration: 500, useNativeDriver: true }),
  ])
).start();

interpolate — 값 매핑

TSX
function ScrollHeader() {
  const scrollY = useRef(new Animated.Value(0)).current;

  // 스크롤에 따라 헤더 투명도와 크기 변화
  const headerOpacity = scrollY.interpolate({
    inputRange: [0, 100],
    outputRange: [1, 0],
    extrapolate: 'clamp', // 범위 밖 값 고정
  });

  const headerHeight = scrollY.interpolate({
    inputRange: [0, 100],
    outputRange: [200, 60],
    extrapolate: 'clamp',
  });

  return (
    <>
      <Animated.View style={{ height: headerHeight, opacity: headerOpacity }}>
        <Text>축소되는 헤더</Text>
      </Animated.View>
      <Animated.ScrollView
        onScroll={Animated.event(
          [{ nativeEvent: { contentOffset: { y: scrollY } } }],
          { useNativeDriver: false } // height는 네이티브 드라이버 불가
        )}
        scrollEventThrottle={16}
      >
        {/* 콘텐츠 */}
      </Animated.ScrollView>
    </>
  );
}

실전: 버튼 프레스 애니메이션

TSX
function AnimatedButton({ title, onPress }: { title: string; onPress: () => void }) {
  const scale = useRef(new Animated.Value(1)).current;

  const handlePressIn = () => {
    Animated.spring(scale, {
      toValue: 0.95,
      useNativeDriver: true,
    }).start();
  };

  const handlePressOut = () => {
    Animated.spring(scale, {
      toValue: 1,
      friction: 3,
      tension: 40,
      useNativeDriver: true,
    }).start();
  };

  return (
    <Pressable
      onPress={onPress}
      onPressIn={handlePressIn}
      onPressOut={handlePressOut}
    >
      <Animated.View style={[styles.button, { transform: [{ scale }] }]}>
        <Text style={styles.buttonText}>{title}</Text>
      </Animated.View>
    </Pressable>
  );
}

정리

  • useNativeDriver: true 를 가능한 항상 사용하세요 — 60fps 성능의 핵심입니다
  • timing은 시간 기반, spring은 물리 기반 — 자연스러운 느낌에는 spring이 적합합니다
  • parallel, sequence, stagger로 복합 애니메이션을 조합할 수 있습니다
  • interpolate로 하나의 값에서 여러 스타일 변화를 파생할 수 있습니다
  • 더 복잡한 애니메이션이 필요하면 Reanimated 라이브러리를 검토하세요
댓글 로딩 중...