Skip to content

Latest commit

 

History

History
184 lines (139 loc) · 10.1 KB

File metadata and controls

184 lines (139 loc) · 10.1 KB

Android 네이티브 UI 컴포넌트

최신 앱에서 사용할 수 있는 수많은 네이티브 UI 위젯들이 있습니다. 그 중 일부는 플랫폼의 일부이며, 다른 일부는 서드 파티 라이브러리로 사용이 가능하지만, 많은 위젯을 사용자의 자체 포트폴리오에서 사용할 수 있습니다. React Native는 ScrollView, TextInput 과 같이 이미 래핑된, 가장 핵심적인 플랫폼 컴포넌트을 여러 개 가지고 있지만, 이전 앱에서 사용자가 직접 작성한 것은 분명 아닙니다. 다행히, 이러한 기존 컴포넌트들을 래핑하여 React Native 애플리케이션과 원활하게 통합할 수 있습니다.

네이티브 모듈 가이드에서와 마찬가지로, 이 가이드도 Android SDK 프로그래밍에 다소 익숙한 사용자를 대상으로 하는 고급 가이드입니다. 이 가이드는 네이티브 UI 컴포넌트를 빌드하는 방법, 코어 React Native 라이브러리에서 사용 가능한 기존 ImageView 컴포넌트의 하위 집합 구현에 대해 안내합니다.

ImageView 예시

이 예시에서는 JavaScript에서 ImageViews를 사용하기 위한 구현 요구 사항들을 살펴보겠습니다.

네이티브 뷰는 ViewManager 또는 좀 더 일반적인 SimpleViewManager 를 확장하여 생성 및 조작합니다. 이 경우 배경 색상(background color), 불투명도(opacity), Flexbox 레이아웃과 같은 공통 속성을 적용하기 때문에 SimpleViewManager 를 사용하는 것이 편리합니다.

이러한 하위 클래스는 기본적으로 싱글톤이며, 브리지에 의해 각각 하나의 인스턴스만 생성됩니다. 이들은 필요에 따라 프로퍼티를 설정 및 업데이트하도록 다시 위임하는 NativeViewHierarchyManager에 네이티브 뷰를 보냅니다. ViewManagers 또한 대표적인 뷰의 대리자이기도 하며, 브리지를 통해 JavaScript에 이벤트를 다시 보냅니다.

뷰를 전송하려면,

  1. ViewManager 하위 클래스를 생성합니다.
  2. createViewInstance 메서드를 구현합니다.
  3. @ReactProp (또는 @ReactPropGroup) 어노테이션을 사용해 뷰 프로퍼티 설정자를 노출시킵니다.
  4. 애플리케이션 패키지의 createViewManagers에 뷰 매니저를 등록합니다.
  5. JavaScript 모듈을 구현합니다.

1. ViewManager 하위 클래스 생성하기

이 예제에서는 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;
  }

2. createViewInstance 메소드 구현하기

뷰는 createViewInstance 메소드 안에서 생성됩니다. 생성된 뷰는 기본 state로 스스로 초기화되어야 하며, 모든 속성은 updateView 후속 호출을 통해 설정됩니다.

  @Override
  public ReactImageView createViewInstance(ThemedReactContext context) {
    return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, mCallerContext);
  }

3. @ReactProp (또는 @ReactPropGroup) 사용해 뷰 프로퍼티 설정자(Setter) Expose하기

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));
  }

4. ViewManager 등록하기

Java에서의 마지막 단계는, 애플리케이션에 ViewManager를 등록하는 것입니다. 이는 애플리케이션 패키지 멤버 함수인 createViewManagers 를 통해서 네이티브 모듈과 비슷한 방식으로 이루어집니다.

  @Override
  public List<ViewManager> createViewManagers(
                            ReactApplicationContext reactContext) {
    return Arrays.<ViewManager>asList(
      new ReactImageManager(reactContext)
    );
  }

5. JavaScript 모듈 구현하기

마지막으로, 새로운 뷰의 사용자를 위한 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`);