제스처 응답 시스템
제스처 응답 시스템은 앱 내에서 제스처의 생명주기를 관리한다. 사용자의 의도를 파악하기 위해 터치는 여러 단계를 거칠 수 있다. 예를 들어, 앱은 터치가 스크롤인지, 위젯을 슬라이드하는 것인지, 탭인지 판단해야 한다. 이러한 판단은 터치가 진행되는 동안에도 바뀔 수 있다. 또한 동시에 여러 터치가 발생할 수도 있다.
터치 응답 시스템은 컴포넌트가 부모나 자식 컴포넌트에 대한 추가 정보 없이도 이러한 터치 상호작용을 조정할 수 있도록 해준다.
모범 사례
앱을 사용자 친화적으로 만들려면 모든 동작에 다음과 같은 속성을 포함해야 한다:
- 피드백/하이라이트: 사용자가 터치한 부분을 시각적으로 표시하고, 제스처를 놓았을 때 어떤 일이 발생할지 알려준다.
- 취소 가능성: 사용자가 동작을 수행하는 도중에도 손가락을 드래그하여 중간에 취소할 수 있어야 한다.
이러한 기능은 사용자가 실수를 두려워하지 않고 자유롭게 앱을 탐색하고 상호작용할 수 있도록 돕는다. 결과적으로 사용자 경험이 더욱 편안해진다.
TouchableHighlight와 Touchable* 컴포넌트
리액트 네이티브의 리스폰더 시스템은 사용하기 복잡할 수 있다. 그래서 '탭 가능한' 요소를 위해 추상화된 Touchable
구현체를 제공한다. 이는 리스폰더 시스템을 활용하며, 탭 인터랙션을 선언적으로 설정할 수 있게 해준다. 웹에서 버튼이나 링크를 사용하는 곳이라면 어디든 TouchableHighlight
를 사용할 수 있다.
Responder Lifecycle
뷰는 특정 협상 메서드를 구현해 터치 응답자로 설정할 수 있다. 뷰가 응답자가 되길 원하는지 확인하는 두 가지 메서드가 있다:
View.props.onStartShouldSetResponder: evt => true,
- 터치 시작 시 이 뷰가 응답자가 되길 원하는지 여부를 결정한다.View.props.onMoveShouldSetResponder: evt => true,
- 뷰가 응답자가 아닐 때 터치 이동이 발생할 때마다 호출된다. 이 뷰가 터치 응답을 "청구"할지 여부를 결정한다.
뷰가 true를 반환하고 응답자가 되려고 시도하면 다음 중 하나가 발생한다:
View.props.onResponderGrant: evt => {}
- 뷰가 이제 터치 이벤트에 응답한다. 사용자에게 현재 상황을 강조하고 보여줄 적절한 시점이다.View.props.onResponderReject: evt => {}
- 다른 무언가가 현재 응답자이며, 이를 해제하지 않을 것이다.
뷰가 응답 중일 때 다음 핸들러가 호출될 수 있다:
View.props.onResponderMove: evt => {}
- 사용자가 손가락을 움직이고 있다.View.props.onResponderRelease: evt => {}
- 터치가 끝날 때 발생하며, "touchUp" 이벤트와 같다.View.props.onResponderTerminationRequest: evt => true
- 다른 무언가가 응답자가 되길 원한다. 이 뷰가 응답자를 해제해야 할까? true를 반환하면 해제를 허용한다.View.props.onResponderTerminate: evt => {}
- 응답자가 뷰에서 제거되었다.onResponderTerminationRequest
호출 후 다른 뷰가 응답자를 가져갔거나, OS가 묻지 않고 가져갔을 수 있다(iOS의 컨트롤 센터/알림 센터에서 발생).
evt
는 다음과 같은 형태의 합성 터치 이벤트다:
nativeEvent
changedTouches
- 마지막 이벤트 이후 변경된 모든 터치 이벤트의 배열identifier
- 터치의 IDlocationX
- 엘리먼트를 기준으로 한 터치의 X 위치locationY
- 엘리먼트를 기준으로 한 터치의 Y 위치pageX
- 루트 엘리먼트를 기준으로 한 터치의 X 위치pageY
- 루트 엘리먼트를 기준으로 한 터치의 Y 위치target
- 터치 이벤트를 받는 엘리먼트의 노드 IDtimestamp
- 터치의 시간 식별자, 속도 계산에 유용touches
- 화면의 모든 현재 터치 배열
onStartShouldSetResponder
와 onMoveShouldSetResponder
는 버블링 패턴으로 호출된다. 이때 가장 깊은 노드가 먼저 호출된다. 즉, 여러 View가 *ShouldSetResponder
핸들러에서 true를 반환하면 가장 깊은 컴포넌트가 응답자가 된다. 이는 대부분의 경우에 바람직한 동작이다. 모든 컨트롤과 버튼이 사용 가능하도록 보장하기 때문이다.
하지만 때로는 부모 컴포넌트가 응답자가 되도록 해야 할 때가 있다. 이 경우 캡처 단계를 사용해 처리할 수 있다. 응답자 시스템이 가장 깊은 컴포넌트에서 버블링되기 전에 캡처 단계를 거치며 on*ShouldSetResponderCapture
를 호출한다. 따라서 부모 View가 터치 시작 시 자식이 응답자가 되지 않도록 하려면 onStartShouldSetResponderCapture
핸들러에서 true를 반환해야 한다.
View.props.onStartShouldSetResponderCapture: evt => true,
View.props.onMoveShouldSetResponderCapture: evt => true,
PanResponder
더 높은 수준의 제스처 해석을 원한다면 PanResponder를 확인해 보자.