Slide 1

Slide 1 text

複雑なフォームを継続的に開発していくための 技術選定・設計・実装 2025-05-24 TSKaigi 2025 @izumin5210

Slide 2

Slide 2 text

@izumin5210 © LayerX Inc. whoami LayerX バクラク事業部 (2022-09 -) Platform Engineering 部 Enabling チーム Staff Software Engineer ISUCON14 4位 好きな関数は cva

Slide 3

Slide 3 text

「フォーム」って難しくないですか? 単純だったらいいけど、フィールド数が多かったり、画面が多かったり、 補完やフィールドのバリエーションが多かったり… © LayerX Inc. 3

Slide 4

Slide 4 text

たとえば経費精算フォーム。 裏側のビジネスルールの複雑さ・ペインを利用者からなるべく隠蔽する代わりに、実装でやることは多い。 さらに、事業会社の自社プロダクトは進化し続ける必要があるが、その一方でバグ・デグレを最小限に抑える必要もある © LayerX Inc. 4

Slide 5

Slide 5 text

難しい(複雑な, 大規模な, 成長する, etc.)フォームに、 設計と技術選定で立ち向かう話をします © LayerX Inc. 5

Slide 6

Slide 6 text

© LayerX Inc. フォームが難しく・大変になる例を見る なぜフォームが難しいのか・それに技術選定や型でどう立ち向かうか 6

Slide 7

Slide 7 text

最も単純な実装 © LayerX Inc. useState だけ この時点で将来的に困る気配がする フィールドが増えると useState も増える 1フィールド更新しただけで全体再描画 バリデーションどうする? これ以上複雑にならないなら全然これでもいい 7

Slide 8

Slide 8 text

フォームライブラリで便利に コード例は React Hook Form © LayerX Inc. 基礎的な面倒事は吸収してくれる useForm して を register する いいかんじに再描画を減らす バリデーションの実行と制御もやってくれる フィールドが多少増えた程度なら、 これだけでも戦える ...本当に? 8

Slide 9

Slide 9 text

フォームライブラリで便利に? 見た目の話と振る舞いの話が混ざるのは(特に規模が大きいと)厳しい © LayerX Inc. フィールドが多少増えた程度なら、 これだけでも戦える ...本当に? UI に埋もれるバリデーション記述 見通しづらい, 対応漏れやすい, テストしづらい, ... バリデーションも重要なロジック 9

Slide 10

Slide 10 text

フォームライブラリ + バリデーションスキーマ コード例は React Hook Form + Zod © LayerX Inc. フォームが扱う値の構造と制約を 宣言的に記述できる(させる) 重要なロジックや構造を UI から 独立して扱える バリデーションのついでにフォームのモデリング を開発者に促す 10

Slide 11

Slide 11 text

© LayerX Inc. フォームはそもそもプログラミングとして難しくなりやすい たぶん 入力があり、それをもとに状態が構築され、同期非同期での操作が行われ、最後に出力が得られる… プログラムを複雑にする要素が多い 11

Slide 12

Slide 12 text

© LayerX Inc. 表面的な難しさ(UIが絡むアレコレなど)と、プロダクトがもつ本質的な複雑さ React Hook Form などのフォームライブラリは前者は回収してくれる が、後者は残る。 これをどうやって低減するか 12

Slide 13

Slide 13 text

© LayerX Inc. プロダクトが持つ複雑さを低減するには… まず、この中心の「フォーム(の状態) 」をどうモデリングしていくかが重要 何に対して・どういう操作が行われるか が曖昧だと、あとからどんどん大変になる Zod などのバリデーションスキーマは、この「フォーム」の「形状」に自然と目を向けさせてくれる 13

Slide 14

Slide 14 text

ここからフォームの例を複雑にしていき どうモデルを進化させるかみていきます © LayerX Inc. 14

Slide 15

Slide 15 text

もうちょっと大変にしてみる © LayerX Inc. EC サイトのカートを考える 商品と数量から合計金額が決まる 合計金額も画面に表示する 15

Slide 16

Slide 16 text

もうちょっと大変にしてみる © LayerX Inc. EC サイトのカートを考える 商品と数量から合計金額が決まる 合計金額も画面に表示する 合計金額に制限がある ← New! 16

Slide 17

Slide 17 text

もうちょっと大変にしてみる © LayerX Inc. EC サイトのカートを考える 商品と数量から合計金額が決まる 合計金額も画面に表示する 合計金額に制限がある 小計も ← New! 17

