これは SmartHR Advent Calendar 2020 の4日目に書かれた記事です。今は 12月4日の42時10分なので、ギリギリ滑り込んだ形になってしまいましたね。
React と自由
SmartHR で開発している様々なプロダクトはその大半 1 がフロントエンドに React を採用している。僕も Twitter で「React が好きだ!TypeScript 最高だ!」と叫んでいたら「弊社 React + TypeScript ですよ」というスカウトをいただいて転職に至ったという経緯があって、それぐらい全社的に React をやっていくぞという意志の統一が果たされている。
とはいえ React というのはフレームワークではなく、あくまでも JSX という記法と各種関数のバインディングを通じて宣言的な UI を構築する機能を持ったライブラリに過ぎないので、アプリケーション全体のデータフローを規定したり、ディレクトリ構造を整えたりというのは開発者がやる必要がある。このへんは弊社がバックエンドに採用している Ruby on Rails が持つ CoC という思想とは真逆の性質を持つかもしれない。
React は本当に自由な使い方ができるツールなので、コンポーネントの分割粒度から宣言場所、 import
/ export
の方針、state 管理、 Hooks の使い方、コードのフォーマット、プロジェクトのディレクトリ構造に至るまで、現場のエンジニアが「あ〜これはこうしたろ」と思いついたものが大体そのまま違和感なくコードベースに吸い込まれてしまいがちであると言える。というか、まあ今まで React を採用した現場では例外なくそれが起きていたし、弊社でも起きている。
僕は今チームのフロントエンドをリードする…というか、大きいものから小さいものまで意思決定に口を挟める立場をいただいているので、今年はこの問題について結構考えた1年だったと思う。
僕の担当プロダクトは社内でも歴史が長く、メンバー入れ替えの都合上フロントエンド不在の時期が存在していたりもするので、他プロダクトと比べても特に考えなければならないことが多かったりはした。バックエンドエンジニアがフロントエンドにコミットすることがあったり (逆も当然ある)、スキルに差があるメンバーがいたり、そもそも歴史的な経緯でマルチアーキテクチャっぽい感じになっていたりする箇所がある中で、どうやってフロントエンドのコードベースに秩序をもたらすか…というかどの辺を落としどころにするか、みたいなことを考えていた。
まだまだ進行中の話なのでお役立ち情報とはいかないけれど、まあ似たような悩みを抱えてる人に対してこういう考え方があるよっていう話にできればいいなと思う。
かっこよさと運用性
一人で実装していると、時々すごく「かっこいい」実装を思いつくことがある。 props
から関数をカリー化してイベントハンドラを自動生成するとか、 reduce
と spread 演算子を組み合わせて複雑な構造の state 更新を一発で実現するとか、データの取得から変形・挿入までを HOC で実現しちゃうとか、そういうのは正直テンションが上がる。コードレビューで「すげー!」みたいなことを言ってもらえるのは大変に気持ちがいい。
ただ、プログラマは…というか少なくとも僕は気持ちがよくなるためにコードを書いているわけではない。僕のお給料を生んでいるのはプロダクトだし、プロダクトはお客様に使っていただくことで初めてお金になるわけで、使っていただくためには絶えずメンテナンスや機能追加をしなければならず、それを僕一人で実現することは絶対にできない。要するに僕しか読めないコードを書きつづければ僕は文無しになる。「すげー!」と言われている時点でその実装はレビュアーにとって理解の範疇を超えているわけで、そんなものが果たして正しくメンテナンスされるのか…もちろん無条件に否という訳ではないけれど、チーム状況と照らし合わせて一考する余地はあるんじゃないかと思う。
僕は去年あたりから機会を見つけては Go を書いているけど、こうした「かっこよさ」を完膚なきまでに叩き潰すほどの非常に徹底した、狂気じみているとさえ思えるほどシンプルに削ぎ落とされた言語仕様 2 を持っていて、それはそれで気持ちがいい。どうやってもかっこよく書けない言語なら誰もかっこよく書けないストレスを感じないし、そもそも大体の局面において1つの問題を解決する方法は1つしか存在しないので「迷う」ことが非常に少ない。そうやってチーム全員が強制的に似たようなレベルの「ほどよくクソ」みたいなコードを書ける環境は割とアリなんじゃないかと思う (とはいえ創造性とか高水準言語との実装コスト差という観点で賛否両論があるのは知ってるし、全員に対してオススメしたいとかは決してない)。
僕がフロントエンドのコードをどうしたいですかと聞かれた時に「ほどよくクソ」ではまずいんだけど、それでも一定程度そういう鮮やかで効率の良い実装を割り切ってでも無秩序を回避すること、理解できるコードベースを保つこと、という方向に振っていきたいなと思うようになっていて、その文脈で出てきたのが表題の「素朴」というワードだった。別にかっこよくなくていいから、思想に一貫性があってカイゼンしやすいものを目指していきたいですね〜という話をメンバーの前でして、「いいっすね」ということになった気がする。
素朴さの実現
まだ途上なので大したことはできていないんだけど、やったこととして、僕は今あの悪名高い「コーディング規約」みたいなものを作っていた。まあ Linter / Formatter で制御できない部分に干渉するべきではない (= 従わせたいならツールを設定するしかない) というのは非常にごもっともなんだけど、大雑把にルールを定めて運用するには些かコストが高い。新たにプロジェクトを立ち上げるならともかく、既に巨大なコードベースが築かれている以上、 .eslintrc
に一行追加するのは「既にアウト」となる箇所に対する膨大な修正タスクの誕生を意味する。それで「やっぱり厳しすぎるからやめた」という決定はいくらなんでもつらすぎる。
かと言って、思想を何も残さないのはまずい。コードレビューで「ここはこう書けるんで、こうしてほしいです」というのをいちいち背景込みで説明するのは非常に面倒だし、レビューを受ける方もお気持ちであれこれ指摘されるストレスというのはそれなりにある。書く前に知りたかったわ、と言われるのが当然の顛末だし、第一「こう書ける」という指摘に対して修正をコミットしてもらえるかはメンバーの人間性とか関係値に依存してしまう。そういう ( [IMO]
的な提案とか気づきの共有) コミュニケーション自体はいいと思うけど、それを PR のマージ可否に関わる規約と同義に振り回されたらたまったものではない。
ひとまずドキュメント化することで、コードレビューのエビデンスとして使うことはできる。規約は隔週でフロントエンドチームが話し合ってメンテナンスし、更新内容は全メンバーに逐一共有する。ドキュメントは「策定中」と「決定」という2つのセクションに分けていて、「こうしたらいいんじゃないか」というアイデアと、実際にコードレビューでの指摘対象となる決定事項は明確に分離する。それぞれに what (何をやるか) / why (なぜやるか) / how (どうやるか) を列挙していく。そんなことをやっているうち、割とコードの良し悪しというものは自然と共有されるようになっていた。これで「いけそう」という結論になれば、それは Linter に組み入れていいということだし、そこまでの文脈があれば前述したような苦行を乗り越えることはそこまで難しくないだろうと思う。
具体的なルールの策定・適用について細かい説明は省くけれど、ディレクトリ構造の刷新が非常に面白かったのでその話だけしておく。
僕のチームでは元々 Atomic Design が採用されており、よってコンポーネントの置き場所もその思想に従ったものになっていた。なので、構造は以下のようになる。独自っぽいのは pages
がバックエンドとの疎通を含めたロジックコンテナで、 templates
はプレゼンテーションとしての画面を意味する、という部分ぐらいのもので、あとは割と素直に Atomic になっている。
src/
components/
atoms/
SomeComponent/
index.ts
SomeComponent.tsx
molecules/
organisms/
modules/
templates/
pages/
services/
...
で、まあ「元々」と言った通り既にこのデザイン手法は捨てられてしまったので、こうしたディレクトリ構造は完全に過去の遺物と化していた3。コードベースがデザイン上の思想と紐づかない構造をしているというのは非常に使い勝手が悪いので、僕たちはこの構造を変えてしまおうという決定をしたのだ。
新たなディレクトリ構造として、 Alexis Mangin 氏が How to better organize your React applications? の中で提唱したものをベースにした再起的なディレクトリ構造を採用することにした。詳しい説明はリンクを参照してもらうとして、新たな構造はこういうことになる。
src/
components/
_shared/ ← ページをまたいで使われるやつ
SomeComponent/
SomeComponent.tsx
styles.ts
services.ts
types.ts
index.ts
components/ ← 共通コンポーネントもネストできるぞ
SomeSubComponent/
SomeSubComponent.tsx
index.ts
pages/
Hoge/
Index/
HogeIndex.ts
index.tsx
templates/
Hoge/
Index/
HogeIndex.tsx
styles.ts
services.ts
types.ts
index.ts
components/ ← 特定ページでしか使われんやつ
HogeIndexSubComponent/
HogeIndexSubComponent.tsx
index.ts
まずデメリットに言及しておくと、これは「コンポーネントのサブコンポーネントのサブコンポーネントのサブコンポーネントのサブコンポーネント」みたいなものを許容してしまうので、理論上ディレクトリ階層が無限に深くなる。そのあたりはもう「お前の善意に賭ける」ということになってしまっており、実際にファイルツリーが見づらいという問題が起きてたりはするっぽい (僕はあんまり使わないのでよくわからない)。
とはいえ、これは以前の構造に比べて判断基準が非常に明確といえる。自分が作ったコンポーネントが「module か organism か」という問いは定性的だけど、新構造においては「特定ページに属するか、複数ページにまたがって使用されているか」という明確な境界条件によって配置ディレクトリを判断できる。これはうれしい。
この構造は全てを解決していないし、決して鮮やかでもない。とはいえ「一貫性があり」、「誰にとっても理解できる」ようにすることには成功している。これは僕たちが導き出した素朴な解決策の好例なんじゃないかなと思う。
総括
まず言っておきたいけど、僕は「いかなる局面でも知識的な属人性を持っていたり、高い能力を前提とした仕組みは悪である」というような思想の持ち主ではないし、決して前述したような「かっこいい解法」や、全てのトレードオフを打破するハイリスク・ハイリターンな切り札の存在を否定したい訳でもない。あの人しか理解できない黒魔術実装とか、機能開発をストップしての大規模リファクタリングとか、そういう手を使わなきゃ切り抜けられない非常事態というのは少ないけれども確実にある。本当に問題なのはそれが非常事態であることを忘れてしまうことなんじゃなかろうか。
素朴な解決策を目指すとは、決してベストな選択肢の探究を諦めるわけでも切り捨てるわけでもない。問題を実現できる最低限の水準をチーム全員が共有し、そこを超えた「かっこよさ」に対して自覚的であるための考え方だと思う。
なんだかフワッとした話になってしまった。これからはここであれこれ揉んだルールを Linter / Formatter に落とし込んで運用していく段階に入るわけで、その時になったらまた色々と適用面でより技術的な問題に直面することだろうと思う。その話もどこかで書きたいな〜
おわり。