Next.jsは要件に合わせやすいフレームワーク

吉井健文氏(以下、吉井):Takepepeです。「フロントエンドの複雑に耐えるために実践したこと」というタイトルで、本日発表します。Reactの地続きとして、READYFORのアプリケーションがNext.js化していくところをお話しします。

分離戦略のゴールです。バックエンドとフロントエンドの責務をきちんと分けて、疎結合すること。フロントエンドの部分ですね。時代に即したベストプラクティスが、いろいろ出てきているので、そういったところを追求できるところがゴールです。

これまでモノリスだったRailsのアプリケーションですが、そこのアプリケーションサーバーはAPIサーバーへ、Viewの部分はJS(TypeScript)だけで完結する。そういった姿を目指します。

Next.jsはこのような背景で選ばれるプロダクトとして、筆頭の選択肢となっているんじゃないかと思っています。静的なキャッシュの恩恵であったり、ゼロコンフィグであったり。また、SSRのハイブリッドができたり、けっこう要件に合わせやすいフレームワークです。

複雑だったフロントの表示ロジック

私は今はちょっと業務委託のようなかたちでお手伝いしていて。お誘いを受けたのが2020年の4月ですね。かれこれ10ヶ月ぐらい経ちますが、この期間に2つのNext.jsの立ち上げをお手伝いしました。この2つ目が、来週にリリースぐらいのところに差し掛かっている状況です。

すでに分離戦略が敷かれていたので、私に求められたことは本当に明確で。しっかり作られたREADYFOR Elements。これを利用して、Next.js Appを構築していく。本当にかなりしっかりしたデザインシステムがあったので、本当にもうパパっと組めちゃうような。そういった状況ができているのかと所感で思っていました。

Railsアプリケーションの上に、react_on_railsというかたちでReactのコンポーネントが乗っている。Elementsが仕上がっていて、すでにちょっとNext.js製のPoCがあり、「こういうふうにしたら実現できそうだよね」みたいなところまで見えていたところからスタートしました。

単純にSPA化するといっても、フロントが抱える部分の表示ロジックがかなり複雑になっていて。ソースを見て、これけっこうやばいな、分岐がすげえなと思って。そういったところの複雑さをどうやって乗り越えたか。設計や実装などをメインにお話ししたいと思います。

状態管理ライブラリにはReduxを選出

今回Next.jsに対して、Reduxを使っています。一番はじめに手がけた、実行者SPAです。要は、クラウドファンディングで募るほうのSPAですが、細かいUIがけっこう組み合わさっていて。部分の再描画が頻出そうな要件がけっこうあったため、状態管理ライブラリはしっかりしたものを選びたいというのがあり、迷わずReduxを選びました。

Reduxのよいところですが、算出の値のメモ化が、このReactノードの外側でできる。そういったところで、引っ張られないような再描画に優れています。devtoolsは、ほかのソリューションと比べたら、かなり充実しています。あと最後に、今日この話もちょっとしますが、integration testが実施しやすい。そういったところもあります。

Reduxは使い込んでいない人からすると「秩序が生まれやすいライブラリだよね」という、ふわっとしたイメージがあるかと思います。しかし、使い方次第で無秩序になりやすいものです。採用の際には、けっこうちゃんとしたガイドラインが必要です。

ファイル構成はModuleごとに管理

ファイル構成です。“Re-ducksパターン”とググるとけっこう出てくると思うので、知っている方もいるかと思いますが、これを採用しています。関心の範囲を境界として、Moduleごとに管理しています。Moduleと呼んでいるのは、ここの部分です。このreduxディレクトリの中にある、ここの1つの単位です。

Moduleの内訳は、こういったかたちです。中にstateのファイルがあり、それを軸にしてreducerとaction、selectorとredux-thunkのファイルがある。

このModule同士ですが、Reduxの1つの強みとして、こういった参照の関係をもてます。

どういうことかというと、例えば「A」で発行された「A」のためのAction。そういったActionであっても、「C」や「B」でも購読できる、そういった参照の関係です。

ただ、この参照関係、しっかり秩序を保たないとスパゲティコードが簡単に生まれてしまいます。ReducerやSelectorなどで参照のねじれは、けっこう頻繁に発生してしまいます。

この参照秩序は公式のガイドラインがいろいろありますが、その中でも「Basic State Shape」という項目があり、そこの「Module prefix」を参考にして明文化しました。

「Module prefixとはいったい何なのでしょう?」というところですが、けっこう簡単な話です。Module単位で区切っていますが、接頭辞をつけると。頭に「Api**」「App**」「UI**」というような、接頭辞をつけることです。このprefixがあることによって「参照権限をこうします」というルールをはじめに敷きました。

この「Api」Moduleですが、APIのレスポンスを取得したり保持したりするだけです。そのため、「Api」以外のところのModuleで、非同期の処理が発生しない状況です。APIのpathと1対1で設けて、そのほかのModule同士の参照は発生しないガイドラインを敷きました。

その下に「App」という接頭辞がついたModuleがきます。アプリケーションで横断的に利用するものの、「Api」と「App」のModuleしか参照できない値を「App」で管理します。

「UI」は、上でしか参照できない。「UI」というのは、よくあるヘッダーやモーダルなど、横断的に利用するModuleを指しています。

最後に「Page」です。上で練り上げられたActionなどを参照できます。PageのComponentと対になるようなModuleです。