Slide 18

Slide 18 text

フォームライブラリ + バリデーションスキーマの限界 © LayerX Inc. React Hook Form + Zod でいうと、 watch や setValue が増えだしたらアラート 多少ならまだいいが、 useEffect とかと組み合わさり始めるとかなり危険信号 危険なサインの例 useEffect or watch で setValue を手続き的に呼び出すことによるバグ混入リスク イベントハンドラ内であっても、フォーム状態を無秩序に setValue することによる、 重要な書き込みロジックが無視されるリスク モデルの振る舞いをコードに反映できてないサインかも 18

Slide 19

Slide 19 text

フォームライブラリ + バリデーションスキーマの限界 © LayerX Inc. 参考: useEffect + setState は危険信号 そのエフェクトは不要かも – React https://ja.react.dev/learn/you-might-not-need-an-effect 詳解:フロントエンドの状態とリアクティブ (なぜuseEffect()でsetState()がアンチパターンか) https://zenn.dev/layerx/articles/22dd45dc69a57c 19

Slide 20

Slide 20 text

フォームライブラリ + バリデーションスキーマの限界 © LayerX Inc. Zod でモデリングできるのはあくまで 「形状」と「制約」 だけ それ以上の、たとえば今回のような計算・ロジックは組み込みづらい (頑張ればできるが、Zod の宣言的に書ける良さは活きづらい) 20

Slide 21

Slide 21 text

適切なモデルを考える © LayerX Inc. 今回の例(小計・合計)は、 「ある値から別の値を計算し、表示やバリデー ションに使いたい」というもの RHF + Zod の都合に合わせるために、計算結果を状態に書く 単純なプログラムなら「一度状態に持つ」とい うことはしないのでは? 状態にせず値としたい 21

Slide 22

Slide 22 text

適切なモデルを支える技術選定例 © LayerX Inc. オブジェクト指向のようなモデルがフィットし ているなら、MobX が選択肢に上がるかも データの依存を素直に記述でき、 それを効率よく React に伝えてくれる 実行時にデータの依存(今回だと getter が何を読んだか)を追跡 し、更新検知・再描画をしてくれる とはいえ MobX は癖があるし、OOP が React を触るエンジニア にとって扱いやすいかなど、別角度での検討事項は多い 22

Slide 23

Slide 23 text

コードはぱっと見でわかりやすくなった?自分が OOP に慣れてるからかもしれない React Hook Form とは独立したコードになっているのも、関心の分離・テスト観点で嬉しい © LayerX Inc. 23

Slide 24

Slide 24 text

ポイント① モデルは「形状」だけでなく、ロジック・振る舞いも含める © LayerX Inc. Zod だと「形状」 「制約」にとどまることが多い が、実際のプロダクトの複雑なフォームでは、 そこ以外にも重要な概念がある 「合計金額は数量と金額の掛け算」とかはシンプルだが、 現実はもっと難しいはず そういうドメイン上重要なものに名前をつけて、 重要だとわかるようにしたい 24

Slide 25

Slide 25 text

ポイント② フォームにおけるモデルを UI から分離する © LayerX Inc. React Hook Form + Zod だと厳しくなってくるようなフォームでは、 価値の高い・重要なロジックが埋まっていることも多いはず それらを UI から分離し「 (重要な)モデルである」というのを わかりやすく・見通しやすく・テストしやすくしておきたい そこをより重点的にテストする, そこはより注意してレビューする, ... 開発者の認知や行動を切り替える (とはいえ規模や複雑さがそうでもないなら UI に co-locate させるほうがメンテしやすい。要はバランス。 ) AI 向けにもモデルと UI が分離されたほうが動きやすいし、我々ももレビューしやすい…んじゃないかな? 25

Slide 26

Slide 26 text

ポイント③ なるべく状態ではなく、値として扱う © LayerX Inc. 状態は難しい 内部の状態の存在を知っていないと、その外から見た振る舞いを正しく知ることは難しい 状態への書き込みを「適切に(もれなく・ミスなく) 」徹底する難易度は高い 今回の例のように readonly な「別の値から計算される値(Derived Value) 」として 扱うことができれば、扱いは非常に単純 26

Slide 27

Slide 27 text

MobX が常に絶対最強? もちろんそんなことはない © LayerX Inc. 27

Slide 28

Slide 28 text

もうちょっと大変にしてみる © LayerX Inc. EC サイトのカートを考える 商品と数量から合計金額が決まる 合計金額も画面に表示する 合計金額に制限がある ドル決済のときはレートを API から取得 する ← New! 28

