Allen Dev Blog

React Native 에서 프레임 드랍없는 애니메이션 만들기

React Native 에서 프레임 드랍없는 애니메이션 만들기

React Native 에서는 애니메이션 효과 적용을 위해 Animated 라이브러리를 기본적으로 제공하고 있습니다. 이외에 애니메이션 효과를 주기 위한 외부 라이브러리도 있는데 대표적으로 react-native-reanimated 라이브러리가 있습니다. 이번 포스트에서는 RN 에서 기본으로 제공하는 Animated 라이브러리가 아닌 react-native-reanimated 라이브러리를 사용하여 Animation 효과를 주는 방법에 대해 설명해보겠습니다.

(이 포스트는 react-native-reanimated v2 기준으로 하며, v2 는 RN 0.62 버전 이상에서만 작동합니다)

RN Animated vs react-native-reanimated

사실 RN 에서 기본으로 제공하는 Animated 자체도 훌륭하며, 이를 통해 많은 애니메이션 효과를 줄 수 있습니다. 하지만 React Native 자체의 구조적인 문제로 인하여 Animated 라이브러리성능 문제가 많이 발생하곤 합니다. (특히 Android 에서) 개인적인 경험으로는 Fade In, Fade Out 등의 효과를 Animated 라이브러리를 사용했을 때는 안드로이드에서 굉장히 낮은 퍼포먼스를 보여주었습니다.

react-native-reanimated 라이브러리는 UI Worklet 을 따로 생성하여 애니메이션에 필요한 계산을 따로 작업하게 됩니다. 프레임 드랍 없이 애니메이션 효과를 주기 위해서는 빠르게 연산이 필요하고, 연산 과정에서 지연이 발생하면 안되는데 react-native-reanimated 는 Worklet 을 따로 생성하여(간단하게 애니메이션 효과를 주기 위한 Thread 를 따로 생성하는 것으로 이해하면 좋을 것 같습니다) 이런 연산 과정을 수행하므로 훨씬 더 효과적으로 애니메이션 효과를 작동시킬 수 있습니다.

또한 현재 react-native-reanimated 는 기본 라이브러리인 Animated 라이브러리에서 제공하는 대부분의 기능들을 모두 제공하고 있습니다.

react-native-reanimated 는 다른 쓰레드를 사용하기 때문에 난이도도 기본 라이브러리에 비해 더 어렵기도 하고, 잘못 구현하면 쓰레드의 특성 상 앱이 강제 종료될 수 있다는 문제점을 가지고 있지만 익숙해지면 훨씬 더 애니메이션을 풍부하고 자연스럽게 표현할 수 있다는 장점이 있습니다.

이러한 점에서 애니메이션 효과를 줄 때 굳이 react-native-reanimated 라이브러리를 사용하지 않을 이유가 없다고 생각합니다.

설치하기

먼저 npm 또는 yarn 패키지 툴을 이용하여 react-native-reanimated 라이브러리를 설치합니다.

yarn add react-native-reanimated

설치하면 끝나는 것이 아니라 안드로이드 및 iOS 에서 구동하기 위해서 추가적인 작업이 필요합니다. 안드로이드부터 먼저 해보겠습니다. 우선 android/app.build.gradle 파일에 들어가서 중간에

...
project.ext.react = [
  enableHermes: true  // 기본값 false 로 되어있는 것을 true 로 변경 
]
...

enableHermes 를 true 로 변경합니다. RN 공식문서에 따르면 hermes 엔진은 자바스크립트 엔진으로 사용하면 초기 실행 속도가 향상되고, 메모리 사용량을 줄일 수 있으며 앱의 크기를 줄일 수 있다고 합니다. 좀 더 자세한 설명은 공식 문서를 확인해보시기 바랍니다.

이제는 android/app/src/main/MainApplication.java 를 들어가서

...

// 추가적으로 Import 하기
import com.facebook.react.bridge.JSIModulePackage;
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
...

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost =
      new ReactNativeHost(this) { 
      ...
  
      // 아래와 같이 함수 추가
      @Override
      protected JSIModulePackage getJSIModulePackage() {
        return new ReanimatedJSIModulePackage();
      }
    };
    
  ...

Java 파일에서 Import 를 추가하고, 중간에 mReactNativeHost 선언부에서 getJSIModulePackage 함수를 오버라이딩해서 위와 같이 작성하면 됩니다.

iOS 에서는 더 간단한데 ios 디렉토리에 들어가서 pod install 만 하면 됩니다.

더 자세한 설치 방법에 대한 문서는 react-native-reanimated 공식 문서 를 참고하면 될 것 같습니다.

Shared Value

먼저 애니메이션 효과를 주기 전에 애니메이션 효과를 주기 위해 사용하는 변수를 정의해야 하는데요. 이 변수를 정의하기 위해 사용하는 것이 Shared Value 입니다. 이름이 왜 Shared Value 일까요?

아까 말했듯이 react-native-reanimated 는 애니메이션 효과의 수학적 계산을 다른 쓰레드에서 작동시킵니다. 따라서 RN 을 작동시키는 JS Main Thread 와 애니메이션 효과를 주는 Thread 간에 공유하는 메모리 공간을 가지고 있어야 하는데요. 이를 위해 정의하는 것이 Shared Value 입니다.

