CarelyにSPAを導入するメリットはあるのか | Dev Driven 開発・デザインチーム CarelyにSPAを導入するメリットはあるのか | 働くひとと組織の健康を創る iCARE

BLOG

CarelyにSPAを導入するメリットはあるのか

こんにちは、エンジニアの澁谷です。
私たちは以前よりチームの成長と成果を目的とした人事制度、「ファイブリングスチャレンジ」を行なっています。
iCARE、新たな人事制度を開始。週2時間を“働くひとの健康創り”に使う「ファイブリングス・チャレンジ」

今年の春ごろ、私はこの制度でCarelyをSPA化したらどうなるのか?
というLTを当時の開発チーム内で行いました。
こちらはその内容を記事化したものになります。

CarelyはRailsとVue.jsを用いたMPA(Multiple Page Application)です。
試しにこちらにSPAを導入してみて、やったこと、気づいたことをまとめました。

SPAとは?

SPAとは「Single Page Application」の略で、単一のページでコンテンツの切り替えを行うWebアプリケーションのアーキテクチャの名称です。
SPAではブラウザによるページ遷移をせずにコンテンツの切り替えなどを行うことで、ユーザー体験(UX)を大きく向上させることができます。
従来のWebページでは遷移時にページ全体が書き換わりますが、SPAではJavaScriptを用いてページ内のHTMLの一部を差し替えてコンテンツを切り替えています。
これにより、ブラウザの挙動に縛られないUIの実現や、パフォーマンスの向上が可能になります可能になります。

引用:「シングルページアプリケーション(SPA)の導入メリット&デメリット」
https://www.oro.com/ja/technology/001/

だいぶ要約しますが、ページ遷移しているように見せかけて単一ページで動くアプリのこと。

都度ページを読み込む必要がないため高速で動く!(ように見える)のが特徴です。
また一般的にはレンダリングをフロント側で行い、表示したいデータの取得だけAPIサーバーにリクエストする形になります。
上記をCSR、初回レンダリングをサーバーで行うことをSPAのオプション的な意味でSSRと呼んだりもします。
(SSRについては複数の捉え方があると思います。)

SPA化するための2種類の方法

.html.erbをマウントする

Railsのルーティングを使用する方法。
一つのviewを利用し、マウントしてレンダリングされたものがSPAとなる形式です。

完全に分離する

サーバーを分ける方法。
フロントの開発をするときはRailsのViewを必要としません。
またその分、機能拡張が行いやすい面もあります。(一部の機能をGoで実装したい!・・とか)

今回は疎結合な完全に分離する方法を試してみます。

技術選定

  • フレームワーク
    • Ruby on Rails
    • そのままに
    • React(CarelyはVue.js)
    • 全部JSで書けるのはいいよね
    • ちょっとやってみたかった
  • ビルドツール
    • Vite
    • ビルドの早さで

実装!

ディレクトリとファイルを用意

プロジェクト名をcarely-spaとして、Reactを選択。

$ yarn create vite

✔ Project name: … carely-spa
✔ Select a framework: › react
✔ Select a variant: › react-ts

フロント構築に必要なファイルが作成される。

create mode 100644 carely-spa/.gitignore
create mode 100644 carely-spa/index.html
create mode 100644 carely-spa/package.json
create mode 100644 carely-spa/src/App.css
create mode 100644 carely-spa/src/App.tsx
create mode 100644 carely-spa/src/favicon.svg
・
・

今回carely-spaはアプリケーションのディレクトリ直下に配置するようにしました。
二つ目のfrontendディレクトリを作成するイメージです。

立ち上げ

yarn installしてyarn dev
これでReactの準備が完了。

$ yarn dev

vite v2.9.12 dev server running at:

> Local: http://localhost:3031/
> Network: use `--host` to expose

ready in 405ms.

codegenを準備する

carelyはGraphQL、そしてGraphQL Code Generatorを利用しています。
新たにViteで立ち上げた環境にもインストールしていきます。

  1. graphqlとapollo-clientをインストール

    yarn add graphql @apollo/client
  2. codegenに必要なパッケージをインストール

    yarn add -D @graphql-codegen/cli
    @graphql-codegen/typescript
    @graphql-codegen/typescript-operations
    @graphql-codegen/typescript-react-apollo
    @graphql-codegen/near-operation-file-preset
  3. codegen.yml作成し、設定を記述する

    overwrite: true
    schema:
    // 今回はrakeタスクであらかじめ生成していたスキーマを参照
    // http://localhost:3000/graphqlでもOK
    - ../frontend/gen/schema.graphql
    generates:
    src/gen/types.ts:
    plugins:
    ・
    ・
    ・
  4. package.jsonにスクリプトを追加

    "generate": "graphql-codegen --config codegen.yml"
  5. yarn genearte
    ここでsrc/gen/types.tsを用意しておきましょう。
    作成できたらyarn generate

    yarn generate
    yarn run v1.22.17
    $ graphql-codegen --config codegen.yml
    ✔ Parse configuration
    ✔ Generate outputs
    ✨  Done in 1.56s.

