React Native + Web 超高速同時展開
株式会社ハロー uiu
uiu @uiu______
Hello ⇦ Cookpad ⇦ Nota ⇦ 京都大学
創業時(3年前) からゼロから作っている
フルスタックエンジニア Ruby / JavaScript
https://gyazo.com/b7f1bfcc3518fd3d8b36a13f938bab5f
React Native これまで
創業時から3年以上 React Native で開発運用している
2サービスを React Native で開発運用
iOS, Android, Web で展開している
React Native で開発しているサービス
AIが予約代行するグルメアプリ
テーブルオーダー
AutoReserve
AIが予約代行するグルメアプリ
自動音声で電話かけてレストランの予約をする
iOS, Android, Web で展開
https://gyazo.com/ea0ec456f377846ca5a146e40de3c8a9
AutoReserve Order
テーブルオーダー
レストラン店舗用 タブレット/スマホアプリ
顧客用メニュー/注文画面 web
https://gyazo.com/9128dfabb447773b378108197bd31c3b
React Native 良かったこと
小さいチームでスピード感のある開発ができる
フロントエンド 1人 + バックエンド 1人 で開発を回せる (iOS / Android / Web)
現在は メイン 2人 + CEO + 業務委託 2-3人 の体制
改善の素早さは武器になる
over-the-air update
特に toB では競合よりも要望改善が速いことはアドバンテージ (実感)
React Native 良かったこと2
人件費
3人必要な内容を2人で開発できる
-> 打席に立てる回数が増える
採用面: 現状では問題ない
Web の React エンジニアから React Native へのコンバートは簡単
技術スタック
REST API (Rails)
TypeScript
React Native
react-native-web
redux / redux-saga
swr (vercel/swr)
テスト: Cypress
今回の話
React Native でのコードベースを活用して Web に同時展開している
Next.js 等との対比の参考に
Web の特徴
view フレームワークとして react-native-web を使っている
Server Side Rendering してない
Web版を開発した背景
AutoReserve ではアプリを先行して開発していた
ユーザー獲得面で SEO / SEM が重要になった
素早く Web 版を開発する必要がでてきた
react-native-web
React Native for Web
React Native と同じAPI (Component, Styles 等) が Webで動く
Twitter, Uber 等で使われている
code:js
import React from 'react';
import { AppRegistry, StyleSheet, Text, View } from 'react-native';
class App extends React.Component {
render() {
return (
<View style={styles.box}>
<Text style={styles.text}>Hello, world!</Text>
</View>
);
}
}
const styles = StyleSheet.create({
box: { padding: 10 },
text: { fontWeight: 'bold' }
});
AppRegistry.registerComponent('App', () => App);
AppRegistry.runApplication('App', { rootTag: document.getElementById('react-root') });
react-native-web
html で render される
accessibility support
<Text accessibilityRole="heading" />
https://gyazo.com/1b98106cf17856669521c3bdecf71003
react-native-web の便利さ
程良く抽象化された component が使える
<Text /> <TextInput /> <TouchableOpacity /> <FlatList />
Styles API での style 指定 (CSS in JS)
すべて style prop に書く
CSSセレクタの優先順位や別の箇所からの上書きについて考えなくていい
react-native-web の強力さ
アプリから最小労力で機能を移植できる
ほぼコピペで同様の見た目の component が作れる
とてつもないスピードで Web 版が完成した
SEO
SSRしない
Dynamic Rendering する
クローラーの時だけサーバーサイドの chrome で HTML を render する
SSR時の問題
当初 Next.js で SSR する予定だったが...
クライアントでは動くがSSRで壊れることがよくあった
サーバーサイドでの API 呼び出し, 認証周り状態の複雑さ
開発コストが高くなった
クローラーからのアクセスの時のみ、サーバーサイドの headless chrome で JS を評価して HTML を返す
Google も認めている方法
Google 等検索エンジンだけでなく LINE / Slack 等にも対応可能
https://gyazo.com/4937eec59681c3eb165e54a360b19209
Dynamic Rendering のデプロイ
コストを抑えるため Cloud Run 上で動かしている
App Engine -> Cloud Run で コストが 10分の1 に
SEOはうまくいっている
https://gyazo.com/68330d6cb2dbc8ed3e509c5ea941125b
SSRを辞めてよかったこと
view側 (CSS in JS 系) で SSRのことを考えなくてよくなった
レスポンシブ対応が簡単になった
React Native とのコード共有が簡単になった
レスポンシブ対応が簡単になった
mobile / pc で操作できるUI
SSRでは細かい挙動の分岐が困難だった
コンポネントの一部の挙動だけSPとPCで変えたいなど
useResponsive() hook 一つでレスポンシブ対応が完了する
レスポンシブ対応
flex レイアウト
分岐して調整
code:js
const { width, sm } = useResponsive()
return (
<TouchableOpacity
style={[
{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
height: width < sm ? 60 : 72,
paddingLeft: width < sm ? 16 : 32,
paddingRight: width < sm ? 16 : 32,
React Native とのコード共有が簡単になった
サーバーサイドでの API 呼び出し, 認証等を気にしなくてよくなった
アプリのアーキテクチャとほぼ同じなのでコピペしやすい
まとめ
React Native で iOS, Android, Web 同時展開している
チームを小さく保って、高速に開発できている
SSRしない選択肢の良い点を紹介しました
今後やりたいこと
app, web の component 共有
E2Eテストの拡充
API Schema
---
おまけ
API Schema
Twirp (protobuf) ベースの RPC API を一部導入している
TypeScript APIクライアント + リクエスト, レスポンスの型自動生成
protobuf
Google のスキーマ言語
そこそこ読み書きやすく、エコシステムが充実している
Twirp
protobuf でスキーマ定義できるRPCフレームワーク
code:protobuf
service BusinessUser {
// require current_owner
rpc Index(IndexRequest) returns (IndexResponse);
rpc GetBusinessUser(GetBusinessUserRequest) returns (BusinessUserResource);
}
message IndexRequest {
int32 page = 1; // (default 1)
int32 per_page = 2; // (default 20)
}
message IndexResponse {
repeated BusinessUserResource business_users = 1;
}
message GetBusinessUserRequest {
string id = 1;
}
message BusinessUserResource {
enum Status {
INVITED = 0;
REGISTERED = 1;
}
enum Role {
ADMIN = 0;
NORMAL = 1;
}
string id = 1;
string email = 2;
string name = 3;
Status status = 4;
Role role = 5;
google.protobuf.Timestamp created_at = 6;
google.protobuf.Timestamp updated_at = 7;
}