Tonbly開発日記 #2 -アーキテクチャやライブラリなどの紹介-
はじめに
絶賛開発停滞中のTonblyというQiitaクローン的なアプリについての記事第二弾。
github.com
前回の記事では開発の背景について記したが、今回はTonblyの構成について紹介する。
尚、文体が前回とまるで異なるのはキャラ付けを模索しているからだ。
アーキテクチャ
以下のような構成になっている。
(Draw.ioで作成した画像が意外と荒くて残念...)
Reactを使ったSPAで、特徴はHasuraを利用してGraphQLによるデータのやり取りを行っているところだろう。
このような構成になった背景としては、以下のテーマで何か開発してみたい、と常々思っていたという部分がある。
- できる限りサーバーサイドの処理を書きたくない
- GraphQLを使ってみたい
- No Redux構成を模索したい
1. できる限りサーバーサイドの処理を書きたくない
以前GrapeCityのイベントに登壇した際に紹介したのだが、一昨年〜昨年にかけて以下の構成でFlowlessというアサイン管理アプリを開発していた。
- フロントエンド
- TypeScript
- React
- Redux
- Redux-Saga
- バックエンド
- Scala
- Scalatra
- Skinny ORM
- PostgreSQL
アサイン管理アプリというのは、どの開発プロジェクトにいつからいつまで誰を参画させるか、という計画を管理するためのものである。
少なくともIT系の企業であればどこでも同じようなことをやっていると思うが、ひとまずWBSのようなものだと思ってもらえればよい。
アプリのUIはこのような感じだ。
Flowlessについてはほとんど触れていないが、興味があればスライドも併せて参照されたい。
そして、Flowlessを開発した際に感じたのが、「たいした処理もないのにサーバーサイドとのやり取りを書くのがとてつもなく面倒」ということだ。
UIを起点として開発していたので、
- 画面にこういう機能を追加したいからこういうデータが欲しい
- そのためにテーブルを作り
- 必要があればViewを作り
- マッピングするScalaのクラスを作り
- Scalatraのコントローラーを作り
- クライアント側で受け取るためのInterfaceを作り
- Fetchする処理を書き
- ReduxのActionやSelectorを作り
...といった感じで、ちょっとしたデータ取得ロジックを作るだけでも相当な手間がかかってしまっていた。
これはさすがにしんどいということで、次に何か作る際はGraphQLあたりを使ってうまく改善できないか、と考えていたのだが、GraphQLはGraphQLでただのクエリ言語であるので結局Resolverなどは自分で書く必要がある。
これでは仮に手間が減っても面倒なのは変わらないのでは...と悩んでいたときに見つけたのが今回採用したHasuraである。
Hasuraを簡単に表現すると、「GraphQLで実装が必要な面倒な部分を全て隠蔽してくれるラッパー」という感じだろうか。
Hasuraについては改めて記事を書きたいと思うが、結果的にHasuraの採用は大正解だった。
後述のApollo、graphql-codegenと組み合わせることにより、
- Hasuraの管理画面(Web)でテーブルを作る
- React上でGraphQLを使って欲しいデータを宣言する
なんとこれだけで必要なコードが全て手に入るようになるのだ。
これは本当に衝撃的で、(開発者体験の方の)DXもここまで来たか!と驚いてしまった。
Hasuraはサンフランシスコとバンガロールを拠点としたスタートアップで、けっこうな額の資金調達を行ったというニュースも最近見たが、本当にHasuraは素晴らしいので納得である。
余談だが、このDX向上にはReactのHooksも大いに寄与している。
Hooksを考えた(はずだったと思うが間違っていたら申し訳ない)Sebastian Markbåge氏は天才だと思う...。
2. GraphQLを使ってみたい
これはタイトルそのままなので割愛する。
3. No Redux構成を模索したい
私はReduxが好きである。
なぜなら、"Single Source of Truth"という思想と、あの"ガチャガチャした感じ"に惹かれるからだ。
今はReactコアチームに入ってアンチReduxになってしまったが、原作者であるDan Abramov氏のカリスマ性も魅力的だ。
お前は何を言っているんだと思われるかもしれないが、エンジニアだって人間なので所詮そんなものだろう...というのは冗談だが、やはり便利なので使っている。
更に言えばRedux-Sagaも好きだ。
Reduxを更にガチャガチャさせたあの感じと、全然Sagaパターンとは関係ないのにSagaと名付けちゃうところが可愛いのである。
この記事の本旨とは何も関係がないが、「Redux-Sagaは難しい」「たいていのアプリにはRedux-Sagaはオーバーキル」という風潮に対しては常々思うところがある。
Redux-Sagaはむしろシンプルではないか?
意地悪な見方をすれば、「関数を返す関数」という概念を理解する必要があるRedux Thunkの方が難しいとも言えるはずだ。
確かにSagaには様々な機能が含まれているが、それは「伸びしろ」として捉えて欲しいのだ。
単純にFetchするだけであればSagaでもThunkでも簡単に書けるが、さらに高度な機能が必要になったときにアーキテクチャを崩さずに対応できるというメリットがSagaにはある。
前置きが長くなったが、とにかくReduxを気に入っている...がそれはそれとして、やはり他の手段も試して模索していきたいとは思っていた。
(Flowless開発時には存在していなかったので試していないが)Redux Toolkitによってボイラープレートが大幅に改善されたとはいえ、やはりコーディング量やファイル移動が多かったりと辛い部分がある。
(Ducksパターンはスケールしないので規模が大きくなると最終的にファイルは分割することになると思う)
そこでステート管理の代替候補としてあがるのは2020年5月現在ではこんなところだろう。
- 何も使わずにProp Drillingで頑張る
- Context + useContext
- Apollo
- MobX
- Recoil
この中から、今回は今のところProp Drillingで頑張っている。
(Apolloでのローカルステート管理は行っていない)
今後グローバルなStateが必要になってきた場合はApolloかRecoilで実現するつもりだが、現在話題沸騰中のRecoilは"これでいいんだよこれで"感があり期待している。
その他ライブラリなどについて
めぼしいものはこんなところだ。
Material-UI
UIフレームワークはMaterial-UIを採用した。
前述のFlowlessで採用したMicrosoftのFluent UI(旧Office UI Fabric React)も素晴らしかったのだが、もう少し業務アプリっぽくない見た目にしたかったのと、以前は煩雑だったスタイル管理やTypeScriptの型合わせが改善されているようだったので、それを確認するためにもMaterial-UIを選択した。
上記の課題も解決されており結果的には満足しているが、欲を言えばデフォルトのCSS in JSライブラリをJSSではなくEmotionかstyled-componentsにして欲しい。
(JSSの良さが私にはわからないのだ...)
世の中には数多のUIフレームワークが存在するが、メンテされていなかったり、コンポーネントが充実していなかったりでなかなか難しい。
Apollo
自分でfetchを使ってGraphQLリクエストを送るのはさすがにないだろう、ということで定番のApolloを採用した。
Hooksも充実しており、ReactでGraphQLやるならこれでいいんだろう、という感じではあるが、キャッシュ周りの思想だけは共感ができなかった。
Apolloあるあるだと思うが、データ更新時のキャッシュ更新を忘れているせいで画面が更新されない、そのキャッシュ更新処理自体はわりと書くのが面倒、という部分があり、最初は混乱してしまった。
(最終的にキャッシュは無効にした...)
画面の表示速度、UXが命のサイトであればこのような機構が必要なのだろうが、そんなレベルのものを作っているわけではないので邪魔なだけであった。
graphql-codegen
クライアント側からGraphQLを叩くためのコードを生成してくれるツール。
これとTypeScript、Apolloとの組み合わせが凄まじく便利で、GraphQLのquery、mutationを書くと、それをApolloから叩くためのHooksをTypeScriptの型定義含めて自動で生成してくれるのだ。
自動生成コードは全てのquery、mutationに対して個別に生成されるので、Apollo単体のときのようにuseQuery
やuseMutaion
にGraphQL文を渡す必要がない。
文章では伝わりづらいと思うので例を挙げる。
Apollo単体の場合
interface RocketInventory { id: number; model: string; year: number; stock: number; } interface RocketInventoryData { rocketInventory: RocketInventory[]; } interface RocketInventoryVars { year: number; } const GET_ROCKET_INVENTORY = gql` query getRocketInventory($year: Int!) { rocketInventory(year: $year) { id model year stock } } `; export function RocketInventoryList() { const { loading, data } = useQuery<RocketInventoryData, RocketInventoryVars>( GET_ROCKET_INVENTORY, { variables: { year: 2019 } } ); return <div>...</div>; }
// 1. 型定義が不要(graphql-codegenが生成する) // 2. GraphQL文の変数定義が不要 // (graphql-codegenが生成するので使いたくなったときでも大丈夫) gql` query getRocketInventory($year: Int!) { rocketInventory(year: $year) { id model year stock } } `; export function RocketInventoryList() { // そのquery専用のHooksが用意される // (引数や結果の型定義も自動生成&推論される!) const { loading, data } = useGetRocketInventoryQuery({ variables: { year: 2019 }, }); return <div>...</div>; }
記述量が段違いなのがわかると思う。
しかも自動生成なのでGraphQLクエリの内容を変えれば各種型や関数も再生成される。
本当に"欲しいものを書くだけ"である。
TOAST UI Editor for React
Markdownエディタを埋め込むためのReactコンポーネント。
Markdownエディタ、WYSIWYGエディタも色々探したが、これが決定版だと思っている。
機能豊富でたいていのことはでき、カスタマイズする口も用意されているのが良い。
Tonblyは技術者以外の利用も想定しているので、WYSIWYGモードが用意されているのは嬉しい限りだ。
v2にメジャーバージョンアップしてMarkdownパーサーの刷新など様々な改善が施されたようだが、とりあえずバージョンを上げただけでまだ追従できていないのでこれから取り組んでいきたい。
Ky
fetchをラップして使いやすくしたHTTPクライアント。
「そうそうこういうのが欲しいんだよ」という感じのシンプルなAPIなのだが、これはコードを見ていただいた方が早いだろう。
// GET const result = await ky.get("https://example.com").json(); // POST await ky.post('https://example.com', {json: {foo: true}})
サーバー側(Express)ではNode版のGotを使っている。
この手のライブラリはAxiosなど色々あると思うが、これで何も問題が無いし、JavaScriptマスター?のSindre Sorhus氏作ということもありこちらを採用した。
さいごに
だいぶ長い記事になってしまったが、参考になれば幸いである。
Hasuraの詳細など、開発で得た知見はまだまだたくさんあるので小出しにしていければと思う。
(そもそもアプリ自体の開発を進めないと...)
フィードバックや質問があればコメント欄かTwitterで是非。