Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AI Agent に“攻略本”を渡したら、150フォームの移行が回り始めた話/登壇資料(高橋 悟生)

AI Agent に“攻略本”を渡したら、150フォームの移行が回り始めた話/登壇資料(高橋 悟生)

TSKaigi 2026
2026年5月22日(金)〜23日(土)
https://2026.tskaigi.org/

Avatar for Hacobu

Hacobu PRO

May 20, 2026

More Decks by Hacobu

Other Decks in Technology

Transcript

  1. Copyright Hacobu, Inc. 2 自己紹介 高橋 悟生(たかはし ごう) 25歳 株式会社Hacobu

    テクノロジー本部 ソフトウェアエンジニア 24卒で株式会社BuySell Technologiesに入社し、複数のプロダクトにおける 技術検証・設計・開発を経験。 現在は株式会社Hacobuにてソフトウェアエンジニアとして、 トラック予約受付サービス「MOVO Berth」の設計・開発に携わっている。 takahashigo
  2. Copyright Hacobu, Inc. 7 01 課題 ── そのフォーム数、150 約 150

    フォームを RHF + Zod へ ── 普通にやれば 2 年仕事 課題 業務アプリに散らばる 約 150 のフォーム 新規 / 編集 / 検索 / モーダル / 配列 / 日付範囲 業務アプリ・複数ロール(管理会社/••/••/••)が絡む 現状:React Final Form ベース render props に UI と validation が同居し、フォームごとに微妙に形 が違う AI に丸投げでは、詰む 移行先に求めたもの 「型で揃う移行先」 = React Hook Form + Zod ① 型を握りたい z.input / z.output で UI と API の値の差を型で表現 ② 単一 schema で検証と型を統一 Zod から runtime 検証と TypeScript 型を同時に生成 ③ uncontrolled で再レンダリング最小化 RHF は購読ベースで部分更新、業務フォームの規模に強い ④ 将来性 / メンテナンス継続性 react-final-form ^6.5.9 / React 19 対応への懸念 Copyright Hacobu, Inc. 7
  3. Copyright Hacobu, Inc. 8 02 AI 丸投げ問題 → "攻略本" としての

    Agent Skills プロンプトより、Skill。判断基準をコード化する。 「このフォームを RHF 化して」と言うだけでは、150 個の判断がブレる ── 結果、150 個の負債。 defaultValues はどこで作る? submit に渡す型は input ? output ? setValue したとき dirty は立つ? モーダルを閉じたら reset する? 子は props か context か? useFieldArray key は index? Solution ── 攻略本 = Agent Skills(YAML frontmatter + Markdown のフォルダ)に判断基準を閉じ込める skills/rhf-migration/ ├─ SKILL.md 判断分岐とステップ ├─ PATTERNS.md 検索/モーダル/配列のパターン別実装 ├─ COMPONENTS.md RHF コンポーネントと Zod schema 一覧 └─ REFERENCE.md 参考 PR と実装上のハマりどころ Skill = プロンプト集ではなく "移行レビュー基準" description にトリガーワードを並べると、エンジニア の自然なプロンプトに skill が当たる。 SKILL.md は Claude Code 発の形式で、Cursor など複数 Agent でも読める。 大きくなったら 4 ファイルに分割。 Copyright Hacobu, Inc. 8
  4. Copyright Hacobu, Inc. 9 03 1 フォーム = 1 ファイル

    ── 唯一の正解を決める 1 フォーム = 1 スキーマ + Input/Output + Factory // editProfileForm.ts // 1. Schema ── UI 入力値と検証ルールを 1 箇所に集約 export const editProfileFormSchema = z.object({ profile: z.object({ name: lengthSchema(1, 255), email: optionalEmailSchema.transform(nullToUndefined), phoneNumber: optionalPhoneNumberSchema.transform(nullToUndefined), }), }); // 2. 型 ── 入力中 (Input) と送信時 (Output) を別の型として取り出す export type InputEditProfileForm = z.input<typeof editProfileFormSchema>; export type OutputEditProfileForm = z.output<typeof editProfileFormSchema>; // 3. マッパー ── API レスポンス → InputForm export const toInputEditProfileForm = (res: ProfileResponse) => ({ profile: { name: res.profile.name.trim() /* ... */ }, }); // 4. Form API ── このフォーム専用の useForm / FormProvider export const { useForm: useEditProfileForm, FormProvider: EditProfileProvider } = hookFormFactory<InputEditProfileForm, unknown, OutputEditProfileForm>(); 命名は機械的に決まる ファイル {camelCase}.ts スキーマ {name}FormSchema Input 型 Input{Name}Form Output 型 Output{Name}Form マッパー toInput{Name}Form AI の差分が同じ形で並ぶ → レビュー観点が激減する Copyright Hacobu, Inc. 9
  5. Copyright Hacobu, Inc. 10 04 ★ 今日いちばん伝えたい話 ── z.input と

    z.output で境界を引く UI の値 ≠ API の値 ── transform で型に押し込める Schema 側 // UI は空文字、API には undefined const profile = z.object({ email: optionalEmailSchema .transform(nullToUndefined), }); // Input: string (空文字を許容) // Output: string | undefined useForm 側 // 3 つ目の型引数を必ず明示する const form = useEditProfileForm({ mode: "onBlur", defaultValues: toInputEditProfileForm(api), resolver: zodResolver(schema), }); const onValid: SubmitHandler<OutputEditProfileForm> = (values) => onSubmit(values); 型境界が決まると、レビュー観点が消える defaultValues に渡すのは Input{Form} ── 型が教えてくれる Container と submit の引数は必ず Output{Form} ── ぶれない transform を増やすたび Input / Output ── が自動追従/修正の波及がない ※ useForm<z.input, any, z.output> の 3 型を必ず明示。書かないと handleSubmit が input で推論される。 Copyright Hacobu, Inc. 10
  6. Copyright Hacobu, Inc. 11 05 hookFormFactory ── プロダクト仕様を 1 箇所に閉じ込める

    "全フォームに散らばる dirty 制御" を、factory に隠す // プロダクト固有の dirty 仕様を factory に閉じ込める const useForm = <T, C, V>(props: UseFormProps<T, C, V>, shouldAccessDirtyFields = true) => { const form = _useForm<T, C, V>(props); // formState は Proxy 購読 ── dirtyFields に触って未保存検知を安定化 if (shouldAccessDirtyFields) form.formState.dirtyFields; return { ...form, // setValue 経由は shouldDirty: true をデフォルト化 setValue: (...args) => form.setValue(args[0], args[1], { shouldDirty: true, ...args[2] }), }; }; export const hookFormFactory = <T, C, V>() => ({ useForm: useForm<T, C, V>, useFormContext: useFormContext<T, C, V>, FormProvider: FormProvider<T, C, V>, useWatch: useWatch<T, any, V>, }); ① ライブラリの一般解 では届かない部分を埋める ② 製品仕様 ( dirty / 離脱 ) を 1 箇所に閉じ込めた ③ AI が触る面積 が小さくなり、出力が安定する Copyright Hacobu, Inc. 11
  7. Copyright Hacobu, Inc. 12 06 RHFInput ── UI adapter で

    20 パターンを制圧 Field render props を直接書き換えない Before ── RFF <Field name="name" validate={Validator.required}> {({ input, meta }) => ( <MField label="氏名" errorMessage={meta.touched ? meta.error : ""}> <AInput value={input.value} onChange={input.onChange} /> </MField> )}</Field> After ── RHFInput <RHFInput controllerProps={{ control, name: "profile.name" }} inputProps={{ onBlur: (e) => { if (e.target.value === "") return; setValue("profile.name", e.target.value.trim()); }, }} fieldProps={{ label: "氏名", labelType: "required" }} /> RHF コンポーネントは 20 個ほど ── Agent はパターン適用として横展開できる RHFInput RHFSelectSingle RHFSelectSearch RHFInputDate RHFInputTime RHFInputToggle RHFCheckboxGroup RHFInputFile2 RHFRadioGroup RHFInputNumber → Skill には「テキスト入力は RHFInput」「単一選択は RHFSelectSingle」と書けば、Agent が同じ判断で並べてくれる。 Copyright Hacobu, Inc. 12
  8. Copyright Hacobu, Inc. 13 07 分業 ── 人間が握る、AI Agent が量をこなす

    "賢いコード" ではなく "同じコード" を量産する 人間が握る ── アーキテクチャと判断基準 AI Agent に任せる ── 量とパターン適用 • 移行後のアーキテクチャ 1 schema + Input/Output + Factory + UI adapter • 型境界 z.input / z.output / 3 型ジェネリクス • プロダクト仕様 dirty / reset / errorMessage / submit 伝播 • 攻略本の整備 Skill / 参考 PR / レビューチェックリスト • 既存フォームを読む Final Form の <Field> を identify • パターンに沿って書き換え RHFInput / RHFSelectSingle に置換 • import / 型 / JSX の整合 useForm の 3 型ジェネリクスまで揃える • 類似フォームへの横展開 1 つ通れば、隣の 5 つは Agent が回す 必要なのは "すごいプロンプト" ではない。必要なのは攻略本。 人間が型でアーキを握り、AI が量をこなす ── 任せる範囲を狭く定義したから速くなった。 Copyright Hacobu, Inc. 13
  9. Copyright Hacobu, Inc. 14 08 結果と学び ── 2 年仕事を、仕組みに変えた 2026.1

    → 5 ヶ月で 140+ 完了。"2 年仕事" を仕組みに変えた 対象フォーム 約 150 置換 DONE フォーム 140+ 残りフォーム 約 10 Validator→Zod 置換 193 大事なのは完了率ではなく、残り 10 フォームも「同じ判断基準」で進められる状態になったこと。 ここから引き出した 5 つの学び 1 まず型を作る ── JSX 置換に見えて本質は境界設計。z.input / z.output で人も AI も迷わない。 2 手順ではなく判断基準を書く ── Skill = 移行レビュー基準。手順より先に判断基準を書く。 3 共通化は AI のためにも効く ── hookFormFactory / RHFInput は Agent の出力安定性に直結する。 4 任せる範囲を狭くするほど速い ──「賢いコード」ではなく「同じコード」を量産する。 5 勝負はコードを書く前につく ── 最初の 1 フォームで作る攻略本が、その先全フォームの速度と品質を決める。 Copyright Hacobu, Inc. 14