Claude Codeでレガシーコードを安全に近代化する実践手順
Claude Codeでレガシーコードをテストで守りながら段階的に近代化する実践手順。失敗例と検証コードも紹介。
レガシーコード近代化は「一気に直す」と失敗する
レガシーコードとは、単に古いコードではありません。仕様を知る人が少なく、テストが薄く、変更した瞬間に別の画面や帳票が壊れそうで、チームが触るのを避けているコードです。Claude Codeはこの状況で強力ですが、魔法の置換ツールとして使うと危険です。
安全な使い方は、Claude Codeにいきなり書き換えさせることではなく、まず観察、次に現状の振る舞いをテストで固定し、その後で小さく変えることです。公式のClaude Code common workflowsでも、コード理解、リファクタリング、テスト、PR作成を段階的に扱う流れが示されています。
この記事では、私が古い注文処理コードを改善するときに使っている実践手順を紹介します。harnessは「エージェントの足場」、つまりClaude Codeが安全に作業するためのテスト、設定、手順書のことだと考えてください。足場を作らずに大規模変更を始めると、AIを使っていても人間だけの開発と同じように事故ります。
最初に決めるべき3つのユースケース
レガシーコード改善では、技術的に美しい順番ではなく、事業上のリスクが下がる順番で進めます。Claude Codeに依頼する前に、少なくとも次の3つのユースケースを切り分けます。
| ユースケース | 目的 | Claude Codeに任せる作業 | 人間が確認する点 |
|---|---|---|---|
| 決済や注文などの重要処理 | 売上や請求ミスを防ぐ | 現状仕様の抽出、テスト追加、境界値の洗い出し | 金額、丸め、税、割引の仕様 |
| 古いJavaScriptからTypeScriptへの移行 | 変更時の事故を減らす | 型候補の作成、anyの段階的削減、型エラー修正 | 公開APIと互換性 |
| コールバックや巨大関数の分割 | 読みやすさと保守性を上げる | 責務分割、命名案、差分の説明 | 例外処理と副作用の維持 |
この3つは小さなプロダクトでもよく出ます。私の感覚では、最初から「全部きれいにして」と頼むより、「注文合計の現状仕様を守る」「公開APIは変えない」「1回の差分は100行以内」のように制約を置いた方が、レビューがかなり楽になります。
flowchart LR
A[調査] --> B[振る舞いをテストで固定]
B --> C[小さくリファクタリング]
C --> D[型と依存関係を更新]
D --> E[人間が差分とリスクを確認]
E --> B
Claude Codeに渡す前の棚卸し
まず、Claude Codeにコードを読ませて地図を作らせます。ここで重要なのは「修正しない」と明示することです。初回から編集を許すと、まだ仕様が分からない状態で正しそうな差分が出てしまいます。
@src/legacy と @test を読んでください。
まだファイルは変更しないでください。
次の形式で調査結果を出してください。
1. 主要ファイルと責務
2. 外部I/O、DB、API、ファイル操作、副作用
3. 仕様として守るべき振る舞い
4. テストが足りない箇所
5. 小さく安全に直す順番
不明な仕様は推測せず「要確認」と書いてください。
このプロンプトは、Claude Codeを実装者ではなくレビュアーとして使うためのものです。公式のHow Claude Code worksにもあるように、Claude Codeはファイルを読み、コマンドを実行し、編集できます。だからこそ、最初のフェーズでは「読むだけ」に絞るのが安全です。
コピーして試せる最小サンプル
以下は記事用に小さくした注文処理の例です。実際の業務コードではDBや外部APIが絡みますが、考え方は同じです。まず動くサンプルを作ります。
mkdir legacy-modernization-demo
cd legacy-modernization-demo
npm init -y
npm install -D vitest typescript @types/node
npm pkg set type="module"
npm pkg set scripts.test="vitest run"
npm pkg set scripts.typecheck="tsc --noEmit"
mkdir -p src/legacy test
古い実装は1ファイルにロジックが集まっています。悪いコードとして極端に壊すのではなく、現場でよくある「動くけれど触りにくい」形にしています。
// src/legacy/orderProcessor.js
export function processOrder(order) {
if (!order || !Array.isArray(order.items) || order.items.length === 0) {
return { status: "error", message: "items is required" };
}
const subtotal = order.items.reduce((sum, item) => {
return sum + item.price * item.qty;
}, 0);
const discount = order.customer?.type === "vip" ? subtotal * 0.1 : 0;
return {
status: "confirmed",
total: subtotal - discount,
items: order.items,
discount
};
}
次に、現状の振る舞いをテストで固定します。ここでは「理想の仕様」ではなく「今壊してはいけない仕様」を書くのがポイントです。
// test/orderProcessor.test.ts
import { describe, expect, it } from "vitest";
import { processOrder } from "../src/legacy/orderProcessor.js";
describe("processOrder legacy behavior", () => {
it("calculates total for a regular customer", () => {
const result = processOrder({
items: [
{ id: "A1", qty: 2, price: 1000 },
{ id: "B2", qty: 1, price: 500 }
],
customer: { id: "C1", type: "regular" }
});
expect(result).toMatchObject({
status: "confirmed",
total: 2500,
discount: 0
});
});
it("applies a 10 percent VIP discount", () => {
const result = processOrder({
items: [{ id: "A1", qty: 1, price: 10000 }],
customer: { id: "C2", type: "vip" }
});
expect(result.status).toBe("confirmed");
expect(result.total).toBe(9000);
expect(result.discount).toBe(1000);
});
it("returns an error when items are empty", () => {
const result = processOrder({
items: [],
customer: { id: "C3", type: "regular" }
});
expect(result.status).toBe("error");
expect(result.message).toContain("items");
});
});
この時点で npm test を実行します。テストが通ったら、Claude Codeには次のように依頼します。
@src/legacy/orderProcessor.js と @test/orderProcessor.test.ts を読んでください。
既存テストを通したまま TypeScript に移行してください。
条件:
- 公開関数名 processOrder は変えない
- 戻り値の status, total, discount, message は互換性を維持する
- まず型定義を追加し、その後に責務を分割する
- 変更後に npm test と npm run typecheck を実行する
- 差分ごとに、何を守ったかを説明する
TypeScript化後の形
近代化後は、型、検証、計算、注文処理を分けます。大事なのは、分割そのものではありません。金額計算の仕様をテストで守ったまま、レビューしやすい単位にすることです。
// src/orderTypes.ts
export type CustomerType = "regular" | "vip";
export type OrderItem = {
id: string;
qty: number;
price: number;
};
export type OrderInput = {
items: OrderItem[];
customer: {
id: string;
type: CustomerType;
};
};
export type OrderResult =
| {
status: "confirmed";
total: number;
items: OrderItem[];
discount: number;
}
| {
status: "error";
message: string;
};
// src/validators.ts
import type { OrderInput } from "./orderTypes";
export function validateOrder(order: OrderInput | null | undefined): string | null {
if (!order || !Array.isArray(order.items) || order.items.length === 0) {
return "items is required";
}
return null;
}
// src/calculators.ts
import type { CustomerType, OrderItem } from "./orderTypes";
export function calculateSubtotal(items: OrderItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
export function calculateDiscount(subtotal: number, customerType: CustomerType): number {
return customerType === "vip" ? subtotal * 0.1 : 0;
}
// src/orderProcessor.ts
import { calculateDiscount, calculateSubtotal } from "./calculators";
import type { OrderInput, OrderResult } from "./orderTypes";
import { validateOrder } from "./validators";
export function processOrder(order: OrderInput): OrderResult {
const validationMessage = validateOrder(order);
if (validationMessage) {
return { status: "error", message: validationMessage };
}
const subtotal = calculateSubtotal(order.items);
const discount = calculateDiscount(subtotal, order.customer.type);
return {
status: "confirmed",
total: subtotal - discount,
items: order.items,
discount
};
}
テストのimport先を ../src/orderProcessor に変え、再度 npm test と npm run typecheck を実行します。ここまでの差分なら、人間のレビューで追えます。逆に、型変換、命名変更、ディレクトリ移動、仕様変更、依存更新を同じPRに入れると、Claude Codeが正しく作業していてもレビューで見落としが出ます。
依存関係更新は別トラックで扱う
レガシー改善でよくある失敗は、コードのリファクタリングと依存パッケージのメジャー更新を同時にやることです。エラーが出たときに、型の問題なのか、API変更なのか、ビルド設定なのかが切り分けられません。
Claude Codeには、まず調査だけを依頼します。
package.json と lockfile を読んでください。
まだ更新しないでください。
次を表で出してください。
- パッケージ名
- 現在のバージョン
- 更新候補
- メジャー変更の有無
- 公式マイグレーションガイドのURL
- 影響するファイル
- 先に追加すべきテスト
テスト、型付け、依存更新は関連していますが、同じ差分に混ぜる必要はありません。Claude Codeの権限設定や確認フローはpermissionsを読み、削除、マイグレーション、デプロイのような副作用が大きい操作は必ず人間の承認を挟みます。
現場での進め方
実務では、1回のClaude Codeセッションに入れる目的をかなり狭くします。たとえば「注文処理を近代化する」ではなく、「注文合計の現状仕様を説明し、テスト不足を3つ挙げる」「VIP割引のテストを追加する」「orderProcessorだけをTypeScriptへ移す」のように分けます。粒度が小さいほど、差分の意味を人間が確認しやすくなります。
私は、レガシー改善のPRでは必ず3つの欄を作ります。1つ目は「守った互換性」です。戻り値、エラー文言、丸め、公開関数名など、既存利用者に影響する点を書きます。2つ目は「変えた設計」です。責務分割、型追加、依存更新など、保守性のために変えた点を書きます。3つ目は「まだ触っていない負債」です。全部を一度に直していないことを明示すると、次の担当者が安心して続きに着手できます。
Claude Codeには、この3欄をPR説明に反映させるところまで依頼できます。ただし、説明が正しいかは人間が読みます。特に金額計算や権限まわりでは、「テストが通った」だけで安心しません。サンプルデータ、実際のログ、過去の障害メモを見ながら、テストに入っていないケースを探します。AIの出力はレビュー対象であって、レビューの代替ではありません。
具体的な落とし穴
1つ目は、テストがないまま「きれいなコード」に変えることです。読みやすくなっても、丸め、割引、例外時の戻り値が変われば障害です。
2つ目は、Claude Codeの提案を仕様として採用してしまうことです。AIが「この方が自然です」と言っても、既存ユーザーが依存している挙動なら互換性を優先します。
3つ目は、巨大PRを作ることです。ファイル移動、型変更、ロジック分割、依存更新、フォーマット変更を混ぜると、レビューの密度が下がります。
4つ目は、エラー処理をきれいにしすぎることです。古いAPIでは null、空文字、特定の文言を返すこと自体が契約になっている場合があります。
5つ目は、ドキュメント更新を最後に回すことです。Claude Codeに変更理由、守った互換性、手動確認手順を同じPRで書かせると、次の保守担当者が助かります。
レビューとCTA
この手順は、リファクタリング自動化ガイド、TDDとClaude Codeの相性、ドキュメント自動生成と組み合わせると実務で使いやすくなります。チーム導入時はCLAUDE.mdのベストプラクティスに、変更禁止領域、テストコマンド、レビュー観点を書いておくと出力が安定します。
ClaudeCodeLabでは、既存プロダクトにClaude Codeを入れるための研修、CLAUDE.mdテンプレート、レガシー改善の相談を扱っています。古いコードをAIで一気に置き換えるのではなく、テスト、権限、レビューの足場を作ってから前に進めたいチーム向けです。
実際に試した結果
この記事のサンプルを legacy-modernization-demo として作り、Vitestで3ケースを固定してからTypeScript版へ置き換える流れを確認しました。効果が大きかったのは、Claude Codeに「変更しない調査」と「テストを通したままの小さな変更」を分けて依頼した点です。最初から全面書き換えを頼むより差分が読みやすく、金額計算とエラー戻り値の互換性も確認しやすくなりました。
特に、失敗したときに戻る場所があることが心理的に大きいです。テスト、型チェック、PR説明の3つがそろうと、AIの提案を採用するか却下するかを冷静に判断できます。レガシー改善では、この落ち着いて止まれる状態を作ることが一番の生産性向上でした。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。