QuipperにおけるReact Native活用事例

この記事はReact Native Advent Calendar 2017の5日目の記事です。


こんにちは。モバイルエンジニアの@hotchemiです。

今回はQuipperにおけるReact Native活用事例に関して紹介したいと思います。

目次

  • 導入の背景/効能
    • 開発におけるメリット/デメリット
    • リリースサイクル
  • 技術スタック
  • おわりに

導入の背景/効能

Quipperが開発しているスタディサプリでは合格特訓プランという現役大学生コーチによる学習伴走サービスを提供しており、コーチとユーザーのコミュニケーションをより円滑にする為にメッセージ機能をベースとした業務補助iOSアプリの開発を実施する事になりました。

最初のバージョンこそSwiftで開発を進めていたものの以下の組織的・技術的な課題に直面した為、React Nativeを用いた開発に方針転換し現在では運用が軌道に乗っています。

  • モバイルエンジニアは既存アプリのエンハンスにアサインされており新規プロジェクトに人をアサインできなかった
    • 開発チームにはReactJS経験のあるフロントエンドエンジニアが所属していた為Webエンジニアを巻きこんで開発する体制を作りたかった
    • 社内のフロントエンドをReactJSに統一していこうという動きがあったのも後押しになった
  • Apple Developer Enterprise Programを利用してバイナリを配布している為App Storeの自動アップデート機能が使えずアプリの更新を都度手動で実施するオペレーションが必要になっていた
    • Microsoftが提供しているCodePushというツールを活用する事でユーザーにアプリのアップデートを意識させる事なくエンハンスを回す事ができるのではないかと考えた

当初プロジェクトメンバーにReact Nativeの開発経験があるメンバーはいませんでしたが、JavaScriptが得意なエンジニアとネイティブ開発の基礎知識があるエンジニア(私)の組み合わせであった為お互いの知識を補完する形で開発を進める事ができ、結果として最初のコミットから約一ヶ月でリリースまで辿り着く事ができました。

開発が佳境に入った段階からはWebエンジニアにヘルプに入ってもらい当初目論んでいたチームのスケールアップも果たす事ができました。

開発におけるメリット/デメリット

ネットで見聞きしていたよりハマりどころは少ないように感じましたが、例えば画像を扱う為にfile systemと話す時などはiOS標準のAPIとインタフェースが異なる為若干苦労しました。具体的にはiOSであればUIImagePickerControllerのdelegateでは以下の様にしてUIImageとして画像データを取得する事ができますが、

  func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    switch picker.sourceType {
    case .photoLibrary:
      guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else { return }
    default:
      break
    }
  }

React Nativeには当然UIImageというクラスはないので、保存された画像のuriを使って表示するなりblobオブジェクトを取得するという処理になります(実際の開発にはreact-native-image-pickerreact-native-fetch-blobを利用しています)。

  ImagePicker.launchImageLibrary({}, response => {
    if (response.didCancel) {
      return;
    }
    if (response.error) {
      return;
    }
    console.log(response.uri);
    console.log(response.fileName);
  });

ですがその一方でlive/hot reloadingを利用してリアルタイムでUIを調整できるのは非常に快適であり、開発速度自体はNativeコードを書いている時と同じかそれ以上に上がったと感じています。

また、react/reduxを採用している為component間が疎結合でありユニットテストが書きやすく、node上でテストを実行するだけなので実行も早くテストに対する心理的障壁が下がったという事も私にとっては嬉しいポイントでした。あとCIも早いです。

React Native本体のバージョンアップがかなり大変という話を聞きますが、そちらはまだ実施していません。

リリースサイクル

現在このアプリは週1ペースで先述したCodePushを通してアップデートのリリースをしています。

Web開発をしている方には普通に感じられるかもしれませんが、iOS/Androidアプリを週1でリリースするというのは実はかなりハードな作業でありバグの混入やユーザーへの浸透ペースを考慮すると2~3週に一度リリースするのが普通かと思います。

