本文へスキップ
hdknr blog
戻る

brownfield を壊さず作り替える — Django × React/TS リファクタリング実践ガイド

新規開発(greenfield)と、改修を重ねてきた既存コード(brownfield)では、リファクタリングの難しさの質がまるで違う。前回の Ponytail の記事では「greenfield で効き、成熟コードベースでは薄まる」という適用条件に触れたが、では その成熟コードベース側を、どう安全に作り替えるのか。この記事は Django(Python)と React(TypeScript)という具体的なスタックに落とし込んだ実践プレイブックだ。

greenfield と brownfield

はじめに用語を整理しておく。

工事現場で、更地(緑の草地)に新築するか、既存の建物(茶色く汚れた土地)を改修するか、という比喩から来ている。実運用中の業務システムのほとんどは brownfield であり、そこでの最大の敵は 過剰実装ではなく「正しさの後退」——間違った項目名、間違った ID、データ不整合、意図しない挙動変化だ。

だからこそ brownfield のリファクタリングは、たった一つの鉄則に集約される。振る舞いを固定してから、内部を動かす。

大原則:安全網を先に張る

リファクタリングの定義は「外部から見た振る舞いを変えずに内部構造を改善する」こと。であれば、振る舞いを固定する手段を最初に用意しなければ、そもそも安全に始められない。

ワークフロー:6 ステップ

安全網を軸に、作業は次の 6 ステップで進める。順序に意味がある——安全網(②凍結)より前に大きく動かさない、削除(⑥)を最後に回す、が要点だ。

Django と React/TS の brownfield リファクタリングを 6 ステップで示したワークフロー図。安全網を張るフェーズ(①計測・②凍結・③継ぎ目)と動かすフェーズ(④変換・⑤段階置換・⑥削除)に分かれ、各ステップに対応するツールが添えられている。⑥削除から①計測へ戻る反復の矢印も描かれている。

  1. 計測 — 「変更頻度 × 複雑度」が高いホットスポットから着手する。感覚ではなく Git 履歴と静的解析で優先順位を出す。
  2. 凍結 — 着手箇所の現状挙動を特性テストで記録する。ここが安全網の要。
  3. 継ぎ目 — 依存を差し込めるテスト可能な接合点を作る。
  4. 変換 — 振る舞い不変の小さな変更を、IDE と codemod で積む。
  5. 段階置換 — 大物は一括でなく、新旧を並行稼働させながら機能単位で差し替える。
  6. 削除 — 置換完了後に旧経路と未使用コードを消す。

段階置換のパターン

「直そうとすると芋づるで壊れる」brownfield 特有の依存に効く、言語非依存の定番手法を押さえておく。

パターンやることDjango / React での当てはめ
Strangler Fig新実装を旧の周りに這わせ、機能単位で差し替え、最後に旧を除去DRF の ViewSet を 1 本ずつ新サービスへ / ルート単位で新コンポーネントに切替
Branch by Abstraction抽象層を挟み新旧を並行稼働、フラグで切替えながら main で継続Django のサービス interface + フラグ / React は Context・Hook を抽象境界にしフラグ分岐
Mikado Method目標→前提依存をグラフ化し葉から潰す。行き詰まったら即 revert循環 import や密結合モデルの分解 / 相互依存した hook・store の解きほぐし
Seam の導入テスト用に振る舞いを差し込める継ぎ目を作るORM 呼び出しをリポジトリ層へ / 副作用を useXxx フックに隔離し純粋化
Scientist(並行検証)旧経路と新経路を本番で並行実行し、結果の差異だけ記録クリティカルな集計 API の無停止移行 / 表示ロジック差し替え時の出力比較

コツは、大物ほど段階置換で「いつでも戻せる」状態を保つこと。Mikado Method の「行き詰まったら即 revert」は、深い依存を相手にするときの精神的な安全弁でもある。

スタック別ツールベルト

6 ステップの各役割に、Django と React/TS の具体的なツールを紐づける。

Django(Python)

ツール役割用途
pytest-django + coverage.py凍結・計測特性テストの土台。--cov で薄い箇所を可視化してから着手する
syrupy / inline-snapshotgolden masterAPI レスポンスや複雑な戻り値をスナップショット比較。個別アサーションが辛い箇所を丸ごと固定
django-test-migrations凍結マイグレーションの前後整合と後方互換を検証。データ移行の事故を止める
import-linter継ぎ目・契約app 間・パッケージ間の層依存をルール化し CI で強制。禁止依存の混入を防ぐ
django-extensions計測show_urls / graph_models で URL・モデル依存を可視化し、結合の実態を掴む
ruff + mypy / pyright変換Lint/format を一手に。型は段階導入し、触った箇所から厳格化する
libcst / django-upgrade / pyupgrade変換(codemod)構文木ベースの一括変換。API 移行や古い書式の機械置換を安全に
django-waffle段階置換フィーチャーフラグで新旧経路を切替え、Strangler / Branch by Abstraction を運用
nplusone / django-silk計測N+1 とクエリのホットスポットを検出。ORM の遅延評価が絡む変更の副作用を見張る
vulture削除デッドコード候補を検出。旧経路除去の最終段で使う

