최신 앱에서 사용할 수 있는 수많은 네이티브 UI 위젯들이 있습니다. 그 중 일부는 플랫폼의 일부이며, 다른 일부는 서드 파티 라이브러리로 사용이 가능하지만, 많은 위젯을 사용자의 자체 포트폴리오에서 사용할 수 있습니다. React Native는 ScrollView, TextInput 과 같이 이미 래핑된, 가장 핵심적인 플랫폼 컴포넌트을 여러 개 가지고 있지만, 이전 앱에서 사용자가 직접 작성한 것은 분명 아닙니다. 다행히, 이러한 기존 컴포넌트들을 래핑하여 React Native 애플리케이션과 원활하게 통합할 수 있습니다.
네이티브 모듈 가이드에서와 마찬가지로, 이 가이드도 Android SDK 프로그래밍에 다소 익숙한 사용자를 대상으로 하는 고급 가이드입니다. 이 가이드는 네이티브 UI 컴포넌트를 빌드하는 방법, 코어 React Native 라이브러리에서 사용 가능한 기존 ImageView 컴포넌트의 하위 집합 구현에 대해 안내합니다.
이 예시에서는 JavaScript에서 ImageViews를 사용하기 위한 구현 요구 사항들을 살펴보겠습니다.
네이티브 뷰는 ViewManager 또는 좀 더 일반적인 SimpleViewManager 를 확장하여 생성 및 조작합니다. 이 경우 배경 색상(background color), 불투명도(opacity), Flexbox 레이아웃과 같은 공통 속성을 적용하기 때문에 SimpleViewManager 를 사용하는 것이 편리합니다.
이러한 하위 클래스는 기본적으로 싱글톤이며, 브리지에 의해 각각 하나의 인스턴스만 생성됩니다. 이들은 필요에 따라 프로퍼티를 설정 및 업데이트하도록 다시 위임하는 NativeViewHierarchyManager에 네이티브 뷰를 보냅니다. ViewManagers 또한 대표적인 뷰의 대리자이기도 하며, 브리지를 통해 JavaScript에 이벤트를 다시 보냅니다.
뷰를 전송하려면,
- ViewManager 하위 클래스를 생성합니다.
createViewInstance메서드를 구현합니다.@ReactProp(또는@ReactPropGroup) 어노테이션을 사용해 뷰 프로퍼티 설정자를 노출시킵니다.- 애플리케이션 패키지의
createViewManagers에 뷰 매니저를 등록합니다. - JavaScript 모듈을 구현합니다.
이 예제에서는 ReactImageView 타입의 SimpleViewManager 를 확장하는 뷰 매니저 클래스 ReactImageManager 를 생성합니다. ReactImageView 관리자에 의해 관리되는 객체 타입이며, 이는 사용자 정의 네이티브 뷰가 됩니다. getName 에 의해 반환된 이름은 JavaScript에서 네이티브 뷰 타입을 참조하는 데 사용됩니다.
...
public class ReactImageManager extends SimpleViewManager<ReactImageView> {
public static final String REACT_CLASS = "RCTImageView";
ReactApplicationContext mCallerContext;
public ReactImageManager(ReactApplicationContext reactContext) {
mCallerContext = reactContext;
}
@Override
public String getName() {
return REACT_CLASS;
}뷰는 createViewInstance 메소드 안에서 생성됩니다. 생성된 뷰는 기본 state로 스스로 초기화되어야 하며, 모든 속성은 updateView 후속 호출을 통해 설정됩니다.
@Override
public ReactImageView createViewInstance(ThemedReactContext context) {
return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, mCallerContext);
}JavaScript에 반영될 속성은 @ReactProp (또는 @ReactPropGroup) 어노테이션이 달린 setter 메소드로 노출되어야 합니다. Setter 메소드는 뷰를 첫 번째 인자로 받아서 현재 뷰 타입을 업데이트하고, 속성 값을 두 번째 인자로 받습니다. Setter는 void 메소드로 선언되어야 하고, public 이어야 합니다. JS로 보내지는 속성 타입은 setter의 인자 값 타입에 따라 자동으로 결정됩니다. 현재 지원되는 타입은 boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap 입니다.
@ReactProp 어노테이션은 String 타입의 name 인자 하나를 필수로 가집니다. Setter 메소드에 연결되는 @ReactProp 어노테이션에 할당된 이름은 JS 측에서 속성을 참조하는 데에 사용됩니다.
name, @ReactProp 어노테이션은 defaultBoolean, defaultInt, defaultFloat 인자를 옵션으로 받을 수 있습니다. 이 인자들의 타입은 각각 boolean, int, float 이어야 하며, setter가 참조하는 속성이 컴포넌트에서 제거된 경우, 이 인자들을 통해 제공된 값이 setter 메소드에 전달됩니다. 기본값은 원시 타입에 대해서만 제공되며, setter가 복합 타입(complex type)인 경우, 해당하는 속성이 제거되었을 때 기본값으로 null이 제공됩니다.
@ReactPropGroup 어노테이션이 달린 메소드의 Setter 선언 조건은 @ReactProp 의 경우와 다릅니다. 자세한 내용은 @ReactPropGroup 어노테이션 클래스 문서를 참조하십시오. 중요! ReactJS에서 속성 값을 업데이트하면 setter 메소드가 호출됩니다. 컴포넌트를 업데이트할 수 있는 방법 중 하나는, 이전에 설정된 속성을 제거하는 것입니다. 이 경우 setter 메소드도 호출되어 뷰 매니저에게 속성이 변경되었음을 알리게 되며, 기본값이 제공됩니다. (원시 타입의 경우 defaultBoolean, defaultFloat 등을 사용해 기본값이 지정되며, 복합 타입의 경우 null 로 설정된 값과 함께 setter 메소드가 호출됩니다. )
@ReactProp(name = "src")
public void setSrc(ReactImageView view, @Nullable ReadableArray sources) {
view.setSource(sources);
}
@ReactProp(name = "borderRadius", defaultFloat = 0f)
public void setBorderRadius(ReactImageView view, float borderRadius) {
view.setBorderRadius(borderRadius);
}
@ReactProp(name = ViewProps.RESIZE_MODE)
public void setResizeMode(ReactImageView view, @Nullable String resizeMode) {
view.setScaleType(ImageResizeMode.toScaleType(resizeMode));
}Java에서의 마지막 단계는, 애플리케이션에 ViewManager를 등록하는 것입니다. 이는 애플리케이션 패키지 멤버 함수인 createViewManagers 를 통해서 네이티브 모듈과 비슷한 방식으로 이루어집니다.
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ReactImageManager(reactContext)
);
}마지막으로, 새로운 뷰의 사용자를 위한 Java와 JavaScript 간의 인터페이스 계층을 정의하는 JavaScript 모듈을 생성합니다. 이 모듈의 컴포넌트 인터페이스를 문서화하는 것이 좋습니다. (예: Flow, TypeScript, 일반 주석 사용)
// ImageView.js
import { requireNativeComponent } from 'react-native';
/**
* Composes `View`.
*
* - src: string
* - borderRadius: number
* - resizeMode: 'cover' | 'contain' | 'stretch'
*/
module.exports = requireNativeComponent('RCTImageView');requireNativeComponent 함수는 네이티브 뷰의 이름을 사용합니다. 컴포넌트가 더 정교한 작업 (예: 사용자 정의 이벤트 처리)을 해야 하는 경우, 네이티브 컴포넌트를 다른 React 컴포넌트로 감싸야 합니다. 이는 아래의 MyCustomView 예제에서 확인할 수 있습니다.
이제 JS에서 자유롭게 사용할 수 있는 네이티브 뷰 컴포넌트를 expose하는 방법을 알게 되었습니다. 그런데 pinch-zoom이나 panning과 같이 사용자로부터 발생하는 이벤트는 어떻게 처리해야 할까요? 네이티브 이벤트가 발생하면, 네이티브 코드는 해당 뷰를 나타내는 JavaScript 코드로 이벤트를 발행해야 하며, 두 개의 뷰가 getId() 메소드에서 반환받은 값으로 연결되어야 합니다.
class MyCustomView extends View {
...
public void onReceiveNativeEvent() {
WritableMap event = Arguments.createMap();
event.putString("message", "MyMessage");
ReactContext reactContext = (ReactContext)getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
getId(),
"topChange",
event);
}
}JavaScript에서 topChange 이벤트 이름을 onChange 콜백 prop에 매핑하려면, ViewManager 에서 getExportedCustomBubblingEventTypeConstants 메소드를 재정의하여 등록하십시오.
public class ReactImageManager extends SimpleViewManager<MyCustomView> {
...
public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.builder()
.put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange")))
.build();
}
}이 콜백은 원시 이벤트(더 간단한 API를 만들기 위해 일반적으로 wrapper 컴포넌트 안에서 처리되는)와 함께 호출됩니다.
// MyCustomView.js
class MyCustomView extends React.Component {
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
}
_onChange(event: Event) {
if (!this.props.onChangeMessage) {
return;
}
this.props.onChangeMessage(event.nativeEvent.message);
}
render() {
return <RCTMyCustomView {...this.props} onChange={this._onChange} />;
}
}
MyCustomView.propTypes = {
/**
* Callback that is called continuously when the user is dragging the map.
*/
onChangeMessage: PropTypes.func,
...
};
var RCTMyCustomView = requireNativeComponent(`RCTMyCustomView`);