CodePushのおかげで緊急時のロールバックやユーザーへの浸透ペースを考えなくてよく、さもWeb開発のような感覚でのリリースを実現できるようになったのは非常に大きな収穫だったと感じています。現在はE2Eテストを拡充し週2, 3とリリースペースを増やしていこうと目論んでいます。

その辺の詳しい所は明日@yuya-takeyamaが別途書いてくれる事になっていますので乞うご期待下さい。

ディレクトリ構成

大体以下の様なディレクトリ構成を採用しています。

feature毎にディレクトリを切ってその下にAction, Reducer, ActionTypeを定義したファイルを置き、更にその下にscreensを作りReact Componentを配置しています。Componentは基本的にPresentationalとContainerの責務で分離しています。

その他共通化できるものに関しては適切になる様にroot下にディレクトリを切っています。

.
├── auth
│   ├── __tests__
│   │   └── authReducerTest.ts
│   ├── authAction.ts
│   ├── authReducer.ts
│   ├── authType.ts
│   └── screens
│       ├── __tests__
│       │   ├── __snapshots__
│       │   │   └── loginScreenTest.tsx.snap
│       │   └── loginScreenTest.tsx
│       ├── loginContainer.tsx
│       └── loginScreen.tsx
├── components
│   ├── alertView.tsx
│   └── loadingIndicator.tsx
├── constants
│   ├── colorPalette.ts
│   └── fontStyle.ts
├── locale
│   ├── index.ts
│   └── languages
│       ├── en.ts
│       ├── index.ts
│       └── ja.ts
├── root.ts
├── rootReducer.ts
├── rootStore.ts
└── utils
    ├── pushNotification.ts
    └── navigationHelper.ts

技術スタック

現在開発で利用しているpackage.jsonの一部を抜粋しておくと、以下の様な感じです。

  "dependencies": {
    "lodash": "^4.14.80",
    "moment": "^2.19.1",
    "native-base": "^2.3.2",
    "react": "^16.0.0",
    "react-native": "0.49.0",
    "react-native-code-push": "^5.1.3-beta",
    "react-native-config": "^0.9.0",
    "react-native-fcm": "^10.0.3",
    "react-native-fetch-blob": "^0.10.8",
    "react-native-gifted-chat": "^0.2.9",
    "react-native-i18n": "^2.0.8",
    "react-native-image-picker": "^0.26.7",
    "react-native-navigation": "^1.1.236",
    "react-native-sentry": "^0.30.2",
    "react-native-tableview-simple": "^0.17.1",
    "react-redux": "^5.0.6",
    "redux": "3.7.2",
    "redux-form": "7.1.1",
    "redux-mock-store": "^1.3.0",
    "redux-persist": "4.8.3",
    "redux-thunk": "2.2.0",
    "validate.js": "^0.12.0",
    "whatwg-fetch": "^2.0.3"
  },
  "devDependencies": {
    "@types/jest": "^21.1.1",
    "@types/lodash": "^4.14.80",
    "@types/react": "^16.0.7",
    "@types/react-native": "^0.48.9",
    "@types/react-redux": "^5.0.12",
    "appcenter-cli": "^1.0.4",
    "babel-jest": "20.0.3",
    "babel-preset-react-native": "3.0.1",
    "babel-preset-react-native-stage-0": "^1.0.1",
    "concurrently": "^3.5.0",
    "github-api": "^3.0.0",
    "husky": "^0.14.3",
    "jest": "21.2.1",
    "jest-context": "^2.1.0",
    "lint-staged": "^4.3.0",
    "prettier": "1.7.4",
    "react-native-debugger-open": "^0.3.15",
    "react-test-renderer": "16.0.0-alpha.12",
    "redux-devtools-extension": "^2.13.2",
    "rimraf": "^2.6.2",
    "ts-jest": "^21.0.1",
    "tslint": "^5.8.0",
    "tslint-config-prettier": "^1.6.0",
    "typescript": "^2.6.1"
  },

TypeScript

複雑化するフロントエンドアプリケーションを開発するにあたって静的型付けの言語は個人的には必須といっても良いと考えています。

TypeScriptFlowを検討しましたが、新規開発であった事と型定義ファイルの総数、コミュニティやドキュメントの充実度を考慮して今回はTypeScriptを選定しました。