개념은 어렵지만 이 Shared Value 는 간단히 앞서 말했듯이 라이브러리 내에서 애니메이션 효과를 줄 때 X축, Y축 위치라던지, 투명도(opacity) 등 애니메이션 효과를 위한 값들을 저장하는 변수라고 보면 될 것 같습니다. 그냥 Javascript 변수와 다른 점은 이 라이브러리는 애니메이션 효과를 위해 쓰레드를 사용하고, 이 쓰레드 간 공유할 수 있어야 한다는 점입니다. 따라서 일반적으로 Javascript 에서 변수를 선언하는 방식이 아니라 react-native-reanimated 에서 제공하는 useSharedValue hook 을 사용하여 정의하여야 합니다.

import { SharedValue } from 'react-native-reanimated';

const App = () => {
  // 애니메이션 효과에 사용하는 변수를 선언합니다. (기본 값은 0)
  const x = useSharedValue(0);
  
  ...
}

애니메이션 효과를 위한 컴포넌트 정의하기

RN 내의 특정 Component 에 Animation 효과를 주기 위해서는 그 컴포넌트를 라이브러리가 애니메이션 효과를 적용할 수 있도록 컴포넌트 특성을 바꾸어야 합니다. 이 때 필요한 함수가 createAnimatedComponent 함수입니다. 클래스 컴포넌트를 넘겨서 이 컴포넌트는 react-native-reanimated 가 애니메이션을 적용할 수 있는 컴포넌트로 변경해주어야 합니다.

(다만 아직 함수형 컴포넌트(Functional Component) 는 지원하지 않으므로 함수형 컴포넌트의 경우 Animated.View 등으로 감싸야 합니다.)

import { View } from 'react-native';
import Animated from 'react-native-reanimated';

// View 컴포넌트를 react-native-reanimated 라이브러리에서 
// 애니메이션 효과를 줄 수 있는 컴포넌트로 변경합니다.
const AnimatedView = Animated.createAnimatedComponent(View);

Custom 으로 제작한 Component 를 Animation 효과를 줄 수 있는 컴포넌트로 변경하려면 createAnimatedComponent 를 사용하여야 합니다. 다만 View, ScrollView, Text 같은 기본적인 컴포넌트의 경우 이미 정의되어 있어서 굳이 함수를 호출해서 컴포넌트를 새로 생성할 필요없이 아래와 같이 Animated.View, Animated.ScrollView, Animated.Text 를 사용하면 됩니다.

import Animated from 'react-native-reanimated';

const App = () => {
  ...
  
  return (
    <Animated.View>
      ...
    </Animated.View>
  )
} 

컴포넌트에 스타일 적용하기

먼저 애니메이션 효과를 주기 위해 스타일을 정의해야 합니다. RN 에서 제공하는 기본 라이브러리와 다르게 애니메이션 관련 스타일은 반드시 useAnimatedStyle Hook 을 사용하여 정의해야 합니다.

import React from 'react';
import { Button, SafeAreaView, StyleSheet, View } from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

const App = () => {
  const x = useSharedValue(0);

  // 애니메이션 효과를 주는 style 의 경우 useAnimatedStyle Hook 을 이용하여 따로 정의합니다.
  const animatedStyle = useAnimatedStyle(
    () => ({
      transform: [{ translateX: x.value }],
    }),
    [],
  );

  return (
    <SafeAreaView style={styles.view}>
      <View style={styles.rectView}>
        <Animated.View style={[styles.rect, animatedStyle]} />
      </View>

      <View style={styles.button}>
        <Button
          title="이동하기"
          // 버튼을 누르면 랜덤한 곳으로 이동합니다.
          onPress={() => (x.value = Math.random() * 255)}
        />
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  view: {
    paddingVertical: 100,
    paddingHorizontal: 50,
    justifyContent: 'center',
    alignItems: 'center',
  },
  rectView: {
    width: '100%',
  },
  rect: {
    width: 80,
    height: 80,
    backgroundColor: '#001a90',
    borderRadius: 10,
  },
  button: {
    padding: 20,
  },
});

export default App;

먼저 Animated.View 컴포넌트를 사용하여 사각형을 정의합니다.

중간에 useAnimatedStyle 을 사용하였고, 우리가 RN 에서 스타일 적용하듯 transform 스타일을 이용하여 현재 위치에서 x 값만큼 이동하라는 스타일을 적용하였습니다. x 의 기본값은 0 이므로 현재는 위치 이동이 되지 않은 상태입니다.

중간에 Button 의 onPress 핸들러를 보면 x.value 값을 0-255 사이의 랜덤 값으로 변경하는 코드가 들어가 있습니다. 따라서 버튼을 누를 때마다 박스는 x 축으로 랜덤하게 이동하게 됩니다.

애니메이션 효과 생성하기

