React Native에서 다채로운 인터랙션을 구현하게 해주는 react-native-reanimated
는 제가 정말 자주 쓰는 라이브러리입니다. 하지만 최근 프로젝트에서 애니메이션 관련 작업을 하며, 이 라이브러리의 강력함 이면에 숨겨진 몇 가지 고민거리를 마주하게 되었습니다.
바로 코드의 반복성, 복잡성, 그리고 일관성의 문제였습니다.
간단한 효과 하나를 위해 매번 선언해야 하는 여러 상용구 코드는 점차 기술적 부채처럼 느껴졌고, 애니메이션에 익숙하지 않은 동료는 복잡한 코드를 파악하는 데 어려움을 겪기도 했습니다.
이 글에서는 제가 이 문제들을 어떻게 해결했는지, 그 과정에서 탄생한 작은 라이브러리 'Reanimated Composer'의 코드와 함께 개발기를 공유해볼까 합니다.
가장 큰 목표는 '선언적으로' 애니메이션을 작성하여, 개발자가 '어떻게'가 아닌 '무엇을'에만 집중하도록 만드는 것이었습니다.
Before:
// Reanimated에서 흔히 볼 수 있는 패턴입니다.
const opacity = useSharedValue(0);
const translateY = useSharedValue(50);
const animatedStyle = useAnimatedStyle(() => ({...}));
useEffect(() => {
if (isVisible) { /* 애니메이션 실행 로직 */ }
}, [isVisible]);
사실 Reanimated의 useDerivedValue 같은 고급 Hook을 활용하면 useEffect 없이도 비슷한 동작을 구현할 수 있습니다. 하지만 이 글에서는 Reanimated에 아직 익숙하지 않은 분들도 쉽게 공감할 수 있는 패턴을 예시로 사용했습니다.
After:
const { animatedStyle } = useAnimation({
trigger: isVisible,
animations: {
opacity: { to: 1 },
translateY: { to: 0, type: "spring" },
},
});
이 아이디어를 구현하기 위해 useAnimation
이라는 커스텀 훅을 만들었습니다. 그 내부를 간단히 살펴보겠습니다.
먼저, 애니메이션을 적용할 각 속성에 대해 useSharedValue
를 사용해 반응형 상태 값을 생성합니다.
// use-animation.tsx
const opacity = useSharedValue(animations.opacity?.initial ?? 1);
const translateX = useSharedValue(animations.translateX?.initial ?? 0);
runAnimation
)runAnimation
함수는 선언된 객체를 Reanimated 함수로 변환하는 핵심 번역기 역할을 합니다. 사용자가 전달한 animations
객체의 type
, sequence
등의 옵션을 분석하여 withTiming
, withSpring
같은 Reanimated의 고차 함수를 조합하고, sharedValue
에 할당하여 애니메이션을 실행시킵니다.
// use-animation.tsx
const runAnimation = useCallback((key, config) => {
// 1. trigger 상태에 따라 최종 목표값인 target을 계산하고
const target = determineTargetValue(config.to, trigger);
// 2. config 옵션에 맞춰 withTiming, withSpring 등으로 변환해주십니다
const reanimatedAnimation = createReanimatedAnimation(target, config);
// 3. 그리고 그걸 sharedValue에 할당하여 애니메이션 실행합니다
sharedValues[key].value = reanimatedAnimation;
}, [trigger]);
자주 사용하는 애니메이션을 매번 새로 정의하는 것은 비효율적입니다. 그래서 간단한 객체 형태로 프리셋을 미리 정의해두고, usePresetAnimation
이라는 래퍼 훅(Wrapper Hook)을 통해 쉽게 불러와 사용할 수 있도록 설계했습니다.
usePresetAnimation
은 이름에 맞는 프리셋 객체를 찾아 overrides
옵션과 병합한 뒤, 최종적으로 위에서 설명한 useAnimation
훅에 전달하는 간단한 구조입니다.
// use-preset-animation.tsx
export function usePresetAnimation(patternName, options) {
const basePattern = animationPatterns[patternName];
const finalAnimations = mergeOverrides(basePattern, options.overrides);
return useAnimation({ /* ... */ });
}
네, 좋은 생각입니다. '어떻게 만들었는지'를 설명했으니, **'어떻게 사용할 수 있는지'**에 대한 명확한 가이드를 추가하면 글이 훨씬 더 유용해지겠죠.
독자들이 글을 읽고 바로 따라 해 볼 수 있도록, 쉽고 명확한 사용법 섹션을 추가해 보겠습니다.
이 내용은 '핵심 아이디어' 섹션과 '어떻게 만들었을까요? (코드 깊이 보기)' 섹션 사이에 넣으면 가장 자연스러울 것 같습니다.
라이브러리 사용법은 아주 간단합니다.
가장 먼저, 프로젝트에 라이브러리를 설치해주세요.
npm install reanimated-composer
# 또는 yarn add reanimated-composer
useAnimation
)직접 애니메이션을 조합하고 싶을 땐 useAnimation
훅을 사용합니다. Animated.View
와 함께 사용하는 전체 예시는 다음과 같습니다.
import React, { useState } from 'react';
import { Button, View, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';
import { useAnimation } from 'reanimated-composer';
export default function App() {
const [isVisible, setIsVisible] = useState(false);
const { animatedStyle } = useAnimation({
trigger: isVisible, // 이 값이 바뀔 때마다 애니메이션이 실행되게 됩니다.(트리거 역할)
animations: {
opacity: { to: isVisible ? 1 : 0 },
transform: [
{ scale: isVisible ? 1 : 0.8 },
{ translateY: isVisible ? 0 : 20 },
],
},
});
return (
<View style={styles.container}>
<Button title="Toggle Animation" onPress={() => setIsVisible(!v => !v)} />
{isVisible && (
<Animated.View style={[styles.box, animatedStyle]} />
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
box: { width: 100, height: 100, backgroundColor: 'royalblue' },
});
trigger
값이 바뀔 때마다, animations
객체에 정의된 스타일이 자동으로 적용됩니다. useEffect
없이도 모든 것이 선언적으로 작동하는 것을 볼 수 있습니다.
usePresetAnimation
)더 간단하게, 미리 준비된 애니메이션을 이름만으로 불러올 수도 있습니다.
import { usePresetAnimation } from 'reanimated-composer';
const { animatedStyle } = usePresetAnimation('slideInUp', {
trigger: isVisible,
});
만약 프리셋을 살짝 바꾸고 싶다면, overrides
옵션을 추가하기만 하면 됩니다.
usePresetAnimation('slideInUp', {
trigger: isVisible,
overrides: {
translateY: { initial: 100 },
opacity: { duration: 500 },
},
});
혼자만의 고민으로 시작했지만, 이 경험을 공유하는 것이 저와 비슷한 문제를 겪는 다른 개발자분들께 작은 도움이 될 수 있겠다는 생각에 글을 작성하게 되었습니다. Reanimated Composer
는 아직 부족한 점이 많은 작은 프로젝트이고, 사실 react native와 react native reanimated에 익숙하신 분들에게는 큰 도움이 안되고, 불필요한 레이어만 추가되어 복잡성만 늘어났다고 생각하실 수 있습니다. 하지만 아직 익숙하지 않은 분들께서 쉽게 애니메이션을 구현할 수 있다는 점과 선언적인 접근을 통해 애니메이션 코드의 복잡성을 줄이려는 시도 자체는 의미가 있었다고 생각합니다.
라이브러리는 아래 링크에서 확인하실 수 있습니다. 코드에 대한 어떤 의견이나 아이디어든 자유롭게 남겨주시면 감사드리겠습니다!
npm install reanimated-composer
긴 개발기 읽어주셔서 감사합니다.
헉 저희 서비스도 리액트 네이티브로 개발중인데 너무 좋네요!! 만들어주셔서 감사합니다(_ _)