Skip to main content

Pointer Events in React Native

· 18 min read
Luna Wei
Luna Wei
Software Engineer at Meta
Vincent Riemer
Vincent Riemer
Software Engineer at Meta

오늘 우리는 React Native를 위한 실험적인 크로스 플랫폼 포인터 API를 공개한다. 이 API의 목적, 작동 방식, 그리고 React Native 사용자에게 어떤 이점을 제공하는지 살펴볼 것이다. 또한 이를 활성화하는 방법에 대한 지침도 제공하며, 여러분의 피드백을 기대한다!

우리가 다양한 플랫폼 비전을 공유한 지 1년이 넘었다. 이 비전은 모바일을 넘어 모든 플랫폼에서 더 높은 기준을 설정하는 것에 대해 다뤘다. 그동안 우리는 VR, 데스크톱, 웹을 위한 React Native에 대한 투자를 늘렸다. 이러한 플랫폼에서의 하드웨어와 상호작용의 차이로 인해, React Native가 입력을 어떻게 종합적으로 처리해야 하는지에 대한 질문이 제기되었다.

터치를 넘어서

데스크톱과 VR은 역사적으로 마우스와 키보드 입력에 의존해 왔고, 모바일은 주로 터치를 사용해 왔다. 이제는 터치스크린 노트북과 모바일에서 키보드와 펜을 통한 상호작용을 지원해야 할 필요성이 커지면서 이 구도가 변화하고 있다. 그러나 React Native의 터치 이벤트 시스템은 이러한 요구를 처리할 수 없다.

이 때문에, out-of-tree 플랫폼 사용자들은 React Native를 포크하거나 커스텀 네이티브 컴포넌트와 모듈을 만들어 호버 감지나 왼쪽 클릭 같은 핵심 기능을 지원한다. 이러한 분기는 이벤트 핸들러가 비슷한 목적을 수행하지만 플랫폼마다 다른 프로퍼티를 사용하게 만들어 중복을 일으킨다. 이는 프레임워크의 복잡성을 증가시키고 플랫폼 간 코드 공유를 번거롭게 만든다. 이러한 이유로 팀은 크로스 플랫폼 포인터 API를 제공하려고 한다.

React Native는 다양한 플랫폼을 지원하면서도 각 플랫폼의 특성을 유지할 수 있는 견고하고 표현력 있는 API를 제공하는 것을 목표로 한다. 이러한 API를 설계하는 것은 어려운 과제지만, 다행히 React Native가 활용할 수 있는 포인터 관련 기존 사례가 있다.

웹을 바라보며

웹은 다양한 플랫폼으로 확장 가능하면서도 미래를 고려한 설계를 필요로 하는 플랫폼이다. W3C(World Wide Web Consortium)는 서로 다른 플랫폼과 브라우저 간에 상호 운용 가능한 웹을 구축하기 위한 표준과 제안을 정하는 역할을 맡고 있다.

우리에게 가장 관련이 깊은 것은 W3C가 포인터라는 추상적인 입력 형태에 대한 동작을 정의한 것이다. Pointer Events 명세는 마우스 이벤트를 기반으로 하며, 크로스 디바이스 포인터 입력을 위한 단일 이벤트 및 인터페이스 세트를 제공하는 동시에 필요할 때 디바이스별 처리를 허용한다.

Pointer Events 명세를 따르면 React Native 사용자에게 많은 이점이 있다. 앞서 언급한 문제를 해결할 뿐만 아니라, 다중 입력 타입 상호작용을 고려할 필요가 없었던 플랫폼의 기능을 향상시킨다. 예를 들어, Android 폰에 블루투스 마우스를 연결하거나 iPad M2에서 Apple Pencil의 호버 기능을 지원하는 경우를 생각해 볼 수 있다.

명세를 준수하면 웹과 React Native 간의 지식 공유 기회도 제공한다. Pointer Events에 대한 웹 기대치를 교육하면 React Native 개발자에게도 도움이 된다. 하지만 React Native의 요구사항은 웹과 다르며, 우리의 명세 접근 방식은 최선을 다하되 잘 문서화된 차이점을 통해 기대치를 명확히 하는 것이다. 접근성 및 성능 API에서 API 분산을 줄이기 위해 특정 웹 표준을 맞추는 관련 작업도 진행 중이다.

웹 플랫폼 테스트 포팅하기

Pointer Events 명세는 API의 인터페이스와 동작을 설명하지만, 우리가 명세를 검증 자료로 삼고 변경 사항을 확신하기에는 충분히 구체적이지 않았다. 하지만 웹 브라우저는 Web Platform Tests를 사용해 규정 준수와 상호 운용성을 보장한다.

Web Platform Tests는 브라우저의 명령형 DOM API를 대상으로 작성되었다. React Native는 자체 뷰 프리미티브를 사용하기 때문에 이를 지원하지 않는다. 따라서 브라우저와 테스트 코드를 공유할 수 없고, 대신 Web Platform Tests를 쉽게 포팅할 수 있는 React Native용 테스트 API를 만들었다.