Slide 29

Slide 29 text

値に非同期処理が絡むなど、依存がさらに複雑なケース © LayerX Inc. MobX でも解くことはできるが、MobX は依存追跡をランタイムで実現しているため 複雑すぎる依存ではデバッグがつらかったりする この発表では詳しくは触れません。 技術にはいろんな特性があるんだなあくらいで捉えておいてもらえると。 ほかの技術だと、たとえば jotai などは複雑な依存や非同期の依存でもうまく扱える 29

Slide 30

Slide 30 text

jotai の簡単な例 © LayerX Inc. jotai は atom という単位で状態を管理する atom 関数は読み書き可能な状態のほか、 Read only な atom をつくることで、 他の値に依存した値(Derived Value)を 表現可能 この get(anAtom) の形でデータの依存グラフを描く 30

Slide 31

Slide 31 text

jotai で非同期の Derived Value を扱う例 © LayerX Inc. Read only atom は非同期の値を返す こともできる 非同期 atom もだいたい通常の atom と同じように扱える 解説サボるために実装を簡略化しています。実際の利用方法は以下の記事を参照してください 「Jotai v2を使いこなすために実は必須級な“async sometimes”パターンの解説」 by uhyo https://zenn.dev/uhyo/articles/jotai-v2-async-sometimes 31

Slide 32

Slide 32 text

jotai は atom でデータの依存グラフを描く 青が状態, オレンジが Derived Value get(anAtom) がグラフのエッジ © LayerX Inc. 32

Slide 33

Slide 33 text

jotai によるモデリング © LayerX Inc. MobX のときに挙げたポイントは jotai もクリアしている ポイント① モデルは「形状」だけでなく、ロジック・振る舞いも含める ポイント② フォームにおけるモデルを UI から分離する ポイント③ なるべく状態ではなく、値として扱う やはり「ロジックを」 「UI と分離して」 「値でいいものは値として扱う」ことで 余計な複雑性を抑えたモデリングができる 33

Slide 34

Slide 34 text

jotai によるモデリング - 非同期処理 © LayerX Inc. 非同期処理を含む atom への依存をそこまで特別扱いせず、 おなじく atom の形で書けるのが強力 前述のレートの例のような「ユーザの入力値を API に渡し、 そのレスポンスを別の値やバリデーションに利用」は稀によくあるケース 非同期処理のために処理の流れを捻じ曲げたり、余計な中間状態を生やしたりは不要。 いつもの async function で OK。 React にマウントするときにまだ未解決なら勝手に Suspend してくれる 便利! …だけど、同じ useAtomValue が atom によって suspend したりしなかったりするのは 逆に認知負荷たかいかも…? ともちょっと思う。むずかしい。 34

Slide 35

Slide 35 text

jotai によるモデリング - テスト © LayerX Inc. コアは React にすら依存してないので、テストも書きやすい API に依存してる場合はモックに差しか可能にするか、 MSW で外側からモックするか 35

Slide 36

Slide 36 text

「フォームのモデリング」という観点から見ると解いてる課題は近い データの依存を Derived Value として表現することで無理のないコードとなり、 余計な複雑性を下げることにつながっている ここでいう 無理のあるコード とは、 状態の更新にフックして別の状態の更新を頑張っていたコード MobX (一見して)普通のクラスで書ける / 暗黙で依存追跡する / ... jotai 独自記法・概念がある / `get(anAtom)` の形で依存を明示する / ... © LayerX Inc. 36

Slide 37

Slide 37 text

MobX と jotai - どっちにするか © LayerX Inc. 依存追跡の方法が大きく違う MobX はランタイムで暗黙的に呼び出しを記憶 jotai は get(anAtom) の呼び出しをリアクティブな(更新されうる)値の呼び出しとする get(anAtom) の記述など、独自 DSL 感のある jotai か 普通の OOP / クラスっぽくかける代わりに、良くも悪くも暗黙的な挙動が多い MobX か 37

Slide 38

Slide 38 text

MobX or jotai から常に選べばいい? もちろんそんなことはない © LayerX Inc. 38

Slide 39

Slide 39 text