gen/types.tsがにスキーマの型が追加されました!
これでいつもと変わらずなフロント実装が行えます。

ルーティングの設定

SPAなのでルーティングの設定がRailsからReactに変わります。
React Routerを導入し、ルーティングにコンポーネントを当てていきます。

yarn add react-router-dom@6 @types/react-router

carely-spa/src/router/index.tsxを用意。

import { Route, Routes, BrowserRouter } from 'react-router-dom'
// 仮置きのルート
import TestComponent from '../features/test/TestComponent'
// 架空の画面(実際は既存の画面を描画)
import ArticleIndex from '../features/articles/index/Index'

const CarelyRoutes = () => (
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<TestComponent />} />
      <Route path="/articles" element={<ArticleIndex />} />
    </Routes>
  </BrowserRouter>
)

export default CarelyRoutes

ここまでくればあとは既存の画面を実装するのみ!です!

動かしてみると

分かっていたことですがやはり速い。
体験としてはこれに勝るスピードはないと感じるほどでした。

移行時にやらなければいけないこと

さて、試してみるのは簡単ですが、これを継続的に開発・運用していくにはどのようなタスクが積まれるのでしょうか。

インフラの整備

私はインフラに関してあまり詳しくないのですが、
今回の方法だと完全にサーバーとフロントを分離しているため、インフラ側もこれに対応する必要があると思います。

認証方法の変更

Railsを使っているなら認証にdeviseを利用することも多いと思います。
これまではそれを利用したセッション認証でログインやログインユーザーの情報を取得していました。
しかしフロントが分離するとこれを行なうことができず、トークン認証に変更する必要があります。
クエリを叩くたびにApolloを通してトークンを渡し、疎通させる必要があるのです。

依存関係の解消

RailsのViewからdata属性でフロントに渡している内容を、すべてAPIで取得できるようにする必要があります。

セキュリティ対策

認証がトークンになることなど、変更によって起こるインシデントに備えが必要です。
脆弱性についてはこちらのスライドなどを活用しながら勉強したいところ。
https://www.docswell.com/s/ockeghem/K2PPNK-phpconf2022

エラーハンドリングはフロントの責務に

これまでサーバーサイドで行われていた認証エラー後のリダイレクトなど、各画面でのハンドリングは基本的にフロントが担うことになります。
ハンドリング用の関数を用意することはもちろん、画面ごとに適用していく必要があります。

などなど、一例を書きましたがおそらくもっともっとあります。

MPAはダメなの?

そんなことはありません!

ビルドされたものが置いてある本番などの環境であれば(処理に問題がなければ)速度が遅くて気になることはほぼ無いですし、速度的にもユーザー体験が劇的に変わるかというとそうではないと思います。

アプリの特性やフェーズ、状況によって選定すれば良く、
よほどこだわりがなければSPAにする必要はないと感じています。

まとめ

  • ユーザー体験を劇的に変えるものではなく、開発におけるメリットの方が多そう

    • BEとの依存性がない
    • ルーティングもサーバーサイドで用意する必要がない👏
    • コードの健全面
    • 分離することにより曖昧になっている互いの責務が明確になりそう。
    • 拡張性
  • 移行には多くの課題が伴い、莫大な工数がかかることが予想される

  • SPAならではの追加実装が多数ある(エラーハンドリング、認証やdata属性の撤去など)

現時点ではSPAに移行するメリットは少ないかと思います。
追加機能実装時の部分的SPAや、マイクロサービス発足時などには導入を検討してもいいかもしれません。
またSPA化は開発側だけではなくプロダクト側にも影響があることです。
サービスとして何を求めるか、フェーズも検討しながら会社全体で方針を決めていけると良さそうですね。

画面を描画するためにやったいろいろメモ

CSRFの無効化

(良い子のみんなは真似しない)

# development.rb
config.action_controller.allow_forgery_protection = false

CORSの設定

# config/application.rb
config.middleware.insert_before 0, Rack::Cors do
  allow do
      origins "http://localhost:3030"
      resource "*",
      headers: :any,
      methods: [:get, :post, :patch, :delete, :options, :head]
  end
end

fetchPolicyはcache-and-networkに

apolloのfetchPolicycache-and-networkにしておくと安心です。
SPAは画面遷移しても都度レンダリングを行いません。
そのためcache-first(デフォルト設定)の場合、キャッシュが残っていれば画面遷移をしても表示は更新前のままです。

import { useArticlesQuery } from '../graphql/articles.generated'
import { useMemo, useState } from 'react'

export const useArticles = () => {
  const { data } = useArticlesQuery({ variables, fetchPolicy: 'cache-and-network' })
  const articles = useMemo(() => data?.articles || [], [data])

  return {
    articles,
  }
}