우리는 RNTester를 통해 구현 사항을 검증하기 위해 새로운 수동 테스트 프레임워크를 구현했다. 이 테스트는 RNTester Platform Tests라는 이름으로 불리며, 아직 기본적인 수준이다. 이 구현은 테스트 케이스를 컴포넌트 자체로 구성할 수 있는 API를 제공하며, 테스트 결과는 UI를 통해 보고된다.

왼쪽은 React Native(iOS)에서 실행된 "Pointer Events hoverable pointer attributes test", 오른쪽은 웹(원본 구현)에서 실행된 동일 테스트를 나란히 비교한 GIF

이 테스트는 Pointer Events 구현의 완성도를 높이는 데 계속 유용할 것이다. 또한 Android와 iOS 이외의 플랫폼에서도 Pointer Events 구현을 테스트할 수 있도록 확장될 것이다. 테스트 스위트의 수가 증가함에 따라, 우리는 이 테스트의 실행을 자동화하여 구현에서의 회귀를 더 잘 포착할 수 있도록 할 계획이다.

동작 원리

Pointer Events 구현의 상당 부분은 기존 터치 이벤트 전달 인프라를 기반으로 한다. Android와 iOS에서는 각각 MotionEvent와 UITouch 이벤트를 활용한다. 이벤트 전달의 일반적인 흐름은 아래 다이어그램에 나와 있다.

Android와 iOS UI 입력 이벤트를 Pointer Events로 해석하는 코드 흐름 다이어그램. Android에서는 "onTouchEvent"와 "onHoverEvent" 입력 핸들러가 "MotionEvents"를 발생시키고, 이를 Pointer Events로 해석한 후 JSI를 통해 React 렌더러로 전달한다. iOS도 유사한 경로를 따르며, "touchesBegan", "touchesMoved", "touchesEnded", "hovering" 입력 핸들러가 "UITouch"와 "UIEvent"를 Pointer Events로 해석한다.

Android를 예로 들면, 플랫폼 이벤트를 활용하는 일반적인 접근 방식은 다음과 같다:

  1. MotionEvent의 모든 포인터를 순회하며 각 포인터의 대상 React 뷰와 그 조상 경로를 결정하기 위해 깊이 우선 탐색을 수행한다.
  2. MotionEvent의 카테고리를 관련 Pointer Events로 매핑한다. MotionEventPointerEvent 사이에는 1대다 관계가 있다. 이 관계를 나타낸 다이어그램에서 점선은 포인팅 장치가 호버를 지원하지 않을 때 발생하는 이벤트를 나타낸다.

Android MotionEvents 타입과 발생하는 Pointer Events의 관계를 나타낸 다이어그램. 일부 Pointer Events는 포인팅 장치가 호버를 지원하지 않을 때 조건부로 발생한다. "ACTION_DOWN"과 "ACTION_POINTER_DOWN"은 pointerdown을 발생시키고, 조건에 따라 pointerenter, pointerover를 발생시킨다. "ACTION_MOVE"와 "ACTION_HOVER_MOVE"는 pointerover, pointermove, pointerout, pointerup을 발생시킨다. "ACTION_UP"과 "ACTION_POINTER_UP"은 pointerup을 발생시키고, 조건에 따라 pointerout, pointerleave를 발생시킨다.

  1. MotionEvent의 플랫폼 세부 정보와 이전 상호작용의 캐시된 상태를 사용해 PointerEvent 인터페이스를 구성한다. (예: button 속성)
  2. Android에서 Pointer Events를 React Native의 코어 이벤트 큐로 전달하고, JSI를 활용해 react-native-rendererdispatchEvent 메서드를 호출한다. 이 메서드는 이벤트의 버블링 및 캡처 단계를 위해 React 트리를 순회한다.

구현 진행 상황

현재 Pointer Events 명세 구현 진행 상황을 살펴보면, 가장 일반적인 이벤트인 누르기, 호버링, 이동 등을 처리하는 견고한 기반 구현에 초점을 맞추고 있다.

이벤트

구현 완료구현 중아직 구현되지 않음
onPointerOveronPointerCancelonClick
onPointerEnteronContextMenu
onPointerDownonGotPointerCapture
onPointerMoveonLostPointerCapture
onPointerUponPointerRawUpdate
onPointerOut
onPointerLeave
info

onPointerCancel은 네이티브 플랫폼의 "cancel" 이벤트에 연결되어 있지만, 이는 웹 플랫폼에서 예상하는 시점과 항상 일치하지는 않는다.

이벤트 속성

위에서 언급한 각 이벤트에 대해, React Native는 event.nativeEvent 속성을 통해 PointerEvent 객체에서 기대되는 대부분의 속성을 구현했다. 구현된 모든 속성 목록은 이벤트 객체의 Flowtype 인터페이스 정의에서 확인할 수 있다. 완전히 구현되지 않은 주목할 만한 예외는 relatedTarget 속성이다. 이 속성을 임시 방식으로 네이티브 뷰 참조로 노출하는 것은 간단하지 않다.

