RELATIONS Developers Blog

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

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

こんにちは。RELATIONS株式会社の久原です。
2019年も開発ブログをどうぞよろしくお願いいたします!

Webエンジニアだけどスマホアプリを作ることになった件

弊社では「ええ会社をつくる」というミッション実現のために、様々な新規事業の仮説検証を行っています。そしてこの度、とある検証のためのスマホアプリを開発することとなり、私が開発を担当をすることになりました。

私はWebのフロントエンドエンジニアではありますが、スマホアプリの開発経験はほぼゼロの人です。そこで今回は「自分のフロントエンドスキルセットを活かしつつ、最速でスマホアプリをリリースするためには、どうすればよいか?」を色々と試行錯誤した結果を綴ってみたいと思います。

PWA(Progressive Web Apps)じゃダメなの?

Web技術でスマホ対応と聞いて、Webエンジニアとして真っ先に思いつくのは、「PWAで実装できるんじゃない?」という話なのですが、今回の要件として「iOSでのリリース」「プッシュ通知が仮説検証のためのコア機能」という2点があり、アプリとしての実装が必須でした。

近い将来、プッシュ通知などを含めたPWAが実現できる環境が(特にiOS側に!)整っていると良いなぁ…と思っています。

結論:React NativeとExpoを採用

結果だけ先にお伝えしますと、React NativeとExpoを採用しました。これによって必要な知識の多くをWeb技術でまかなえたことから、素振り0.5ヶ月、フロント1ヶ月、バックエンドを含めても合計2ヶ月ほどで、ドッグフーディング版をリリースできました。

昨今、React NativeとExpoを使って、開発がとても早くできたよ!という事例も、数多く見かけるようになってきたと思います。今回はReact NativeとExpoを採用した場合、Webエンジニアとしては、どのあたりの知識が差分になり、どこを学習すれば良いか、という勘所を中心にお伝えできればと思います。

React NativeとExpo

選定基準

「最速で仮説検証するために、最小のコストでリリースできる」ことを技術の選定基準としました。私はWebのエンジニアなので、特に「可能な限りネイティブの知識を学ばずに済む」ようにすることがコストの削減につながると考えました。

ネイティブの知識とは、SwiftやKotlin、StoryboardやXML、XcodeやAndroid Studioなど。ここをWebの知識(HTML/CSS/JS)だけで済ませたい。

そこでWeb技術ベースで開発を行える環境をあたることになるのですが、著名な例としてはReact NativeやApache Cordovaなどの環境が挙げられるかと思います。いずれも長所がありますが、社内でReactを使っていることや、Expoという素晴らしいツールチェインの存在を知ったこともあり、React NativeとExpoを使用することを選択しました。

React Native・Expoの特徴と勘所

f:id:mkubara:20190204162952p:plain
React NativeとExpo

React Native(以下、RN)はFacebook製のフレームワークで、JavaScript(以下、JS)記述のみでiOSやAndroid向けのスマホアプリを開発することができるものです。ベースはReactであり、その主要知識であるJSX・state/props・ライフサイクルメソッド・イベントハンドリングなどは、RNでもReactと同じ知識で実装が可能です。

Webエンジニアとしては、Reactに関する知識があれば、ただ「ビルドターゲットをスマホに向けるだけ」という感覚で開発が可能になります。コードはすべてJS記述になるので、ロジック部分はそのまま流用でき、多くのコードベースを活かすことができます。

ExpoはRNの開発環境です。CLIとライブラリ群を提供しています。CLIとしてのExpoは、スケルトン生成から、ビルド、ライブリロード、デプロイ、実機テスト、リリースまでの開発のすべてのフローをサポートしてくれる、強力なツールチェインになっています。

Webエンジニアとしては、create-react-appのnative版+リリースツールだと認識いただければ、ビルドやリリースなどの部分をwebpackやcreate-react-appに任せて、自身はアプリの機能開発に集中できることが想像できると思います。

加えてライブラリとしてのExpoは、ネイティブUI、プッシュ通知などのラッパーを、JSのインターフェースにて提供しています。つまりこれひとつで多くのネイティブ機能がJS記述だけで追加できるという、非常に便利なライブラリになっています。

まとめると、RNとExpoを使えば、開発フロー全体はExpoに任せつつ、全体の記述をJSで、UIの記述をReactベースで行えることになります。かなり多くの知識をWeb技術から流用できそうです。

