RELATIONS Developers Blog

RELATIONS株式会社の開発ブログです。

Webエンジニアがスマホアプリをリリースするまでに学んだ32のコト(後編)

こんにちは。RELATIONS株式会社の久原です。

最近はAtomicDesignを生かしたデザインシステムの構築を試しています。こちらはまた別の記事で改めてご紹介できればと思っています。できればアプリとWebの共通システムにしてみたい(難しそう)。

さて、前回の記事では、Webのフロントエンドエンジニアである私が、「自分のフロントエンドスキルセットを活かしつつ、最速でスマホアプリをリリースするためには、どうすればよいか?」を試行錯誤した結果について、その前半部分をご紹介しました。

今回の記事は、残りの後半部分になります。

カテゴリ 学習したところ
開発環境 React Native・Expo
ナビゲーション React Navigation・スタックベースナビゲーション
ネイティブUI NativeBase・レイアウト手法
メディア表現とプリロード ロード方法・CSSとの差の埋め方
ネイティブ機能 プッシュ通知・アイコン・スプラッシュ
デバッグ storybook・react-native-debugger
ビルドとリリース expo-cli・expo client
フロントエンド以外の技術 Firebase

後編では、「ネイティブ機能」「デバッグ」「ビルドとリリース」について、加えて「フロントエンド以外の技術」について学習したところについて、ご紹介したいと思います。

各種ネイティブ機能の利用と設定 (5)

Webエンジニアには利用が難しいネイティブ機能、その中でもプッシュ通知はアプリ開発におけるコア機能と言えます。こういったネイティブ絡みの機能の活用は、React Native(以下、RN)でアプリを作るための大きなモチベーションですね。

そんなプッシュ通知機能ですが、Expoはライブラリを提供するだけでなく、なんとプッシュ通知用のサーバまで提供してくれます!ですのでExpoの領域内で、プッシュ通知の生成から配信までを一貫して構築可能になっています。素晴らしいですね!

exports.sendNotification = functions.firestore
  .document('users/{userId}/messages/{messageId}')
  .onCreate(async () => {
    const { params: { userId, messageId } = {} } = context || {};

    const userRef = db.collection('users').doc(userId);
    const userSnapshot = await userRef.get().catch(() =>({}));
    const user = userSnapshot.data();
    const { deviceToken } = user || {};

    const message = {
      to: deviceToken,
      sound: 'default',
      body: '新しいメッセージを受信しました!',
      data: { type: 'messageCreated', userId, messageId },
    };

    const chunks = expo.chunkPushNotifications([message]);
    const tickets = [];

    chunks.forEach(async chunk => {
      const ticketChunk = await expo
        .sendPushNotificationsAsync(chunk)
        .catch(error => console.error(error));
      tickets.push(...ticketChunk);
    });

    return tickets;
  });

詳細な例が、弊社大川の記事に載っていますので、ぜひご覧ください!

アプリのアイコン(AppIcon)やスプラッシュスクリーンなどの設定も、Expoで完結できます。app.jsonというファイルに設定を記述することによって、環境の差を自動で埋めてくれますので、開発者としてはこれらの画像の用意と、簡単な記述を行うだけでOKです。

{
  "expo": {
    "orientation": "portrait",
    "primaryColor": "#665d8c",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "cover",
      "backgroundColor": "#9a96dd"
    },
    ...
  }
}

その他のネイティブ機能やネイティブ設定などについても、その多くがExpoのライブラリによってサポートされます。実際にこれらの実装においてXcodeやAndroid Studioを開く必要はなく、すべてJSで完結することができました。本当にありがたい話です。

デバッグ (4)

Webエンジニアとしてはお馴染みのStorybookで、Webと同様のUIカタログが作れます。表示はシミュレータ上で、操作はWebブラウザから、それぞれ行うことができます。

import React from 'react';
import { storiesOf } from '@storybook/react-native';
import styled from 'styled-components/native';

import TextButton from '..';
import CenterView from '../../CenterView';

storiesOf('atoms/TextButton', module)
  .addDecorator(getStory => <CenterView>{getStory()}</CenterView>)
  .add('with text', () => <TextButton>Hello Button!</TextButton>)
  .add('variant=primary', () => <TextButton variant="primary">Hello Button!</TextButton>)
  .add('variant=primary + roundMd', () => (
    <TextButton variant="primary roundMd">Hello Button!</TextButton>
  ))
