こんにちは、エンジニアの富岡です。この記事はウォンテッドリー・アドベントカレンダー2025夏の13日目の記事です。
私たちは昨年の夏に福利厚生サービス「Perk」の iOS / Android アプリをリリースしました。このアプリは枠組み部分を Flutter で作り、中身は WebView で構築するというアーキテクチャを採用しています。
この記事では、私たちがなぜこの技術選定を行ったのかという背景、WebView ベースでアプリを開発する上で対応が必要だったこと、そしてリリースから1年ほど経った今の状況からこの技術選定にしてみてどうだったかについてご紹介します。
目次
Perk について
Perk アプリについて
WebView + Flutter という技術選定について
WebView 利用の理由
枠組み部分に Flutter を使った理由
プロトタイピングによる実現性の検証
WebView + Flutter によるアプリ開発の実際
WebView と Flutter の棲み分け
Flutter と WebView のログイン状態の連携
Web 側に加えている主なアプリ用の分岐処理
この技術選定にしてみてどうだったか
よかった点
妥協が必要な点
WebView ベースがフィットする状況
まとめ
Perk について
Perk はウォンテッドリーが 2020年から提供を開始した福利厚生サービスです。従業員の皆様の毎日を豊かにするための、多彩な特典を提供しています。
https://engagement.wantedly.com/perk/
Perk アプリについて
Perk はサービスリリース以来、Web のみでサービスを提供してきましたが、昨年夏に iOS / Android アプリをリリースしました。 ここ数年、福利厚生市場における競争は激化しており、アプリがないということは Perk にとって競争上の大きな課題となっていました。この状況を解消するため、迅速にアプリを開発しリリースすることが開発チームにとってのミッションでした。
リリースしたアプリの画面は以下のようになっています。
WebView + Flutter という技術選定について
アプリの技術選定にあたっては、以下の3つの選択肢を検討しました。
- Swift / Kotlin によるネイティブ実装
- Flutter や React Native といったクロスプラットフォーム技術で実装
- WebView ベースで実装
結論として、私たちは「コンテンツの大部分を WebView で表示し、枠組み部分(ガワ)を Flutter で構築する」という構成を選択しました。
WebView 利用の理由
WebView ベースとする選択は、ビジネス要求、組織体制、プロダクト特性の3つを考慮して行いました。
まずビジネス要求については、以下のような要求がありました。
- Web で提供している機能は、基本的にアプリでも利用できること
- 初回リリースまでのリードタイムをできる限り短縮すること
- iOS と Android の両アプリを、なるべく時期を空けずにリリースすること
- リリース後の機能追加も、できる限り速度を落とさずに継続できること
競争が激しい事業環境において、2点目と4点目は特に重要でした。また福利厚生というサービスの特性上、全従業員が等しく使えるという公平性も重要になるため、3点目も大事でした。
次に組織体制についてですが、当時社内のモバイルエンジニアは3名で、主に別プロダクトの開発に従事していました。 モバイルエンジニアの採用は難易度が高く、Perkアプリ開発のためにモバイルエンジニアの安定したリソースを確保し続けることは難しい状況でした。 1→10の成長期にある Perk 事業において、モバイルエンジニアのリソースが確保できずに機能リリースが滞るという事態は避けるべき問題でした。
最後にプロダクトの特性についてですが、Perk の主な機能は、特典を検索・閲覧し、利用するという比較的シンプルなものです。 カメラやモーションセンサーといった、OS固有の高度な機能への依存度は低いという特性がありました。
これらの状況を考えたときに、WebView ベースで作るという選択肢は非常に合理的でした。Perk にはすでにスマートフォンに最適化されたWebの実装が存在したため、ネイティブや他のクロスプラットフォーム技術で一から実装する場合と比較して、リリースまでのリードタイムを大幅に短縮できます。また、iOS と Android の両方をなるべく時間を空けずにリリースできる、という要件も実現することができます。今後の機能リリース速度についても、Web とアプリで実装を共通化できること、モバイルエンジニアのリソースを確保せずとも大部分の変更は Web のエンジニアだけで行えること、そしてストアの審査を経ずにリリースできることから、最もスピードを維持できると考えました。
WebView ベースのデメリットとしては、触ったときのスムーズさや気持ちよさが劣る点や、OS機能との連携にやや手間がかかる点がありますが、私たちの状況においてはメリットがデメリットを上回ると判断しました。
枠組み部分に Flutter を使った理由
ログイン処理など一部の機能は WebView の外側で実装する必要があります。 こうした部分についても、クロスプラットフォーム技術で実装を共通化し、開発・メンテナンスコストを抑えたいという狙いがありました。
また、React Native は過去に社内でプロダクト利用した経験がありましたが、Flutter はプロダクトで利用した経験がなかったため、この機会に知見を蓄積したいというモバイルチームの狙いもありました。
プロトタイピングによる実現性の検証
アプリの大部分を WebView で構築することや、Flutter の利用は、社内にとって初めての試みでした。 そのため、技術的な実現性や、Flutter と WebView の連携に問題がないかといった点には不確実性がありました。そこで開発初期の1ヶ月ほどでプロトタイプ実装を行い、実現性の確信度が高まったことで、正式にこの方針で行くことを決めました。
WebView + Flutter によるアプリ開発の実際
ここからは、実際に WebView + Flutter でどのようにアプリを構築しているかを紹介します。
WebView と Flutter の棲み分け
まず、WebView で実装する部分と Flutter で実装する部分の棲み分けをどうしたかですが、可能な限りWebエンジニアで変更を行えることを重視し、大部分を WebView に寄せる方針を採りました。 一方で、ネイティブの機能が必要な箇所や、WebView では実装が難しい部分は Flutter で実装しています。
以下の図のように、ログイン画面やボトムナビゲーションバーといったアプリの「枠」の部分を Flutter が担当し、その内側で表示されるコンテンツ部分は WebView になっています。
具体的には Flutter で実装しているのは、以下のような箇所です。
- ログイン、ログアウト処理
- バックグラウンドから復帰した際のアカウント有効性チェック
- ユニバーサルリンクのハンドリング
- ボトムナビゲーションバー
ログイン周りについては、主要なソーシャルログインがセキュリティ上の理由から、WebView 内では動作しない ため、Flutter 側での実装が必要となります。
WebView 領域内の画面遷移は、通常の Web のページ遷移で行っています。 遷移のたびに WebView インスタンスを生成する方式も検討しましたが、特に Android においてパフォーマンス上の懸念があったため採用しませんでした。 Web のページ遷移ではどうしてもネイティブアプリのような滑らかな画面遷移とはなりませんが、遷移時に画面下部にローディングインジケーターを表示することで、「タップしたのに反応がない」と感じる違和感を軽減しています。
またボトムナビゲーションバーは、各タブが独立して今開いているページや遷移履歴などの状態を保持できるように、タブごとに一つずつ WebView インスタンスを作っています。
Flutter と WebView のログイン状態の連携
ユーザーのログイン状態は、FlutterとWebViewの間で以下のように連携しています。
- まず、Flutter 側でバックエンドに対してログイン認証リクエストを送信し、成功したらアクセストークンを受け取ります。
- WebView で最初のページ読み込みを行う際、アクセストークンとクッキーの交換エンドポイントにリクエストします。この際、本来開きたいページのURLをURLパラメータで渡します。アクセストークンはHTTPヘッダーに付与します。
- バックエンドは受け取ったアクセストークンを検証し、正当であればセッション用クッキーを発行した上で、本来開きたいページにリダイレクトします。
- WebView ではログインが済んだ状態でページが開かれます。
- 以降、WebView にはクッキーがセットされているため、ページを遷移してもログイン状態が維持されます。
(図中のエンドポイントのURLは実際のものとは異なります)
ログアウト時は、Flutter 側で持っているセッション情報や WebView のクッキーのクリアを行っています。
Web 側に加えている主なアプリ用の分岐処理
WebView 内では基本的にスマートフォン向けの Web 画面を表示していますが、一部、アプリとして自然な体験を提供するために分岐処理を入れています。なお、Web ページが WebView 内で開かれているかどうかの判定は、Flutter 側から WebView に対して特定のクッキーをセットすることで行っています。
具体的に行っている主な対応は以下の通りです。
不要な要素の除去
まず Web に設置されているサイトフッターは、いかにも Web サイトをただ持ってきました感が出るため除去しています。利用規約やプライバリーポリシー、お問い合わせなどの各種リンクは、アプリではアカウントタブに移動しました。
アプリ用のヘッダーを設置
同様に Web 版のグローバルヘッダーも除去し、代わりにモバイルアプリとして自然な、「戻る」ボタンを配置したヘッダーをWebで実装しています。 「戻る」ボタンは、history.back() によってWebView の遷移履歴を戻る、シンプルな実装です。当初このヘッダーは Flutter 側で実装していましたが、ページの表示内容に応じてヘッダーの表示を切り替える際にラグが発生し不自然さを感じたため、最終的に Web 側で実装する形にしました。
外部サイトへのページ遷移を WebView 外で開く
Perk には特典プロバイダーのサイトや利用規約・ヘルプページなど、外部サイトへの遷移が存在します。Web では target="_blank" のように別タブで開くようになっているリンクも、WebView 内では通常のページ遷移として扱われてしまいます。 このような外部サイトへの遷移が自然なユーザー体験になるように、アプリ内ブラウザや外部ブラウザで開くように制御しています。
実装としては、webview_flutter パッケージが提供する addJavaScriptChannel という機能を使って、Web 側の JavaScript からFlutter を呼び出し、Flutter 側でブラウザを起動させています。コードのイメージは以下の通りです。
// Flutter 側
final WebViewController controller = WebViewController();
controller.addJavaScriptChannel(
'perkAppOpenInAppBrowser',
onMessageReceived: (message) async {
await _handleOpenInAppBrowser(message);
},
);
Future<void> _handleOpenInAppBrowser(JavaScriptMessage message) async {
final json = jsonDecode(message.message);
final String url = json['url'];
await launchUrlString(url, mode: LaunchMode.inAppBrowserView);
}
// Web 側
const openInAppBrowserIfInWebView = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (window.perkAppOpenInAppBrowser) { // WebView 内でのみ以下の処理を実行する
event.preventDefault(); // 通常のページ遷移をキャンセル
window.perkAppOpenInAppBrowser.postMessage(JSON.stringify({ url: event.currentTarget.href }));
}
};
...
<a href={providerUrl} rel="noopener" target="_blank" onClick={openInAppBrowserIfInApp}>
特典プロバイダーのWebサイトをみる
</a>
なお、addJavaScriptChannel を用いる場合、外部サイトから勝手に postMessage を呼び出されてしまうとセキュリティ上の問題があるため、外部サイトは WebView の外側で開くように徹底する必要があります。
この技術選定にしてみてどうだったか
この技術選定で実際にリリースまで開発してみて、そしてその後も1年ほど機能開発を行って感じた点についてまとめます。
よかった点
まずよかった点としては、技術選定の際に持っていた要求はすべて狙い通りに実現することができました。
初回リリースまでのリードタイムとしては、モバイルエンジニア1人 + Webエンジニア1人という体制で、プロトタイピングから iOS アプリのリリースまでが 3ヶ月、その後 Android アプリは1ヶ月でリリースができました。WebView で Web アプリケーションを動かすにあたり、より多くの問題が発生することを予想していましたが、実際には対応が必要な箇所は思いのほか少なかったです。また、iOS アプリのリリース後、Android アプリをリリースするために必要だった作業としては、ソーシャルログインのための OAuth クライアント登録、CI/CD の整備、ストア申請といった周辺作業がメインで、Flutter で実装しているアプリケーションコード自体の対応は最小限で済みました。
初回リリース後も小中規模の機能をいくつかリリースをしてきましたが、そのほとんどが期待通りWebエンジニアだけで完結できています。実際には Web 用の実装をすれば、ほとんどのケースでそれがそのままアプリ上でも問題なく動くため、アプリに機能展開するための開発工数のオーバーヘッドは非常に小さく済んでいます。位置情報利用の許諾や、iOS ユニバーサルリンク / Android アプリリンクの対応など、OSの機能に関わるような部分はモバイルエンジニアの協力が必要でしたが、そうした場面は今のところそこまで多くありません。
Flutter の利用に関しては、Web エンジニアである私の立場からすると、アプリのロジックがどうなっているか調べるときに見に行く先のコードが一箇所で済むところも助かっています。
妥協が必要な点
この構成で大きく困ったという場面は今のところ少ないですが、妥協が必要になる場面は存在します。多いのは UI デザインで、このデザインはこの構成だと難しいから別の形にしよう、となった場面は何度かありました。
また、戻るボタンで画面を戻ったときに情報が古いままになっているというような問題も起きます。例えば、トップ画面から別の画面に遷移してポイントを利用した後に、戻るボタンでトップ画面に戻ったときに保有ポイント数が減っていない、というような問題です。これは WebView に限らずブラウザ上でも起きる問題ですが、アプリ内では戻るボタンがより気軽に利用されるようになるため問題が顕著になります。これは現状の Perk の Web が MPA な作りになっていることも原因であり、SPA にして画面にまたがってデータの更新管理を行えば解決する問題ではあります。今のところは画面を再読み込みすれば情報が更新されるので許容することにしています。
WebView ベースがフィットする状況
Perk では諸々の背景や状況があって、WebView ベースという構成がうまくフィットしました。改めて整理してみると、以下のような状況では WebView ベースでアプリを作ることは有力な選択肢になると思います。
- 事業特性
- Webとモバイルアプリの両方でサービスを提供し、機能変更をなるべく遅滞なく全プラットフォームへ届けたい。
- コンテンツ部分のデザイン変更を伴うA/Bテストやキャンペーンなどを頻繁に実施したい。
- ネイティブアプリのような滑らかなUIへの要求が、事業の成功に対してクリティカルではない。
- 組織状況
- Webエンジニアのリソースは確保できるが、モバイルエンジニアのリソースを安定的に確保することが難しい。
まとめ
この記事では、Perkアプリにおける WebView + Flutter という技術選択の背景、実際にどういった対応が必要だったか、初期リリースやその後の機能開発をしてみてどうだったかについて紹介しました。WebView ベース、あるいは WebView + Flutter という構成でのアプリ開発を検討されている方の参考になれば幸いです。
最後に、Perk を一緒に成長させてくれる仲間を募集しています。ご興味のある方は、リンクの募集からぜひお気軽に話を聞きにきてください!