問題によって適するモデル・設計は変わる モデル・設計が変われば適する技術も変わる © LayerX Inc. 今回は偶然 Derived Value / データの依存グラフ / データの流れ などに着目した設計が ハマる問題(プロダクト特性)だった 問題(プロダクトや機能)によって適する回答は変わりうる ウィザードっぽい複数画面からなるフォームであれば、 XState のような状態遷移に着目した技術が適するかも Undo 機能みたいなものが必要なら Redux / Zustand などが適するかも 単にフィールドが多いだけのフォームなら、 Zod で「形状」をモデリングするだけで事足りるかも プロダクトの複雑さはどこから来ているかを見極める 39

Slide 40

Slide 40 text

「フォーム」にこだわりすぎない © LayerX Inc. 今回の例も、フォームの問題にフォーム専用でない 状態管理ライブラリを持ち込んでいる 表面的な要素に囚われすぎず、 「いまどんな問題に向き合ってるんだろう」を一段上から考えてみる e.g. 「相関バリデーションが難しいことに困ってたんだけど、実はデータの依存関係をうまくモデリングできて いないだけだな」 40

Slide 41

Slide 41 text

フォームライブラリを使わないことで失うものもある © LayerX Inc. これもトレードオフのひとつ たとえば React Hook Form であれば、 各フィールドの dirty check, バリデーション発火タイミング制御, etc. なくなったら面倒ではあるが、 「本質的な複雑さ」というよりは「シンプルに面倒」な類のものも多い なので最小限のユーティリティを実装してなんとかするという手もある 41

Slide 42

Slide 42 text

複数の技術・設計を組み合わせるパターンもありうる © LayerX Inc. たとえば以下の2つは両立しうる 「バリデーション対象フィールドが多いので Zod で宣言的 に記述したい」 「依存関係を持つフィールドが多いので jotai を使いたい」 Zod のバリデーション結果も Derived Value にしてしまえ ばいい jotai と Zod を組み合わせると、 superRefine などは不要になるはず 42

Slide 43

Slide 43 text

最初から100点の答えを見つけに行く必要はない © LayerX Inc. 特に PMF 前のプロダクトや機能のプロトタイプフェーズなどでは、 あるべきの形が見えてないことも多い そんな状態で未来を先読みするのは難しいし打率も低い なので、そこそこいい感じになるところから始めよう やはりフォームライブラリ(e.g. React Hook Form)とバリデーションスキーマ(e.g. Zod)の 組み合わせから入るのはコスパがいい まあこの2つで事足りるケースのほうが多いだろうし… 43

Slide 44

Slide 44 text

設計・選定の先 - 継続的に開発していくために © LayerX Inc. 44

Slide 45

Slide 45 text

「変更影響を受けた(壊れた)こと」に早く気付けるコードを書く © LayerX Inc. 気づくスピード: 型 > Lint > Unit Test > ... > エンドユーザ 特に規模が大きい/複雑/パターン数が多い部品では なるべく型やテストが最初に壊れるようにしておく 現状の仕様をモデル+テストで表現できているのであれば 型で将来的な拡張への耐性を上げることが有効 45

Slide 46

Slide 46 text

早く壊れる型の例. などなど… © LayerX Inc. enum, union で取りうる値の幅が増えてハンドル漏れが起きるケース foo satisfies never のような Exhaustiveness checking を常に書く 渡せるパラメタが増えたが、呼び出し側でその対応が漏れるケース アプリケーションコードでは optional は使わない(不要なら null などを明示的に渡させる) 変更が伝播し、意図せず Atom の型(関数の返り値)が変わって壊れる 関数の返り値型は推論に頼らず明示する 46

Slide 47

Slide 47 text

限界を迎えるサインには気をつける © LayerX Inc. 設計が限界を迎える(プロダクトの複雑さを受け入れきれなくなる)と、 機能開発に時間かかったりバグが増えたりする どれだけ丁寧に進めても、プロダクトの進化を読みきれず設計が耐えきれなくなること はありうる さっきの話と同じで、完璧な先読みはできない 限界を迎える兆候にはやめに気づいて手を打ち始められると Good React Hook Form でいうと、 watch や setValue が増えだしたらアラート 47

Slide 48

Slide 48 text

おわり © LayerX Inc. フォームは難しい 「ユーザに変更される状態を持つ UI」の難しさと、「プロダクト由来の難しさ」の両方に直面する 問題領域に適したモデリング・適した技術選定をすることで、 後者の複雑さメンテナンス性は改善できるはず たとえばデータの依存が複雑なのであれば、Derived Value をうまく扱える MobX や jotai は有効 最初から最適な選択は困難なので、限界を迎えるサインに早めに気付けるようにしたい 48