javascripterです。ハローでは、プロダクトのローンチ前からAutoReserve の開発に関わっています。
この記事では、AutoReserveウェブ版が、Next.jsを一度採用したがやめ、その後create-react-app + react-routerの構成に移行した経緯を書きます。
ウェブ版開発の背景
AutoReserve はAIが電話予約を代行してくれる飲食店向け予約グルメアプリで、現在はiOS / Android / ウェブにサービスを展開しています。 元々はReact Native製のネイティブアプリのみ展開していましたが、ユーザ獲得の面でウェブ版が必要となったため、 追加でウェブ版を実装し、現在の3プラットフォームでの展開に至ります。
最初の技術選定
ウェブ版の最初のバージョンでは、フレームワークとしてNext.jsを採用しました。Reactで書け、SEOのための最適化やサーバーサイドレンダリングが行えるためです。
理由をいくつか挙げると、
- AutoReserveは全国のレストラン情報を掲載するメディアサイトなため、SEOを考慮する必要がある
- 先行してローンチしたReact Native製のアプリと技術スタックをなるべく揃えたい
といったものがあります。
また当時、AutoReserveアプリで採用していた技術として、
- React Native
- redux / redux-saga
があげられます。よって、ウェブ版でも同様にredux, redux-sagaを使用することにしました。
React Native for Webの採用
ウェブ版を開発するに当たって、初期はVercelの styled-jsxをCSS in JSとして利用し、プレーンなReactアプリとして実装していました。
しかし、下記のような課題が生じていました。
- アプリ版とウェブ版で二重にコンポネントを実装しメンテナンスしていくのが非効率的
- styled-jsxではCSSセレクタを自由に使えるが、CSSセレクタには詳細度などの概念があり、最終的にどのスタイルが適用されるのかぱっと見でわからず複雑
これらの問題を解決するため、styled-jsx
を使用するのはやめ、React Native for Webという、React NativeのAPIやコンポネントをウェブ向けに実装したライブラリを使用し、React Nativeのコードをそのまま移植する方針を取りました。
React Native / React Native for Webでコードを書く場合はこのようになります。
function Component() { return ( <View style={{ flex: 1, backgroundColor: 'red', }} > <Text>Hello</Text> </View> ) }
React Nativeのスタイリングの仕組みの特徴としては
- Viewのスタイルに元々
display: flex
が当たっている - メディアクエリや
::before
、::after
などには対応していない- 動的なスタイルの変更はJSによって行う
- Atomic CSSを採用しているため、CSSのカスケーディングが存在しない(ウェブ)
ということが挙げられます。
発生した課題
AutoReserveのアプリ、ウェブ版の大きな違いとして
- アプリ版はスマホ向けレイアウトオンリー
- ウェブ版はスマホ、PC向けのレイアウトの両方が必要
ということがあります。
React Native for Web自体はメディアクエリに対応していないため、レスポンシブに実装する必要がある場合、divをコンテナとして囲って、そこにスタイルを当てる必要がありました。
また、オートリザーブでは、単純なレスポンシブデザインではなく、PCとモバイル向けで、完全にわかれたコンポネントを出し分けたい場面が多くありました。
具体的には、
- PCの場合は予約のフォームをインラインで出したい
- モバイルの場合は予約ボタンのみを表示し、タップしたらハーフモーダルを出したい
といった場面です。こういった場合、@artsy/fresnelなどのライブラリを使い、PC・モバイル両方のコンポネントをレンダリングしメディアクエリで片方のコンポネントを隠す必要がありました。
その他、対応が必要だった点を挙げると、
- redux-sagaでロード完了状態を待ってSSRした結果を返す必要がある
- ログインが必要なページはキャッシュを行わないようにする
- reduxのstateをサーバーサイドとクライアントサイドで共有する
- cookieに認証情報を同期する
などが挙げられます。一つ一つの問題は解決可能でしたが、全体として見ると複雑度が高まり開発効率が低下する原因となっていました。
SSRをしないという選択
なぜNext.jsでうまくいかなかったか
- SSRを前提としていない設計のアプリのコードをウェブに移植した
というのが移植の上で発生した問題の主な要因かなと思います。
SPAへの移行
そこで、思い切って元々のアプリの設計に寄せるため、サーバーサイドレンダリングを行わず、SPA(Single Page Application)として書き換えることにしました。
SSRについては、Dynamic Renderingを行ってみることにしました。
Dynamic Renderingとは、JavaScriptを解釈しないクローラのために、クローラがページにアクセスした際に裏でヘッドレスのブラウザを立ち上げJavaScript実行後のHTMLを返す、という技術です。AutoReserveのウェブサイトではRendertronを使用しています。
オートリザーブでは、クローラからのアクセスがあった場合、モバイル版のサイトをDynamic Renderingし返すように実装しています。
Dynamic Rendering の具体的な運用については以下で詳しく説明しています: tech.hello.ai
サーバーサイドレンダリングを行わずSPAになったため、技術スタックは下記のようになりました。
- react-router
- react-native-web
- redux/redux-saga(現在はreduxではなくswrを使用)
どうなったか
実行環境がブラウザのみになったため、Node.jsで実行した際の動作環境の差異を考慮する必要がなくなり、開発スピードが上がり不具合の発生頻度も減りました。
レスポンシブ対応についても、JSの分岐によりレスポンシブ対応を行えるようになり、コンポネントを切り替えたり、プロパティの値を分岐したりといった柔軟な対応が行えるようになりました。 具体的には、このようなコードで書けるようになりました。
function Component() { const { width, sm, md } = useResponsive() if (width < sm) { return <SPComponent /> } return <PCComponent style={{ paddingHorizontal: width < md ? 16 : 24 }} /> }
現在に至るまで、サイトのトラフィックの増加は順調で、SEO上の問題は生じていません。
また、サーバーサイドレンダリングを廃止し、React Native for Webを使用するようになったため、ウェブとアプリのコード差異がほとんどなくなりました。 結果的に、ウェブのコードの大部分をアプリからのコピペで実装できるようになり、現在ではar_sharedという共通パッケージを作成し、ウェブとアプリでコードの共通化をおこなっています。
現在、アプリとウェブでコード共有できている部分は
- ボタン、チェックボックスなどのUIコンポネント
- 色の定義などの定数
- Reactのhooksや日付・数値のフォーマット、文字列操作などのユーティリティ関数
- TypeScriptの型
- 予約の表示画面、予約送信画面のコンポネントなど、アプリケーション画面のコード
です。ウェブとアプリでコードを共通化する取り組みについては、今後このブログで別記事で詳しく取り上げる予定です。 アプリとウェブの画面遷移やルーティングの仕組みの違いなどプラットフォームの差異を吸収するため設計上工夫が必要な面もあり、そのあたりについても解説していく予定です。
まとめ
サーバーサイドレンダリングをしない、という選択によって開発を単純化し、少人数で多プラットフォームに同時展開できるようになったという事例を紹介しました。
社内ではNext.jsを使用しているプロジェクトもあり、プロジェクトごとに最適な技術選択を行い生産性を追求しています。
ハローでは、開発スピードを追求する本物のエンジニアを募集しています。
少しでも気になる方は気軽に javascripter にDMでお声がけください!