</QuizButton>);

f:id:mkubara:20190327105716p:plain
Storybook for React Native

注意点として、執筆時点ではExpoとStorybookの食い合わせがやや悪いため、storyの更新にreact-native-storybook-loaderが必要になりました。近日v5で解決されるかもしれません。

余談ですが、v4からスタンドアロンアプリ形式でstorybook配布が可能になりました。ビルドパイプラインを分けて、storybook用の配信チャネルを別途設けておくと、デザインチェックが捗る気がします。ぜひ試してみたいですね。

デバッグは、WebエンジニアはChromeの検証タブを頻用していると思います。react-native-debuggerをインストールすると、この検証タブをRNアプリに接続できるようになりますのでオススメです。UIのインスペクタ表示もReduxのトレースも可能です。

f:id:mkubara:20190327105840p:plain
react-native-debugger

もうひとつ、RN組み込みのデバッグメニューもあり、こちらはシミュレータ上からCmd+Dで表示できます。こちらの固有機能としては、ライブリロードのON/OFFなどがあります。(Cmd+Rで手動更新できるので、ロジック調整時などに使用しています)。

このように、デバッグについてはWeb開発とほぼ同様の手法で可能です。心強いですね!

ビルドとリリース (4)

  • スタンドアロンビルド: expo build
  • テスター向け配信: TestFlight/Expo Client
  • パブリッシュ/コードアップデート: expo publish --channel
  • アプリリリース

Webの場合と大きく異なる部分です。ですが多くのフローをExpoがサポートしてくれます。

スタンドアロンのアプリをビルドする操作は、expo-cliを使ってbuildコマンドを叩くだけです。オプション無指定であれば、iOS/Androidそれぞれのバイナリを一括生成してくれます。

$ expo publish

配布については、各プラットフォームの流儀に則って進めていく必要があります。

テスター向けにアプリを配信する場合、Webであれば自前テストサーバにデプロイすればOKですが、スマホアプリの場合は審査が入ります。

iOSの場合はTestFlightを使い、審査を受けてからテスターへ公開します。Androidの場合は公開用のExpoアカウントを作成し、ExpoClient経由でテスターへ公開する形が最短です(審査不要)が、よりしっかりとやる場合は、GooglePlayのテスト配信機能を使うことになるでしょう。

本番リリースにおいても、審査を通して、公開するというプロセスが必要になります。Webのリリースと比べて、どうしても面倒に感じてしまうところですね…。

他方、コードのアップデートについては、JSバンドルの再配布のみで気軽に行なえます。CLIからpublishを行えば、Expoサーバ経由で更新が自動配信され、設定した更新ポリシーに基づいて自動更新できます(自動更新・ダイアログで確認して更新など)。

デバッグ用/テスト用/公開用など、チャネルを分けてパブリッシュも可能です。テストビルドやstorybookビルドなどの配布は、個別にチャネルを分けて配信することができます。

$ expo publish --release-channel stg

以上のように、アプリの公開に関してはExpoの力を借りることはできないのですが、アプリのビルドやアップデートに関しては、Expoの力で簡単に行うことができるようになっています。

フロントエンド以外の技術 (4)

f:id:mkubara:20190327110149p:plain
https://firebase.google.com/?hl=ja より引用)

いわゆるバックエンド側はFirebaseで固めました。mBaaSを使うことで、バックエンド部分を専門のエンジニアに実装を依頼する形ではなく、自分自身で、データストア(Firestore)・バッチ処理(Cloud Functions)・認証(Authentication)まで構築することに成功しました。しかもこれらの実装記述はJSだけで可能です。昨今JSがどんどん汎用性を持っていくことに驚きと楽しさを感じます。

データベースにはCloud Firestoreを使用しています。NoSQL系であり、情報を正規化せずに保存することが多いため、慣れが必要かもしれません。しかしながら上手くデータモデリングすると、「ReadキャッシュがJSONで保存されている」ような環境が実現できます。

CRUDは専用のAPIを通して行います。単にRESTのような使い方もできますが、リアルタイムデータベースですので、データ更新のリッスンができ、更新内容のプッシュを受けることができます。リッスンはエンティティだけではなくクエリで行うことも可能で、たとえばクエリで画面全体のデータを表現できていれば、結果的にオートリフレッシュのような機能を高速に実装することも可能になります。