react-native-reanimated 에서는 애니메이션 효과를 주기 위한 다양한 함수를 제공합니다. 대표적으로 특정 시간동안 값을 서서히 바꾸게 하는 withTiming 함수가 있습니다.

  ...
  // x 의 값을 현재 값에서 300 으로 1초동안 서서히 바뀌게 합니다.
  x.value = withTiming(300, { duration: 1000 });
  ....

이제 아까 그 코드에서 애니메이션 효과를 생성해보겠습니다.

이를 위해서는 두 가지의 접근 방법이 있는데 첫 번째로는 useAnimatedStyle 에 애니메이션 효과를 넣는 방법, 두 번째로는 onPress 핸들러에 애니메이션을 넣는 방법이 있습니다.

첫 번째 방법으로는

   ...
   const animatedStyle = useAnimatedStyle(
     () => ({
       transform: [{ translateX: withTiming(x.value, { duration: 300 }) }],
     }),
     [],
   );
   ...

onPress Handler 에 의해서 x 의 값이 변경되면 useAnimatedStyle 에 정의된 것처럼 300ms 동안 x 의 값만큼 translateX 의 값이 이동합니다.

두 번째 방법은 onPress Handler 에서 withTiming 을 사용하여 애니메이션 효과를 적용하는 방법입니다.

  ...
  <Button
    title="이동하기"
    // 버튼을 누르면 랜덤한 곳으로 이동합니다.
    onPress={() =>
      (x.value = withTiming(Math.random() * 255, { duration: 300 }))
    }
  />
  ...

어떤 방식을 사용해도 상관없으므로 애니메이션 효과를 적용할 때 상황에 맞게 사용하면 됩니다.

스프링 효과 적용하기

react-native-reanimated 에는 기본적으로 spring 효과를 줄 수 있는 함수 withSpring 을 제공합니다. 기존의 withTiming 대신 withSpring 을 사용하면 됩니다. 다만, withSpring 의 경우 duration 값을 넣을 수는 없어서 애니메이션 지속 시간을 지정할 수는 없습니다.

  ...
  <Button
    title="이동하기"
    onPress={() => (x.value = withSpring(Math.random() * 255))} 
  />
  ...

기타 효과 적용하기

이제 useSharedValue 를 이용하여 변수를 생성하고, useAnimatedStyle 을 이용하여 애니메이션 효과를 적용하는 방법을 알았으니 다른 style 도 적용해봅시다. 방금까지 translateX 를 통해 X 값 이동만 살펴봤지만 우리가 react native 에서 사용하는 대부분의 style property 를 적용할 수 있습니다.

애니메이션 효과를 반복하기 위해서는 withRepeat 를 이용하고, 여러 애니메이션을 순서대로 실행시키기 위해서는 withSequence 를 사용할 수 있습니다. 아래는 rotation 스타일을 사용하여 반복적으로 네모 박스를 흔들게 하는 애니메이션을 적용하는 예시입니다.

  ...
  const rotation = useSharedValue(0);

  useEffect(() => {
    // withRepeat 로 애니메이션을 반복합니다.
    rotation.value = withRepeat(
      // 100ms 동안 값을 5로 이동하고, 다음 100ms 동안 값을 -5로 이동합니다. 
      withSequence(
        withTiming(5, { duration: 100 }),
        withTiming(-5, { duration: 100 }),
      ),
      // 반복횟수 지정을 0 으로 설정하면 무한히 반복합니다.
      0,
    );
  }, [rotation]);

  const rotationStyle = useAnimatedStyle(
    () => ({
      transform: [
        {
          // rotate 값으로 -5에서 5 사이를 왔다갔다하는 rotation 값에 deg 를 붙여서
          // -5도에서 5도 사이를 왔다갔다하는 스타일을 적용합니다.  
          rotate: `${rotation.value}deg`,
        },
      ],
    }),
    [],
  );
  ...

이제 위에서 만든 rotationStyle 을 박스 Animated.View 스타일에 적용하면 아래와 같이 됩니다.

Conclusion

이번 포스트에서는 react-native-reanimated 라이브러리를 사용하여 간단한 애니메이션 효과를 생성해보았습니다. 라이브러리에서 제공하는 useSharedValue, useAnimatedStyle, withTiming, withSequence, withRepeat 이 대표적인 5가지의 함수를 응용하여 다양한 애니메이션 효과를 만들 수 있습니다. 예를 들어 withTiming 으로 값을 0에서 1로 또는 1에서 0으로 값을 바꾸게 하고, 이를 useAnimatedStyle 에서 opacity 값에 넣으면 FadeIn, FadeOut 효과를 낼 수 있습니다.

react-native-reanimated 라이브러리에서는 좀 더 Interactive 한 애니메이션 효과를 주기 위해서 애니메이션 효과가 끝난 후에 어떤 작업을 수행할 것인지 등을 정의할 수 있는 다양한 기능들을 제공하고 있습니다. 이와 관련해서는 다음 포스트에서 다루도록 하겠습니다.

React Native 에서 프레임 드랍없는 애니메이션 만들기
Prev post

Redis Sorted Set 을 이용한
실시간 랭킹 시스템 구축

Next post

디파이 개발 (유니스왑 편)
(1) 유니스왑 기본 개념

React Native 에서 프레임 드랍없는 애니메이션 만들기

Get in touch