Gesture Handler는 React Native의 터치 시스템을 대체하여 네이티브 수준의 부드러운 제스처 처리를 제공합니다.

스와이프로 삭제, 드래그 앤 드롭, 핀치 줌 같은 복잡한 제스처는 기본 터치 시스템으로는 한계가 있습니다. Gesture Handler는 이를 네이티브 스레드에서 처리합니다.


설치

BASH
npm install react-native-gesture-handler
TSX
// App.tsx 또는 index.js 최상단에 추가
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <MainApp />
    </GestureHandlerRootView>
  );
}

제스처 타입

Pan (드래그)

TSX
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';

function Draggable() {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const context = useSharedValue({ x: 0, y: 0 });

  const panGesture = Gesture.Pan()
    .onStart(() => {
      context.value = { x: translateX.value, y: translateY.value };
    })
    .onUpdate((e) => {
      translateX.value = context.value.x + e.translationX;
      translateY.value = context.value.y + e.translationY;
    })
    .onEnd((e) => {
      // 속도 기반 관성 이동 후 원위치
      translateX.value = withSpring(0, { velocity: e.velocityX });
      translateY.value = withSpring(0, { velocity: e.velocityY });
    });

  const style = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
    ],
  }));

  return (
    <GestureDetector gesture={panGesture}>
      <Animated.View style={[styles.box, style]} />
    </GestureDetector>
  );
}

Tap (탭)

TSX
function DoubleTapZoom() {
  const scale = useSharedValue(1);

  const doubleTap = Gesture.Tap()
    .numberOfTaps(2)
    .onEnd(() => {
      scale.value = withSpring(scale.value === 1 ? 2 : 1);
    });

  const style = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  return (
    <GestureDetector gesture={doubleTap}>
      <Animated.Image
        source={{ uri: 'https://example.com/photo.jpg' }}
        style={[{ width: 300, height: 200 }, style]}
      />
    </GestureDetector>
  );
}

Pinch (핀치 줌)

TSX
function PinchToZoom() {
  const scale = useSharedValue(1);
  const savedScale = useSharedValue(1);

  const pinchGesture = Gesture.Pinch()
    .onUpdate((e) => {
      scale.value = savedScale.value * e.scale;
    })
    .onEnd(() => {
      savedScale.value = scale.value;
      // 최소/최대 줌 제한
      if (scale.value < 1) {
        scale.value = withSpring(1);
        savedScale.value = 1;
      } else if (scale.value > 5) {
        scale.value = withSpring(5);
        savedScale.value = 5;
      }
    });

  const style = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  return (
    <GestureDetector gesture={pinchGesture}>
      <Animated.View style={style}>
        <Image source={{ uri: imageUrl }} style={{ width: 300, height: 300 }} />
      </Animated.View>
    </GestureDetector>
  );
}

Long Press

TSX
const longPress = Gesture.LongPress()
  .minDuration(500)
  .onStart(() => {
    // 진동 피드백
    runOnJS(Haptics.impactAsync)();
  })
  .onEnd((_e, success) => {
    if (success) {
      runOnJS(showContextMenu)();
    }
  });

제스처 조합

TSX
// 동시 실행 — 핀치 줌 + 드래그 동시 처리
const composed = Gesture.Simultaneous(pinchGesture, panGesture);

// 독점 실행 — 한 제스처만 인식
const exclusive = Gesture.Exclusive(doubleTap, singleTap);

// 순차 인식 — race 방식
const race = Gesture.Race(panGesture, longPressGesture);

스와이프로 삭제 구현

TSX
function SwipeToDelete({ onDelete, children }: {
  onDelete: () => void;
  children: React.ReactNode;
}) {
  const translateX = useSharedValue(0);
  const itemHeight = useSharedValue(60);

  const panGesture = Gesture.Pan()
    .activeOffsetX([-10, 10])
    .onUpdate((e) => {
      // 왼쪽으로만 스와이프 허용
      translateX.value = Math.min(0, e.translationX);
    })
    .onEnd(() => {
      if (translateX.value < -100) {
        // 삭제 threshold 초과
        translateX.value = withTiming(-400, {}, () => {
          itemHeight.value = withTiming(0, {}, () => {
            runOnJS(onDelete)();
          });
        });
      } else {
        // 원위치
        translateX.value = withSpring(0);
      }
    });

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

  const containerStyle = useAnimatedStyle(() => ({
    height: itemHeight.value,
    overflow: 'hidden',
  }));

  return (
    <Animated.View style={containerStyle}>
      {/* 뒤에 보이는 삭제 배경 */}
      <View style={styles.deleteBackground}>
        <Text style={{ color: 'white' }}>삭제</Text>
      </View>
      {/* 스와이프되는 콘텐츠 */}
      <GestureDetector gesture={panGesture}>
        <Animated.View style={[styles.item, itemStyle]}>
          {children}
        </Animated.View>
      </GestureDetector>
    </Animated.View>
  );
}

runOnJS — UI 스레드에서 JS 함수 호출

TSX
import { runOnJS } from 'react-native-reanimated';

const gesture = Gesture.Tap().onEnd(() => {
  // worklet 안에서 일반 JS 함수를 호출할 때 runOnJS 필요
  runOnJS(navigation.navigate)('Detail');
  runOnJS(Alert.alert)('탭됨!');
});

제스처 핸들러 콜백은 UI 스레드(worklet)에서 실행됩니다. 일반 JavaScript 함수(네비게이션, Alert, 상태 변경 등)를 호출하려면 반드시 runOnJS로 감싸야 합니다.


정리

  • Gesture Handler는 네이티브 스레드에서 제스처를 처리 하여 부드러운 인터랙션을 제공합니다
  • Pan, Tap, Pinch, LongPress 등 다양한 제스처 타입을 지원합니다
  • Simultaneous, Exclusive, Race여러 제스처를 조합 할 수 있습니다
  • Reanimated와 함께 사용 하면 제스처에 연동된 애니메이션을 UI 스레드에서 처리할 수 있습니다
  • worklet에서 JS 함수 호출 시 runOnJS 를 잊지 마세요
댓글 로딩 중...