こういった、下流のModuleが上流のModuleを参照するような命名規則による法則です。こういった参照秩序を設けました。

ガイドラインの目的は責務の所在地を明確にすること

このガイドラインの目的ですが、参照権限を限定することで、責務の所在を明確にします。

責務の所在地が明確であれば、設計の属人化が最小限になります。また、必要な責務のみが下流に降りてきます。先ほども言ったように、「これ、横断的な値ですよね?」と話になったときには、PageのところからAppにマイグレーションしたり、APIの話がほかに降りてこないような。そういったことです。

特定ページ専用のこういったModuleですが、このページの処理のみに専念できる。そのため、やはり一番複雑になるのは、ここのModuleです。

このガイドラインのよいところは、実装中に異常な参照のねじれにすぐに気づけること。また、コンテキストの理解がたやすくて、レビューの負荷が下がります。この命名規則だけでけっこう開発体験がよくなるので、検討するといいかなと思っています。

後発のプロジェクトにはBDDを採用

では次に、実践BDD(ビヘイビア駆動開発)というところで、Cypressについてお話しします。

冒頭でお話ししたとおり、表示分岐のロジックがかなり複雑になることが想定されていたため、この部分のintegrationテストは不可欠だと感じていました。そのため、後発のプロジェクトに関しては、立ち上げの初期からこのBDDを採用しました。

BDDっていったい何だというところで、ちょっとWikipediaを引用します。「これから作成しようとするプログラムに対して期待される『振る舞い』と『制約の条件』、つまり『要求仕様』に近いかたちで、自然言語を併記しながらテストコードを記述する」と。

実践あたって採用したのがCypressです。Cypressは動作が軽快でBDDに適したフレームワークです。

ReduxとCypressは相性がよく、Action dispatchで特定の条件の再現が可能です。こういったところで、Reduxの特性がけっこう活きます。

例えば、込み入った分岐条件でのみ現れる画面など。「どの状態やActionがあれば、この画面って再現できるんだっけ?」というところが、すぐにテストコードに落ちてきます。

特定制約時の再現が容易なため、テストファイルを細分化できます。表示を即座に確認できるため、ドキュメントとしても役に立つことがあります。この書きやすさが、ファイルを分割するところでかなり役立ってくるところがあって。それは普通のModuleも同じようなことです。

ワークフローの工夫でテストは書きやすくなる

OpenAPI定義によるMockサーバーを、Cypressのintegrationテストにも活用しました。テストの書きやすい環境は、ライブラリやツール選択で明らかに差が出てきます。

また、これはあまり聞かない話かと思いますが、ワークフローの工夫もけっこう大切かなと思っています。タスク分解の段階で、書きやすいものがけっこう出てくるかなと思っています。タスクを本当に小さく細分化することで、テストはけっこう書きやすくなると思っています。

具体的な話で言うと、チケットを発行して、そのチケットの番号とともに要求仕様の空のテストを、一緒にコミットしてしまう。

あとから実装したときに、その仕様を満たしているときにスキップを外して。テストも書く。そういったことです。プルリクに概要を詳らかに書かなくても、どういった内容の機能が追加されたのかが伝わる状態が理想かと思っています。

ライブラリ側もテストツール群も、けっこう不得意や得意なところ、オーバースペックがあると思います。そのため、ライブラリの選択の際は、テスト観点を含め、バランスをとりながら選ぶ必要があります。

振り返りと今後について

振り返りと今後についてです。READYFOR Elementsの再利用のところは、本当にうまくいっていました。もともと造りがしっかりしていたため、スタイルを書くようなこともありませんでした。しかし、一部検討余地があったところを紹介したいと思います。

例えば、この非制御Component+Formの前提です。そういったpropsの設計が、READYFOR Elementsの中に紛れていました。Form都合のpropsを剥がす、そういったリファクタリングに、想定以上の工数がかかりました。

また、制御Componentと比べると、非制御ComponentってReduxとちょっと相性がよくないんです。

とはいえ、こういった段階を踏んでいるため、ReactOnRailsで利用していた箇所としては非制御Componentのほうがベストでした。こういった背景があるので、移行に必要なコストで間違いありません。

READYFOR Elementsのような横断的なデザインシステムを構築するのであれば、制御Componentか非制御Componentかを選択できるようなI/Fを、初期設定からComponentに盛り込む余地があったんじゃないかとはちょっと思っています。

今後もReduxを使い続けるか?

このReduxをこれからも使い続けるかです。公式にuseContextSelectorのようなAPIが入ろうとしているところですが、このReduxの代替となるものもけっこう出てきているので、状態管理ライブラリの選定ってけっこう悩みどころです。

これからReduxを使い続けるかは、都度検討かと思っています。READYFORは機能単位でNext.jsを分割しているため、状態管理も機能要件に合わせて選択できるのかなと思っています。

Reduxのテストの優位性は挙げたとおりですが、フロントに複雑な表示ロジックを置く場合は、導入のメリットがかなり多いかと思っています。

表示ロジックがフロントに寄る場合は採用するし、サーバーに寄る場合は不採用。それがいい切り分けかと思っています。

READYFORのフロントエンドの分離戦略は、スタートを切ったばかりです。またいつか、この続きを共有できることを楽しみにしていますということで、私の発表は以上です。ご清聴ありがとうございました。