Animated API — 기본 애니메이션 구현
Animated API는 React Native 내장 애니메이션 시스템으로, 60fps 부드러운 애니메이션을 네이티브 드라이버로 처리합니다.
모바일 앱에서 애니메이션은 선택이 아니라 필수입니다. 부드러운 전환과 피드백이 사용자 경험을 크게 좌우합니다.
기본 개념
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
// 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 — 시간 기반
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 — 스프링 물리
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 — 감속
// 초기 속도에서 점점 감속 (관성 스크롤 효과)
Animated.decay(value, {
velocity: 0.5, // 초기 속도
deceleration: 0.997, // 감속률
useNativeDriver: true,
}).start();
transform 애니메이션
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>
);
}
복합 애니메이션
// 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 — 값 매핑
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>
</>
);
}
실전: 버튼 프레스 애니메이션
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 라이브러리를 검토하세요
댓글 로딩 중...