db.collection("cities").where("state", "==", "CA")
  .onSnapshot(function(querySnapshot) {
    var cities = [];
    querySnapshot.forEach(function(doc) {
      cities.push(doc.data().name);
    });
    console.log("Current cities in CA: ", cities.join(", "));
  });

プッシュ通知のバックエンドは、Cloud Functionsを利用します。Firestoreの更新トリガと連携させることができ、そこからExpoサーバと接続してプッシュする形式です。下記が簡易的な例になります。詳細は弊社大川の記事をぜひご覧ください!

exports.sendNotification = functions.firestore
  .document('users/{userId}/messages/{messageId}')
  .onCreate(async () => {
    const { params: { userId, messageId } = {} } = context || {};

    const userRef = db.collection('users').doc(userId);
    const userSnapshot = await userRef.get().catch(() =>({}));
    const user = userSnapshot.data();
    const { deviceToken } = user || {};

    const message = {
      to: deviceToken,
      sound: 'default',
      body: '新しいメッセージを受信しました!',
      data: { type: 'messageCreated', userId, messageId },
    };

    const chunks = expo.chunkPushNotifications([message]);
    const tickets = [];

    chunks.forEach(async chunk => {
      const ticketChunk = await expo
        .sendPushNotificationsAsync(chunk)
        .catch(error => console.error(error));
      tickets.push(...ticketChunk);
    });

    return tickets;
  });

アプリ用の管理画面(Web)も作成したのですが、CRUDのロジックはアプリと全く同じ知識が使用できたため、アプリと同じようにFirestoreを叩くだけで実装できました。このためかなりの処理が共通化できています。

管理画面のデプロイについては、Hostingが提供するCLIのコマンド一つで完了できます。実装以外にかかるはずだった手間がほぼゼロになり、そのぶん開発に集中できる環境を得ることができました。非常に大きなメリットです。

$ firebase deploy

このようにFirebaseには、フロントと同じJS記述でバックエンド開発が可能になる環境が整っていますので、フロントエンドエンジニアとしてはガンガン採用したいサービスです。ちなみにほかの選択肢としてAWS Amplifyなどもあります。こちらも試してみたいですね。

結果

開発の結果ですが、社内のドッグフーディング版をリリースするまでに、素振り期間を加えても2ヶ月程度で完成させることができました。

チーム内でアプリの開発実績がない中、このスピード感でリリースすることができたことについて、プロダクトオーナー側からは、品質・速度感ともに高い評価を受けました!

まとめ

Webエンジニアがスマホアプリをリリースするまでに学んだことについて、前編・後編として共有させていただきました。

全編を通してまず感じたのは、JSスキルのカバレッジの高さです。ブラウザの世界を超え、バックエンドからアプリ開発まで、非常に潰しの効くスキルになってきたなと感じます。

もうひとつは、Web開発スキルのポータビリティが向上したことです。RNによって環境間におけるスキルセットの垣根が低くなったことは、とても喜ばしいことだと感じています。

これらによりエンジニアは、適応すべきコンテキストを減らせることになりますから、より本質的価値の開発に素早く注力でき、ビジネス側の期待にも応えやすくなることでしょう。

もちろんRN万能!というわけではないと思います。速さを求めるアプリや、個別のカスタマイズが多いアプリ、デバイスのエッジな機能を使うようなアプリなどでは、RNよりもネイティブで開発したほうが良い場面も出てくると思います。

要は使い所の問題なだけで、今回のように仮説検証のような速度感を求められるフェーズにおいては、最適解のひとつだなと感じました。

個人的には、スマホアプリの開発ができるようになったことで、Webエンジニアの域を超えて、表現の幅を大きく増やせたことに満足感を得ています。自分の提供可能なスキルセットに「スマホアプリ」を加えられたことは、より多くの環境で自分がコミットできることになるため、さらに多くの面白い案件に関われることになりそうで楽しみです。

上記のように、弊社ではWebアプリ、スマホアプリ、ほか様々な環境でチャレンジできる案件が揃いつつあります。また今回のように技術選択もエンジニアの裁量で行うことがほとんどです。そんな現場にご興味を持っていただけたなら、ぜひこちらからお気軽にオフィス訪問などお申込みください!

www.wantedly.com