Gesture Handler — 네이티브 제스처 처리
Gesture Handler는 React Native의 터치 시스템을 대체하여 네이티브 수준의 부드러운 제스처 처리를 제공합니다.
스와이프로 삭제, 드래그 앤 드롭, 핀치 줌 같은 복잡한 제스처는 기본 터치 시스템으로는 한계가 있습니다. Gesture Handler는 이를 네이티브 스레드에서 처리합니다.
설치
npm install react-native-gesture-handler
// App.tsx 또는 index.js 최상단에 추가
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<MainApp />
</GestureHandlerRootView>
);
}
제스처 타입
Pan (드래그)
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 (탭)
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 (핀치 줌)
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
const longPress = Gesture.LongPress()
.minDuration(500)
.onStart(() => {
// 진동 피드백
runOnJS(Haptics.impactAsync)();
})
.onEnd((_e, success) => {
if (success) {
runOnJS(showContextMenu)();
}
});
제스처 조합
// 동시 실행 — 핀치 줌 + 드래그 동시 처리
const composed = Gesture.Simultaneous(pinchGesture, panGesture);
// 독점 실행 — 한 제스처만 인식
const exclusive = Gesture.Exclusive(doubleTap, singleTap);
// 순차 인식 — race 방식
const race = Gesture.Race(panGesture, longPressGesture);
스와이프로 삭제 구현
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 함수 호출
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를 잊지 마세요
댓글 로딩 중...