향후 작업과 탐구

위에서 언급한 이벤트 외에도 Pointer Events와 관련된 몇 가지 API가 더 있다. 앞으로 이 프로젝트의 일환으로 이러한 API를 구현할 계획이다. 이 API들은 다음과 같다:

  • Pointer Capture API
    • setPointerCapture(), releasePointerCapture(), hasPointerCapture()와 같은 엘리먼트 참조에 노출된 명령형 API를 포함한다.
  • touch-action 스타일 속성
    • 웹에서는 이 CSS 속성을 사용해 브라우저와 웹사이트의 이벤트 처리 코드 간 제스처를 선언적으로 조정한다. React Native에서는 View의 포인터 이벤트 핸들러와 부모 ScrollView 간의 이벤트 처리를 조정하는 데 사용할 수 있다.
  • click, contextmenu, auxclick
    • click은 접근성 패러다임이나 다른 플랫폼 특성 상호작용을 통해 트리거될 수 있는 상호작용의 추상적 정의다.

네이티브 Pointer Events 구현의 또 다른 장점은 현재 터치 이벤트에만 제한되고 Responder, Pressability, PanResponder API에 의해 자바스크립트에서 처리되는 다양한 형태의 제스처 처리를 재검토하고 개선할 수 있다는 점이다.

또한 React Native 호스트 컴포넌트(즉, add/removeEventListener)에 대한 EventTarget 인터페이스 구현을 포함하는 방안을 계속 탐구하고 있다. 이를 통해 포인터 상호작용을 처리하는 더 많은 사용자 정의 추상화가 가능해질 것으로 기대한다.

직접 해보기

포인터 이벤트 구현은 아직 실험 단계지만, 커뮤니티의 피드백을 적극적으로 받고자 한다. 이 API를 직접 사용해보고 싶다면 몇 가지 기능 플래그를 활성화해야 한다:

기능 플래그 활성화

note

포인터 이벤트는 새로운 아키텍처(Fabric)에서만 구현되었으며, 이 글을 작성하는 시점에서 출시 후보인 React Native 0.71+ 버전에서만 사용할 수 있다.

여러분의 자바스크립트 진입 파일(기본 React Native 앱 템플릿의 index.js)에서 포인터 이벤트를 사용하려면 shouldEmitW3CPointerEvents 플래그를 활성화해야 한다. 또한 Pressability에서 포인터 이벤트를 사용하려면 shouldPressibilityUseW3CPointerEventsForHover 플래그도 활성화해야 한다.

import ReactNativeFeatureFlags from 'react-native/Libraries/ReactNative/ReactNativeFeatureFlags';

// W3C 포인터 이벤트 구현의 자바스크립트 측면 활성화
ReactNativeFeatureFlags.shouldEmitW3CPointerEvents = () => true;

// Pressability의 호버 이벤트를 포인터 이벤트 구현으로 지원
ReactNativeFeatureFlags.shouldPressibilityUseW3CPointerEventsForHover =
() => true;

iOS 특화 기능

네이티브 iOS 렌더러에서 포인터 이벤트가 전송되도록 하려면, 네이티브 앱의 초기화 코드(일반적으로 AppDelegate.mm)에서 네이티브 기능 플래그를 활성화해야 한다.

#import <React/RCTConstants.h>

// ...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTSetDispatchW3CPointerEvents(YES);

// ...
}

iOS에서 Pointer Event 구현이 마우스와 터치 포인터를 구분할 수 있도록 하려면, Xcode 프로젝트의 info.plistUIApplicationSupportsIndirectInputEvents를 추가해야 한다.

안드로이드 특화 기능

iOS와 마찬가지로 안드로이드도 앱 초기화 시 기능 플래그를 활성화해야 한다. 일반적으로 루트 React 액티비티나 화면의 onCreate 메서드에서 설정한다.

import com.facebook.react.config.ReactFeatureFlags;

//... 초기화 코드 어딘가에

@Override
public void onCreate() {
ReactFeatureFlags.dispatchPointerEvents = true;
}

자바스크립트

function onPointerOver(event) {
console.log(
'파란 박스 위에서의 오프셋: ',
event.nativeEvent.offsetX,
event.nativeEvent.offsetY,
);
}

// ... 어떤 컴포넌트 내부
<View
onPointerOver={onPointerOver}
style={{height: 100, width: 100, backgroundColor: 'blue'}}
/>;

피드백 환영

현재 Pointer Events는 VR 플랫폼과 Oculus 스토어에서 사용되고 있다. 우리는 지금까지의 접근 방식과 구현 내용에 대한 초기 커뮤니티 피드백을 기대하고 있다. 앞으로의 진행 상황을 여러분과 공유하게 되어 기쁘다. 이 작업에 대한 질문이나 의견이 있다면, Pointer Events에 대한 전용 토론에 참여해 주길 바란다.