demoru.net でのサービス公開イメージ
demore.net とは、弊社で提供している SPA 体験サイトです。
SPA とは何か? (アプリ利用者の目線)
Actor (アプリ利用者) にとって シングルページWebアプリケーション (以下 SPA) とは、いくつかの JavaScript ファイルと css ファイルで構成されている、ブラウザ上で動作するアプリです。
SPA は通常、ユーザ認証を行なってユーザを特定し、Web API を用いたさまざまな情報サービスを提供します。サーバサイドのデータベースやディスクスペースを利用することもありますが、これらのデータアクセスも例外はありますが Web API を用いています。
SPA ではない、従来のサーバサイドの Webアプリ は、ボタンやリンクをクリックするごとに以下の動作を繰り返します。
- ブラウザはサーバに情報取得要求 (リクエスト) を送出する (ex. 情報提供サーバに japan の covid-19 新規感染者数の日別データを要求する)
- ブラウザはサーバからの応答 (レスポンス) を待つ
- ブラウザはレスポンスを受信したらレスポンスに含まれる情報をブラウザ上に描画する
- 通常この描画はブラウザ全体を再描画する動作を伴う (ex. japan の covid-19 累計感染者数の時系列グラフを表示する)
ブラウザとサーバのやりとりは、このリクエストしてレスポンスを待ち、再描画するという「ギッコン、バッタン」スタイルで進んでいきます。
SPA は、必要なアプリケーションとデータを可能な限りまとめてブラウザに渡します。アプリの操作や、他のユーザからの操作によってブラウザ画面上の情報を変更する場合は、変更が必要な箇所だけを再描画する仕組みをもっています。「ギッコン、バッタン」スタイルから解放されることから UX は向上します。
SPA とは何か? (開発者の目線)
いにしえの昔、SPA は javaアプレット や Adobe Flash などのブラウザプラグインに依存していましたが、現在は JavaScript を用いた実装になっています。2014年にオライリージャパンから「シングルページWebアプリケーション」(ISBN978-4-87311-673-0)という書籍が発行され、フロント (ブラウザ) 側だけでなく、バックエンド (サーバ) 側の Webサーバ (Node.js) とデータベース (MongpDB) もすべて JavaScript で実装するというアプローチが紹介されました。
しかしながら JavaScript でアプリを実装するというのは、小規模なものであれば実現可能でも、多機能かつ大規模なアプリ開発では非現実的と考えていました(個人の感想です)。しかし次のスタックを用いることによって、高品質なアプリをカジュアルに、なおかつ楽しんで開発できるようになりました。
重大な課題
弊社における SPA 開発には2点、課題があると思っています。
1点目、Google検索でヒットしない
SPA は JavaScript なので Google のクローラーは Webページとして認識してくれません。検索結果に反映されないので、SEO は別途検討する必要があります。
SPA でありながら、Google のクローラーにも評価してもらうには、Nextjs などを用いた、サーバサイドでの静的ページの生成が必要になります。Vercel社をはじめ、いくつかサービスがありますので SEO が重要な場合はこれらを選択することになります。
2点目、ブラウザの履歴が機能しない
SPA はなにも考慮していないと、ブラウザのリロードボタンを押すと、最初の状態に画面が戻ってしまいます。この動作がユーザに期待に反する場合には対応が必要になります。
解決策に複数ありますが、1点目の課題と同様に、Nextjs などを用いて、サイトURLが各アプリの画面を指すようにすることで、ユーザが期待する動作にすることが可能になります。
React + redux tool kit + TypeScript による開発
React
Facebook (現在 Meta) 社がつくってくれた JavaScript ライブラリで、2013年にオープンソースになりました。
- コンポーネント単位での実装
- とかく複雑になりがちな UI 実装を、独立したコンポーネント単位に分割することができるようになり、地方分権を容易にしてくれた
- コンポーネントでは JSX という簡易 JavaScript 構文を使用することができ、複雑な JavaScript と同等の実装を HTMLタグのように簡潔に記述できるようになった
- React コンポーネントは JavaScript で実装することも可能だが、JSX で実装することによりプログラムの可読性が大きく向上する
- フック (Hooks) の採用
- フックとは何か? は後述
- React には組み込みのフックが用意されており、定番の処理を React らしく実装できる
- サードパーティが提供するフックが多く存在するので、難しい実装はこれらに任せることが可能になる
- カスタムフックの実装も容易でかつ実用性が高い
- 仮想 DOM の採用
- React は 仮想 DOM を更新し、実際の (ブラウザの) DOM に対しては仮想 DOM との差分のみを反映することにより、効率のよい画面描画を実現している
フック (Hooks) とは何か?
フックは Reactコンポーネント をクラスを使わないで実装するための機能で、React 16.8 (2019年公開) より利用できるようになった。React コンポーネントの実装には、クラスコンポーネントと関数コンポーネントという2つの実装方式がある。Reactコンポーネントは、関数コンポーネント + フック を採用する方が容易でかつ簡潔に実装できる。
redux tool kit (RTK)
とても有用なライブラリであるが「アプリケーションの状態管理のためのライブラリ」というように簡単に紹介されてしまうことが多い。Reactフック を用いた実装になっており、2019年に公開されている。
- state (ある状態、ステータス) を保持できる (ex. ログイン済、未など)
- 各コンポーネントは useSelectorフック を使って state の値を参照できる
- 各コンポーネントは useDispatchフック を使って state の値を更新できる
- state の値は Redux Dev Tools でブラウザからいつでも参照することができる
RTK のおかげで複雑なデータ構造を持つ大規模アプリケーションであっても、データの見通しが悪くなることなく、快適かつ安全に開発を進めることができる。
TypeScript
TypeScript は、JavaScript の型定義を厳密にする「型付け」に特化した JavaScript のスーパーセット (JavaScript を拡張した言語) である。
TypeScript を用いることによって、コーディングにかかる工数は若干増加するものの、コーディングアップ時の品質が向上するので、トータルでの導入効果は高く「使わない手はない」環境といえる。ちなみに Web 上で見かけるサンプルコードも、TypeScript のものが圧倒的に品質が高い。
TypeScript は Microsoft社が開発した言語であるためか、VSCode との相性が良く、両者を採用することで最適な開発者体験が得られている。
開発環境イメージ
- 開発者のローカル環境に、プロジェクトごとの Dockerコンテナ を立てて開発している
- イメージは node:current-slim という node.js 公式イメージを使用している
- docker-compose.yml は後述
- プログラムコードは docker volume を使ってコンテナからローカルディスクをマウントした領域に配置している
- このマウントした領域のフォルダをポイントして VSCode からコードを編集している
- このマウントした領域は GitHub でホストされているリモートリポジトリになっている
ex. docker-compose.yml
$ pwd
/Users/r/Docker/unrwebpage
$ cat docker-compose.yml
version: '3'
services:
node:
image: node:current-slim
container_name: 'unr_webpage'
hostname: 'unr_webpage'
ports:
- '3001:3001'
stdin_open: true
tty: true
working_dir: '/usr/src/app'
volumes:
- ./app:/usr/src/app
networks:
- dumynet
networks:
dumynet:
external: true
$
開発環境の構築手順
- Mac (Intel chip) での開発に必要なツールのインストール
- Docker container を用いた React + Redux Toolkit + TypeScript Webフロントエンド開発環境の構築
開発スタイル
現在は以下のスタイルで開発しています
- slice をつくる
- コンポーネントをつくる
- テストする
まず slice をつくる
コンポーネントより先に slice をつくります。
slice とは state と、以下に示す reducer を用途 (ex. ログイン機能) ごとにまとめたものになります。
slice をつくることにより、以下が明確になります。
- slice の名称 (ex. auth)
- state の構造
- state の名称やデータ形を interface として定義することが多い (ex. ユーザID, ロール, displayName, アバター画像のURL など)
- state の初期値
- reducer の定義
- reducer とは コンポーネントから受け取るデータと、state を更新する論理を action (ex. login) として定義したもの
- コンポーネントは useDispatchフック を使って action を呼び出す
- コンポーネントから参照可能な state の定義
- コンポーネントは useSelectフック を使って state を参照する
現実の開発スタイル
slice からつくって、次にコンポーネントをつくり、結合してテストする、というスタイルは、小人数で開発する際のスタイルだと思っています。現実には slice で定義されるデータ構造と action をドキュメント等に定義し、この定義に従って、
- 同時に開発する
- slice はダミーコンポーネント (弊社で dumymain と呼んでいるもの) と結合してテストする
- コンポーネントはモックと結合してテストする
- 単体でテスト済みの slice とコンポーネントを結合してテストする
という流れになるんだと思っています。