では、新しく学習が必要なところはどこでしょうか?

Webの知識で戦えるところ、戦えないところ

f:id:mkubara:20190205110436p:plain
知識マップ

Webの知識で戦えるところ

カテゴリ 学習済みの知識で戦えるところ
UIのコンポーネント化・イベント周り React
フロントエンドロジック Redux, Redux-Saga
フォームバリデーション Redux-Form
汎用ロジック moment, lodash, validator, normalizr, etc.
スタイル記述 (CSS in JS) styled-components
Linter/Formatter ESLint, Prettier

RNとExpoによって、ReactでのUI記述や、JS記述のロジックなど、多くの技術がそのままスマホアプリへ転用できることがわかります。心強いですね!

Webの知識だけでは戦えないため、学習したところ

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

ネイティブ固有のUIやナビゲーション、メディアの活用、デバッグ手法など、Webの概念と微妙に異なる知識もあります。次章では、この知識の差分を埋めるために学習した、32のコトをご紹介していきます。

新たに学習した32のコト

開発環境 (2)

前述の通りです。インストールは非常に簡単で、npmコマンドだけで終わります。あとはスケルトンプロジェクトを expo init で作成するだけで、すぐに開発が開始できます。

留意点として、Expoはアカウント登録が必要になりますが、登録するとExpoサーバへデプロイが可能になり、実機テストやコードのパブリッシュも容易になりますので、メリットのほうが強いです。

ナビゲーション (3)

React Navigationは、アプリ内での遷移部分を担当します(react-routerのポジション)。URLリンクでの遷移ではなく、アクションを起動してスクリーン間を遷移させるような記述法になります。 onClick = id => { this.props.history.push({id}); } みたいな感じです。

遷移記述は「this.props.navigationからアクションを叩く」か「Redux Middlewareで紐つけておいてSagaなどから叩く」かになります。遷移先の指示はスクリーンにつけたunique名を使い、querystringみたいなものはparamsとして引き渡せますので、/users/:id や /posts?q=Expo みたいな指示が実質的に可能です。

handlePressNav = yearMonth => {
  this.props.navigation.navigate('List', { yearMonth });
}

ナビゲーションの種類としては、push遷移型のスタック(戻れる)と、replace遷移型のドロワー、タブ、スイッチなどがあり、それらをネストさせて組み上げることが可能です。

ナビゲーションの記述については、JSXではなく専用のデータモデルを使用しますが、React-RouterでSwitch/Routeだけのコンポーネントをページ単位で記述するような感覚で、Navigatorコンポーネントをexportできます

import { createSwitchNavigator, createStackNavigator } from 'react-navigation';

// アプリのメイン部分のナビゲーションスタック
const AppStack = createStackNavigator(
  {
    Main: MainTab, // タブナビゲーションを積む
    Quiz: QuizStack, // タブナビを使わないスタック
  },
  {
    initialRouteName: 'Main',
    headerMode: 'none',
  },
);

// 認証などを含む、最上位のナビゲーション
const RootNavigator = createSwitchNavigator(
  {
    Initialize, // 自動認証し、成功したらApp、失敗したらSignInへスイッチ
    SignUp,
    SignIn,
    App: AppStack, // Appへナビゲーションした場合は、AppStackがルーティングを担当
  },
  {
    initialRouteName: 'Initialize',
  },
);

export default RootNavigator;

ナビゲーション用のUIも提供されます。ドロワー・ヘッダ・タブなどがあり、Navigatorコンポーネントに自動付与されます。環境ごとに最適なUIが表示されるため、これらの実装を省略し、作りたいことに集中できるのは、React Navigationの非常に良い点でした。

import { createBottomTabNavigator } from 'react-navigation';
import SvgUri from 'react-native-svg-uri';

const homeIcon = require('./icon-nav-home.svg');
const homeIconCurrent = require('./icon-nav-home-on.svg');

const HomeIcon = ({ focused }) => (
  <SvgUri width="24" height="24" source={focused ? homeIconCurrent : homeIcon} />
);

const MainTab = createBottomTabNavigator(
  {
    Home: {
      screen: Home,
      navigationOptions: {
        title: 'ホーム',
        tabBarIcon: HomeIcon,
      },
    },
    Ranking: { ... },
    Setting: { ... },
  },
  {
    initialRouteName: 'Home',
    tabBarOptions: { ... },
  },
);

f:id:mkubara:20190204162742p:plain
下タブナビ

