공식 예제를 통한 ReAnimated 입문 해보기

진돌·2023년 1월 10일
3

React-Natvie

목록 보기
1/1


안녕하세요.
오늘은 Reanimated 2 에 관련해서 공식문서를 보고 간단하게 정리해본 내용을 설명 드리겠습니다.

ReAnimated?

한문장으로 설명드리면 UI thread에서 실행되는 애니메이션 및 상호 작용을 생성해주는 React Native 라이브러리입니다.

등장 배경

React Native 자체는 Animation을 제공해주지 않냐? 당연히 그것은 아닙니다.
React Native에서 공식적으로 Animated API를 제공해주고 있습니다.

근데 Animated API thread에서 비동기적으로 UI thread와 통신을 하고 있으며 이 때 JS thread가 할일이 많아지게 되면 프레임 드롭이 생기는 문제가 발생하게 됩니다.

그래서 애니메이션 및 상호 작용의 무거운 작업들을 모두 UI thread에서 작업을 필요로 하게 되며 등장하였습니다.

Reanimated V2 개념

worklets

UI 스레드에 보조 JS 컨텍스트를 생성하여 javascript 기능을 실행할 수 있게 해줍니다.

(즉 별개의 컴파일 과정을 거칩니다.)

정해진 인수 외에도 정의된 컨텍스트를 캡처합니다.

  • 생성 방법
const width = 135.5;

function otherWorklet() {
  'worklet';
  console.log('Captured width is', width);
}

js 코드 안에서 함수안에 'worklet'의 지시문을 넣어주면 됩니다.

하지만!

실제 사용할 때는 worklet을 생성할 필요는 거의 없습니다.
실제 공식 예제를 봐도 worklet을 사용한 예제는 거의 없습니다.

아래 hook을 사용하여 worklet을 생성하고 shareValue의 변경을 감지하며 사용할 수 있습니다.

useAnimatiedStyle, useDerivedValue, useAnimatedGestureHandler

SharedValue

JS, UI Thread 간의 공유 할 수 있는 value입니다. js의 모든 타입을 지원합니다.

UI 쓰레드에서 읽히도록 최적화 되어 있습니다. UI 쓰레드에서만 동기적으로 실행이 되고

JS 쓰레드에서는 비동기적으로 처리 됩니다.
(즉 렌더링이 일이나기전까지는 JS 쓰레드 상에서는 변경되어도 이전 값을 보게 됩니다!)

const offset = useSharedValue(0);

offset SharedValue가 변경될 때마다 종속된 worklet을 실행하게 될 것입니다.

간단한 예제

const Box = React.memo(function Box() {
  const offset = useSharedValue(0);

  const [render, setRender] = useState(0);

  const animatedStyles = useAnimatedStyle(() => {
    return {
      transform: [{translateX: offset.value}],
    };
  });

  return (
    <>
      <Animated.View style={[styles.box, animatedStyles]} />
      <Button
        onPress={() => (offset.value = Math.random() * 255)}
        title="Move"
      />
    </>
  );
});

useSharedValue를 통해서 share value를 생성해주었고
해당 값을 버튼을 클릭할 때마다 랜덤으로 변경해주고 있습니다.

위에서 잠깐 언급했던 useAnimatedStyle을 사용하여 worklet이 생성하였습니다.
그러므로 안에서 사용하는 shareValue가 변경될 때마다 useAnimatedStyle로 만든 메소드가 호출이 되고 스타일이 업데이트 됩니다.

하지만 뭔가 애니메이션이라기에는 뚝뚝 끊기는 느낌이 듭니다 ㅡㅅㅡ
custom을 해봅시다!!

Custom

https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/animations

해당 파트는 사용시에 각 메소드별로 더 자세한 설명은 공식문서를 통해서 보는 것을 추천드립니다. 어떤 동작을 하는지에 대한 간단한 설명만 하고 넘어가도록 하겠습니다.

utility methods

먼저 동작 모션 자체의 도움을 주는 메소드들 입니다.
뚝뚝 끊기지 않고 부드럽게 이어지는 기능들을 제공해줍니다.

  • withTiming
<Button
	onPress={() =>
		(offset.value = withTiming(Math.random() * 255, {
			duration: 3000,
			easing: Easing.out(Easing.exp),
		}))}
	title="Move"
/>

  • withSpring
<Button
	onPress={() =>
  		(offset.value = withSpring(Math.random() * 255))}
	title="Move"
/>

animation modifiers

애니메이션을 반복 작업하게 해준다던가, 하나의 이벤트로 만드는 것을 제공해주는 함수들이 있습니다.

<Button
        onPress={() =>
          (rotation.value = withSequence(
            withTiming(-10, {duration: 50}),
            withRepeat(withTiming(20), 6, true),
            withTiming(-10, {duration: 50}),
            withTiming(0, {duration: 100}),
          ))
        }
        title="rotate"
      />

Event

다음은 Event 관련 파트입니다!

ReAnimated에서는 react-native-gesture-handler 패키지를 사용하여 이벤트를 다루고 있습니다.

이벤트 파트에 앞서 간단하게 제스처 헨들러에 대해서 설명드리겠습니다.

TapGestureHandler, PanGestureHandler 등 여러가지 제스처 헨들러가 존재하고 이번에 설명할 때 예시를 보여드릴 제스처는 PanGestureHandler 입니다.

<PanGestureHandler onGestureEvent={eventHandler}><Animated.View style={[styles.ball, uas]} /></PanGestureHandler>
  • useAnimatedGestureHandler

이 hook을 사용하여 발생하는 아래 세가지의 제스처에 따른 worklet을 정의할 수 있습니다.