React(TypeScript)

ツール役割用途
Vitest / Jest + Testing Library凍結ユーザー視点の振る舞いを固定。実装詳細でなく「見える挙動」でテストするのが特性テストの肝
Playwright凍結(E2E)クリティカルなユーザーフローを端から端まで凍結。境界を跨ぐ改修の安全網
Storybook + Chromaticgolden masterコンポーネントの見た目をビジュアルリグレッションで固定。UI の意図しない変化を検出
dependency-cruiser / madge計測・契約循環依存と層違反を検出しルール化。import の依存グラフを CI で強制
ts-morph / ast-grep / jscodeshift変換(codemod)AST ベースの一括変換。props リネーム、API 差し替え、パターン置換を機械的に
typescript-eslint / Biome変換Lint/format。Biome は大規模でも高速。ルールを段階的に厳格化
tsc --strict + type-coverage変換strict を一括でなくファイル単位で導入し、型カバレッジを ratchet(後退禁止)で上げる
フィーチャーフラグ段階置換ルート・コンポーネント単位で新旧を切替え、Strangler を安全に進める
React DevTools Profiler / react-scan計測再レンダリングのホットスポットを特定。Context 分割や memo 化の効果を before/after で確認
knip削除未使用の export・依存・ファイルを検出。旧経路撤去の最終段で安全に削る

AI エージェントは「テストで挟んで」使う

Claude Code や Cursor は brownfield でも強力だが、主要リスクが「正しさ」である以上、丸投げではなくテストで検証するループに乗せるのが前提だ。前回の Ponytail 記事で触れた「成熟コードベースでは『少なく書く』最適化がリスクと直交する」という論点と、ここは地続きである。

具体的には次の小刻みループを回す。

  1. 現状を凍結させる — まず AI に特性テスト(RTL / pytest / snapshot)を書かせ、今の挙動を固定する。
  2. 小さく変換を依頼 — 抽出・リネーム・1 パターンの codemod など、範囲を絞った変更だけ頼む。
  3. テストで不変を確認 — 特性テストが緑のまま=振る舞い不変。赤なら即 revert して粒度を下げる。

大きな一括変換を一発で頼まないこと。「凍結 → 小変換 → 検証」の小刻みループのほうが、成熟コードベースでは圧倒的に安全で速い。AST を理解する ts-morph / libcst のような codemod と組み合わせると役割分担が効く。AI は「変換の設計」に集中し、機械的な置換はツールが担う。

たとえば「凍結」ステップの特性テストは、正しい仕様を問わず、いま返っている出力をそのまま固定するだけでよい。

# Django: 現状のレスポンスをスナップショットで固定する(syrupy)
def test_order_summary_characterization(client, snapshot):
    res = client.get("/api/orders/42/summary/")
    assert res.status_code == 200
    assert res.json() == snapshot  # 初回実行時の出力を「現状」として記録
// React: 実装詳細でなく「見える挙動」を固定する(RTL)
test("送信ボタンを押すと確認ダイアログが出る", async () => {
  render(<OrderForm order={order} />);
  await userEvent.click(screen.getByRole("button", { name: "送信" }));
  expect(screen.getByRole("dialog")).toBeInTheDocument();
});

これらが緑のまま内部を作り替えられれば、振る舞いは不変だと機械的に保証できる。

危険地帯:この 2 スタックで壊れやすいところ

「振る舞い不変」の落とし穴。ここは特性テストを厚めに張ってから触る。

Django

React

まとめ

brownfield のリファクタリングは、greenfield のような「良い設計を最初から選ぶ」ゲームではなく、「動いているものを壊さずに少しずつ良くする」ゲームだ。だからこそ、

  1. まず安全網(特性テスト)を張り、
  2. 計測でホットスポットを選び、
  3. 継ぎ目を作って小さく変換し、
  4. 大物は段階置換で可逆に進め、
  5. 最後にデッドコードを削除する。

Django と React/TS には、この各ステップを支える成熟したツールが揃っている。AI エージェントも「テストで挟む小刻みループ」に乗せれば、正しさを損なわずに変換速度だけを上げられる。過剰実装を抑える Ponytail が greenfield の武器なら、こちらは brownfield の武器だ。



前の記事
料理写真のライティング実践ガイド — 面光源と定常光で「美味しそう」に撮る
次の記事
SuperpowersとGrill with Docs:Claude Codeエージェント設計思想の変遷