ネイティブUIの提供 (5)

NativeBaseは、ReactベースのUIフレームワークです。Material-UIやSemantic-UIをご存知であれば、同様の書き心地で記述ができます。環境ごとのネイティブUIを適切に使ってくれますし、テーマの設定や、styled-componentsによる個別上書きも可能でした。

import { Container, Button, Text } from 'native-base';

export const NativeBaseSample = () => (
  <Container>
    <Button>
      <Text>Button</Text>
    </Button>
  </Container>
);

スタイル記述はCSS in JSを使うことになります。styled-componentsがRNに正式対応しており、CSS的な記法でスタイリングが可能です。

レイアウトについてWebとアプリで考え方が異なる点は、Webの場合は溢れた要素を縦方向に流しますが、アプリの場合は一画面に収まるように要素全体をスケーリングする場合が多いことです。そこで、react-native-responsive-screen, react-native-responsive-fontsizeの2点を使って、画面幅基準の%指定によるレイアウトを行うようにしました。

import styled, { css } from 'styled-components/native';
import { Button as NbButton, Text as NbText } from 'native-base';
import { heightPercentageToDP as hp } from 'react-native-responsive-screen';
import rf from 'react-native-responsive-fontsize';

import { hasVariant } from '../../../utils/style';

export const Button = styled(NbButton)`
  display: flex;
  justify-content: center;
  align-items: center;
  align-self: center;

  background-color: ${props => (hasVariant(props.variant, 'primary') ? '#40356f' : 'white')};

  ${props =>
    hasVariant(props.variant, 'rounded') &&
    css`
      height: ${hp('6.6%')};
      border-radius: ${hp('3.3%')};
    `};
`;

export const Text = styled(NbText)`
  align-self: center;

  font-size: ${rf(2.4)};
  font-weight: bold;
  color: ${props => {
    if (hasVariant(props.variant, 'primary')) return 'white';
    if (hasVariant(props.variant, 'default')) return '#4facfe';
    return 'gray';
  }};
`;

f:id:mkubara:20190204162831p:plain
ログインボタン

モーダルについては、Webでは擬似的なものなので無限に重ねられますが、アプリの場合はシステムモーダルを使う関係で、RN側で1枚制限などがかかります。特に表示切り替え時にアニメーションを入れた場合は、切り替え後のモーダルが表示制限に引っかかって表示されず、waitが必要だった、なんていう問題が出たりしますので注意が必要でした。

メディア表現とプリロード (5)

画像やアイコン、フォントなどについては、Webと同じように遅延読み込みされます。気をつけたい点としては、Webの世界では初期レンダリング時にコンテンツが完全表示されないことは一般的な事象ですが、アプリの場合はアセットをすべてダウンロードしてから画面を表示することが多いでしょう。

そういった場合は、アセットの準備完了を同期するために、 Asset.fromModule, Image.prefetch, Font.loadAsync などのプリロードメソッドをそれぞれ使用し、await Promise.all([...]) で待ち受けてから表示する、というような実装が必要でした。

function cacheImages(images) {
  return images.map(image => {
    if (typeof image === 'string') {
      return Image.prefetch(image);
    }
    return Asset.fromModule(image).downloadAsync();
  });
}

const assetImages = cacheImages(images);
await Promise.all([...assetImages, ...others]);

グラデーションなどのエフェクトは、CSS in JSでは記述できず、ネイティブ機能に頼ることになります。ExpoがLinearGradientコンポーネントを提供していますので、私はそれを使用しました。アニメーションについてはLottieが推奨されています。

import { LinearGradient as ExLinearGradient } from 'expo';

const LinearGradient = styled(ExLinearGradient).attrs({
  colors: ['#cccaee', '#6c699b'],
})`
  flex: 1;
  width: 100%;
  height: 100%;
`;

export const MainContainer = ({ children }) => (
  <SafeAreaView>
    <Container>
      <LinearGradient>
        <Content>{children}</Content>
      </LinearGradient>
    </Container>
  </SafeAreaView>
);

続きは後編にて!

まだ半分ほどのご紹介ですが、続きは後編にてご紹介していきたいと思います。コンテンツは以下のものを予定しております。近日公開予定です!

  • ネイティブ機能
  • デバッグ
  • ビルドとリリース
  • フロントエンド以外の技術
  • 実際の開発の経過

※追記(2019.3.28):後編を公開しました!

developers.relationsgroup.co.jp