https://velog.velcdn.com/images/yeojinseuk/post/fa52c4bd-631b-4b0f-894d-01c7363b83a3/image.png

아래처럼 useAnimatedGestureHandlers를 통해서 생성해줄 수 있습니다.

const eventHandler = useAnimatedGestureHandler<
    PanGestureHandlerGestureEvent,
    GestureCtxType
  >({
    onStart: (event, ctx) => {
      pressed.value = true;
      ctx.startX = x.value;
      ctx.startY = y.value;
    },
    onActive: (event, ctx) => {
      x.value = ctx.startX + event.translationX;
      y.value = ctx.startY + event.translationY;
    },
    onEnd: (event, ctx) => {
      pressed.value = false;
    },
  });

각 worklet은 event와 context 인수를 제공해줍니다.

event는 클릭한 event에 대한 정보입니다.

context는 각 제스처 worklet에서 공유할 수 있는 javascript Object 입니다.

Event 예제

위에서 설명한 제스처 이벤트와 worklet, shareValue 모두 다 사용한 예제를 만들어 봅시다!

https://velog.velcdn.com/images/yeojinseuk/post/61ce6e2e-08cc-48fe-a873-3eb8a2004ac9/image.gif

어떻게 만들지 감이 오시나요?

뭐가 필요한지 하나씩 같이 생각해 봅시다.

  1. 제스처 이벤트
  2. pressed 여부를 파악할 shareValue
  3. event위치에 따른 transform 위치를 넣을 x축,y축의 shareValue

이렇게 세가지로 나눠서 생각해볼 수 있을 것 같네요!

  1. sharedValue와 worklet 만들어주기
  const pressed = useSharedValue(false);

  const x = useSharedValue(0);
  const y = useSharedValue(0)

  const animationStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: pressed.value ? 'tomato' : 'yellow',
      transform: [{translateX: x.value}, {translateY: y.value}],
    };
  });

위에서 말한 것처럼 pressed 여부와 x,y의 좌표값을 받아줄 수 있는 sharedValue를 만들어주었습니다.그리고 useAnimatedStyle() hook을 사용하여 worklet 메소드를 만들어주었습니다.이제 sharedValue가 변경될 때 마다 해당 애니메이션 스타일을 갖고 있는 view는 해당 스타일로 변경될 것입니다.

  1. 제스처 이벤트 헨들러 view 만들어주기
  return (
    <PanGestureHandler onGestureEvent={eventHandler}><Animated.View style={[styles.ball, animationStyle]} /></PanGestureHandler>);

PanGestureHandler로 감싸진 Animated.View를 만들어주었습니다.

  1. 이벤트 헨들러 메소드 만들어주기
  const eventHandler = useAnimatedGestureHandler<
    PanGestureHandlerGestureEvent,
    GestureCtxType
  >({
    onStart: (event, ctx) => {
      pressed.value = true;
      ctx.startX = x.value;
      ctx.startY = y.value;
    },
    onActive: (event, ctx) => {
      x.value = ctx.startX + event.translationX;
      y.value = ctx.startY + event.translationY;
    },
    onEnd: (event, ctx) => {
      pressed.value = false;
    },
  });

useAnimatedGestureHandler를 사용하여 PanGestureHandlerGestureEvent와 공유할 context값의 타입을 지정하여 메소드를 만들어 주었습니다.

클릭하였을 때인 onStart에서는 pressed 값을 true로 변경해주고공유해줄 값에 시작위치 x,y 좌표를 넣어주었습니다.

눌린 상태에서 움직일때마다 호출될 onActive함수에서는 현재 event의 좌표를 context값에 업데이트를 계속 해주고 있습니다.

손을 떼었을 때 발생할 onEnd에서는 pressed value를 false로 만들어주었습니다

이렇게 함으로써 pressed는 눌리는 여부에 따라 색상 변경이 되었고눌린 상태에서 움직일 때마다 x,y좌표가 변하게되면서 view의 위치도 같이 변하는 애니메이션을 완성하였습니다.

아래는 전체 코드입니다.

type GestureCtxType = {
  startX: number;
  startY: number;
};

export const AnimationStep2 = React.memo(function AnimationStep2() {
  return <EventsExample />;
});

const EventsExample = () => {
  const pressed = useSharedValue(false);

  const x = useSharedValue(0);
  const y = useSharedValue(0);

  const animationStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: pressed.value ? 'tomato' : 'yellow',
      transform: [{translateX: x.value}, {translateY: y.value}],
    };
  });

  const eventHandler = useAnimatedGestureHandler<
    PanGestureHandlerGestureEvent,
    GestureCtxType
  >({
    onStart: (event, ctx) => {
      pressed.value = true;
      ctx.startX = x.value;
      ctx.startY = y.value;
    },
    onActive: (event, ctx) => {
      x.value = ctx.startX + event.translationX;
      y.value = ctx.startY + event.translationY;
    },
    onEnd: (event, ctx) => {
      pressed.value = false;
    },
  });
  return (
    <PanGestureHandler onGestureEvent={eventHandler}><Animated.View style={[styles.ball, animationStyle]} /></PanGestureHandler>);
};

const styles = StyleSheet.create({
  ball: {
    width: 100,
    height: 100,
    borderRadius: 50,
    backgroundColor: 'black',
    marginTop: 10,
  },
});

마치며

공식문서를 보며 튜토리얼 과정을 따라해보는 아주 간단한 정리였지만 누군가에게는 도움이 되었기를 바라겠습니다.

읽어주셔서 감사합니다.

0개의 댓글