IDEとしてはVSCodeを使っていますが現在に至るまで特に問題も発生せずとても快適な開発を実現できています。

Lintにはtslint、formatterにはprettierを利用しています。

Redux

当初、ReduxMobXを検討しましたが実装の薄さと市場への浸透度を考慮してreduxを選定しました。redux-devtools-extension等のデバッグツールやMiddlewareの充実度の恩恵を預かっておりこちらも特には困っていません(Middlewareはredux-thunkやredux-persist等基本的なものを利用しています)。

また、TypeScriptと併せて用いる事でActionやReducerを型安全に扱う事ができ、これらの組み合わせによって安心して開発が進められていると感じています。機会があれば別の記事でご紹介できればと思います。

Navigation

Navigation周りはWebエンジニアが特につまづきやすい所の一つだと思います。当然WebブラウザではないのでBrowser History APIはなく、殆どの場合代わりに3rd partyのライブラリを使う事になります。

コミュニティ的にはreact navigationが最も有名かと思いますが、個人的にはJSレイヤーで画面stackを管理している事と遷移アニメーションにどうしても違和感があり、今回はreact-native-navigationというUIViewCotroller/Activityベースの画面遷移を実現できるライブラリを選定しました。若干APIの癖が強いのですが、いわゆるネイティブの画面遷移をそのまま実現できるのでこだわりたい方にはオススメできるライブラリだと思います。

一例を紹介すると、UIViewControllerのpushはprops.navigator.push、UINavigationViewControllerのpresentはprops.navigator.showModalに対応しているので簡単にiOSの画面遷移を構築する事ができます。

this.props.navigator.push({
  screen: 'example.ScreenThree', // unique ID registered with Navigation.registerScreen
  title: undefined, // navigation bar title of the pushed screen (optional)
  titleImage: require('../../img/my_image.png'), // iOS only. navigation bar title image instead of the title text of the pushed screen (optional)
});

this.props.navigator.showModal({
  screen: "example.ModalScreen", // unique ID registered with Navigation.registerScreen
  title: "Modal", // title of the screen as appears in the nav bar (optional)
  passProps: {}, // simple serializable object that will pass as props to the modal (optional)
  navigatorStyle: {}, // override the navigator style for the screen, see "Styling the navigator" below (optional)
  animationType: 'slide-up' // 'none' / 'slide-up' , appear animation for the modal (optional, default 'slide-up')
});

最近のアップデートで3D touch APIにも対応したようです。気になる方は公式docを参照してみてください。

UI

native-baseの各コンポーネントに社内のデザインガイドラインを適用して使っています。サッとそれなりのものを作れるのはいいのですが、カスタマイズ製に難があったりするので将来的には自前でコンポーネント群を持った方が良いかもしれないと考えています。

競合としてはreact-native-elementsreact-native-material-uiがありますが好きなものを使うで良さそうです。

Test

メインはオールインワンのJestを利用しています。特にcomponentのsnapshot機能が気に入っており簡易リグレッションテストとして用いています。

E2Eテストに関しては未導入ですがAppiumdetoxを現在比較検討している段階です。

Misc

余談ですが、CodePushではJSレイヤーの変更しか適用ができない為、Nativeコードが含まれているライブラリを導入する場合はバイナリ自体のアップデートが必須になりますので、CodePushでの運用を真剣に考えている方がいましたら気に留めるべき事の一つかなと思います。

おわりに

正直な所、当初このプロジェクトが成功するかは半信半疑だったのですが、結果として想定以上の成果を出す事ができた為現在本体アプリへの導入の検討、実証を進めています。

個人的に、教育や医療などの公共機関で使われるユースケースが想定されるアプリでは、品質やアップデートの浸透度が他業種のアプリに比べて格段に重要になってくる為、React Nativeで実現できるアプローチが非常にsuitableであると感じています。

QuipperというとこれまでWeb/Railsの会社というイメージが強かったかと思いますが、今後はReact Nativeに関してもOSSやmeetupの場で皆様と知見を交換していく事ができればと考えています。

現場からは以上です。