클라이언트의 요청사항 중 인터렉션과 관련된 요구사항에 외부 라이브러리를 사용하기 어렵거나, 이미 만들어진 틀에 새로운 기능을 추가해야 하는 경우가 있습니다.
예를 들어, 기존에는 클릭으로 다음 페이지를 넘어가거나 특정한 모달을 띄웠다면, 거기에 더하여 좌우로 스와이핑(swiping)하여 페이지를 넘길 수 있게 하는 이밴트 기반의 요구사항 같은 종류가 있겠습니다.
다양한 해결 방법이 있겠지만, 저는 react-native-gesture-handler를 통해 해결한 방법을 공유하고자 합니다.
먼저 react-native-gesture-handler를 설치합니다.
react-navigation을 사용하고 있다면 이미 설치되어 있을 것 같습니다.
npm install react-native-gesture-handler
설치후에 앱 최상단 컴포넌트를 루트컨테이너로 덮어줍니다.
flex:1 속성을 입력하지 않으면 빈 공백이 출력됩니다!
<GestureHandlerRootView style={{flex:1}}> // 루트컨테이너.
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="RenderPage" component={Home} options={{ headerShown:true }} />
<Stack.Screen name="SomePage" component={Some} options={{ headerShown:false }} />
</Stack.Navigator>
</NavigationContainer>
</GestureHandlerRootView>
이제 앱 내 어디에서든 [Something]GestureHandler를 통해 원하는 인터렉션을 구현할 수 있습니다.
먼저 특정 페이지에서 탭을 두번 두드리면 어떤 인터렉션이 일어나게끔 만들어 보겠습니다.
import { FlatList, View, useWindowDimensions } from 'react-native'
import React, { memo, useCallback, useRef, useState } from 'react'
import OrgazationGroupItem from '../orgazationgroup/OrgazationGroupItem'
import { State, TapGestureHandler } from 'react-native-gesture-handler'
const OrgazationChartFlatList = ({group,showModalHandler,setTabIndex,setFoundUser,tabIndex}) => {
const tapRef = useRef(null);
const { width: windowWidth } = useWindowDimensions()
const [firstTouchOffSet,setFirstTouchOffSet] = useState(0)
const onDoubleTapEvent = useCallback((e) => {
if(e.nativeEvent.state === State.ACTIVE){
tabIndex === 2 ? setTabIndex(0) : setTabIndex((prev) => prev + 1)
}
},[State.ACTIVE,tabIndex])
return (
<TapGestureHandler
ref={tapRef}
onHandlerStateChange={onDoubleTapEvent}
numberOfTaps={2}
>
<FlatList
// scrollEventThrottle={16}
// onScrollEndDrag={(event) => {
// if(event.nativeEvent.contentOffset.y > 100) {
// setFoundUser([]);
// };
// }}
// data={Object.keys(group)}
// keyExtractor={(item) => item}
// renderItem={({ item }) => (
// <OrgazationGroupItem
// item={item}
// showModalHandler={showModalHandler}
// key={item.staff_name}
// group={group}
// />
// )}
/>
</TapGestureHandler>
)
}
export default memo(OrgazationChartFlatList)
사용방법은 간단합니다.
인터렉션이 일어날 컴포넌트의 최상단에 사용할 [Something]GestureHandler로 덮어준 뒤, [SomeThing]에 맞는 State가 Active가 될 때 어떠한 이밴트가 실행되야 하는지를 입력해 주면 됩니다.
const onDoubleTapEvent = useCallback((e) => {
if(e.nativeEvent.state === State.ACTIVE){
tabIndex === 2 ? setTabIndex(0) : setTabIndex((prev) => prev + 1)
}
},[State.ACTIVE,tabIndex])
TapGesture의 State.Active는 유저가 화면을 tapping하였을 때 트루값이 되지만, Handler의 numberOfTaps 속성을 2로 주었기 때문에 2번 tapping이 일어나야지만 State.Active가 트루값이 됩니다.
정말 간단합니다.
하지만 고객이 하나의 컴포넌트에 하나의 인터렉션만을 원하지는 않을겁니다.
이럴때 하나의 컴포넌트에 2개의 중첩된 GestureHandler를 사용할 수는 없기에 다른 방법을 찾아야 하는데요,
제 경우는 더블탭과 동시에 Swiping으로도 이벤트가 일어나야 했고, 기본 RN의 OnTouchStart, onTouchEnd api를 사용했습니다.
import { FlatList, View, useWindowDimensions } from 'react-native'
import React, { memo, useCallback, useRef, useState } from 'react'
import OrgazationGroupItem from '../orgazationgroup/OrgazationGroupItem'
import { State, TapGestureHandler } from 'react-native-gesture-handler'
const OrgazationChartFlatList = ({group,showModalHandler,setTabIndex,setFoundUser,tabIndex}) => {
const tapRef = useRef(null);
const { width: windowWidth } = useWindowDimensions()
const [firstTouchOffSet,setFirstTouchOffSet] = useState(0)
const onDoubleTapEvent = useCallback((e) => {
if(e.nativeEvent.state === State.ACTIVE){
tabIndex === 2 ? setTabIndex(0) : setTabIndex((prev) => prev + 1)
}
},[State.ACTIVE,tabIndex])
return (
<TapGestureHandler
ref={tapRef}
onHandlerStateChange={onDoubleTapEvent}
numberOfTaps={2}
>
<View
onTouchStart={(e) => {
setFirstTouchOffSet(e.nativeEvent.pageX)
}}
onTouchEnd={(e) => {
const positionX = e.nativeEvent.pageX
const range = windowWidth / 20
if(positionX - firstTouchOffSet > range){
tabIndex === 0 ? setTabIndex(2) : setTabIndex((prev) => prev - 1)
}
else if(firstTouchOffSet - positionX > range){
tabIndex === 2 ? setTabIndex(0) : setTabIndex((prev) => prev + 1)
}
}}
>
<FlatList
// scrollEventThrottle={16}
// onScrollEndDrag={(event) => {
// if(event.nativeEvent.contentOffset.y > 100) {
// setFoundUser([]);
// };
// }}
// data={Object.keys(group)}
// keyExtractor={(item) => item}
// renderItem={({ item }) => (
// <OrgazationGroupItem
// item={item}
// showModalHandler={showModalHandler}
// key={item.staff_name}
// group={group}
// />
// )}
/>
</View>
</TapGestureHandler>
)
}
export default memo(OrgazationChartFlatList)
사용자가 좌/우로 swiping할 때, 디바이스의 width값(windowWidth)을 구하고, 디바이스의 일정 비율만큼 swiping하였다면, 다음 탭으로 넘어가는 기능입니다.
React Native에서 View Class를 상속 받는 모든 위젯은 기본으로 onTouchStart, onTouchEnd와 같은 이벤트 메서드를 제공합니다.
GestureHandler처럼 쉽게 구현할 수 있는 것은 아니지만, 이 또한 잘 활용한다면 좋은 기능이 될 것 같습니다.
그렇다면, swiping 기능을 제공하는 GestureHandler는 무엇일까요?
FlingGestureHandler는 사용자의 상/하/좌/우 Swiping을 감지하여 특정 방향일 때 특정 이밴트를 발생하게 도와주는 GestureHandler입니다.
import { Directions, FlingGestureHandler, State } from 'react-native-gesture-handler';
//...
<FlingGestureHandler
direction={ Directions.RIGHT | Directions.LEFT}
onHandlerStateChange={(e) => {
if(e.nativeEvent.state === State.ACTIVE){
navigation.navigate('Some',{showChart:0})
}
}}
>
<View>
// ... 너무 지저분해서 내용은 전부 날렸습니다!
</View>
</FlingGestureHandler>
저는 좌/우만 필요하기에 direction 속성의 값으로 Right / Left만 사용한다고 명시해주었습니다.
direction을 여러개 사용할 때에는 ||가 아닌 |를 사용해아 합니다!
FlingGestureHandler 역시 위 TapGestureHandler와 마찬가지로 State.ACTIVE의 값에 따라 실행 여부가 달라집니다.
그리고 direction과 관계 없이, State.ACTIVE가 트루값이 되면, 로케이션 객체를 가지고 Some 페이지로 이동하게 하였습니다.
만약 direction에 따라 다르게 분기처리를 하고 싶다면, Directions 클래스를 활용하여 분기처리를 하면 되겠습니다.
여기까지 간단하게 GestureHandler를 알아보았습니다.
구현은 간단하였지만, 여기까지 생각하며 고민했던 시간이 많이 길었던 것 같습니다.
긴 글 읽어주셔서 감사합니다.