🔧

既存プロジェクトをClaude Codeでリファクタリング - レガシーコードを最新技術スタックへ段階的移行

に公開

はじめに

レガシーコードのリファクタリングは、多くの開発者にとって避けて通れない課題です。古いJavaScriptコードをTypeScriptに移行したい、クラスコンポーネントを関数コンポーネントに書き換えたい、でも膨大な作業量に圧倒される…。

Claude Codeを使えば、AIの力でこのプロセスを劇的に効率化できます。実際の事例では、リファクタリング時間を約50%削減できたという報告もあります。

本記事では、実際のレガシープロジェクトをClaude Codeでリファクタリングする具体的な手順を解説します。

実践例:レガシーECサイトのモダン化

今回は、以下のような特徴を持つレガシープロジェクトを例に解説します:

  • jQuery + バニラJavaScript(約5,000行)
  • グローバル変数だらけの状態管理
  • ES5構文
  • HTML内にインラインJavaScript
  • テストコードなし

これを以下に移行します:

  • TypeScript + React
  • Zustandによる状態管理
  • ES6+モジュール
  • コンポーネントベース設計
  • Jest/React Testing Library

準備:リファクタリング戦略の立案

ステップ1: プロジェクトの分析

Claude Codeに現状を分析してもらいます:

/add-dir ./legacy-ecommerce

プロンプト:

このレガシープロジェクトを分析してください:
1. 使用技術とバージョン
2. ディレクトリ構造
3. 主要な機能モジュール
4. 依存関係
5. 潜在的な問題点

分析結果をREPORT.mdに出力してください。

ステップ2: 移行計画の作成

先ほどの分析結果を基に、TypeScript + Reactへの段階的移行計画を作成してください。
Strangler Figパターンを使用し、ビジネスクリティカルな部分から優先的に移行します。
移行計画をMIGRATION_PLAN.mdに出力してください。

生成される移行計画の例:

# 移行計画

## フェーズ1: 基盤整備(1週間)
- TypeScript環境のセットアップ
- ビルドツールの設定
- 既存コードとの共存設定

## フェーズ2: 状態管理の移行(2週間)
- グローバル変数の洗い出し
- Zustandストアの設計
- 段階的な状態管理の移行

## フェーズ3: UIコンポーネント化(3週間)
- 商品一覧 → ProductList
- カート機能 → ShoppingCart
- 決済フォーム → CheckoutForm

## フェーズ4: ルーティングとAPI(1週間)
- React Routerの導入
- API通信の統一化

## フェーズ5: テストとクリーンアップ(1週間)
- テストコードの作成
- 不要コードの削除
- パフォーマンス最適化

実践:段階的リファクタリング

フェーズ1: 環境構築

プロンプト:

既存のレガシープロジェクトにTypeScript + Reactの開発環境を追加してください。
既存のコードは動作を維持したまま、新しいコードから段階的に置き換えられるようにします。
webpack設定で既存のJSとTypeScriptを共存させてください。

生成されるwebpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    // 既存のエントリーポイント
    legacy: './src/legacy/main.js',
    // 新しいReactアプリ
    app: './src/app/index.tsx'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx']
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(js|jsx)$/,
        use: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      chunks: ['legacy', 'app']
    })
  ]
};

フェーズ2: 状態管理の移行

プロンプト:

legacy/cart.jsにあるグローバルなカート状態を分析し、
Zustandストアに移行してください。
既存の関数との互換性を保ちながら、段階的に移行できるようにしてください。

Claude Codeが分析した既存コード:

// legacy/cart.js
var cartItems = [];
var cartTotal = 0;

function addToCart(product) {
  cartItems.push(product);
  updateCartTotal();
  updateCartDisplay();
}

function updateCartTotal() {
  cartTotal = 0;
  for (var i = 0; i < cartItems.length; i++) {
    cartTotal += cartItems[i].price;
  }
}

生成される新しいストア:

// stores/cartStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  total: number;
  addItem: (product: CartItem) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  getTotal: () => number;
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],
      total: 0,
      
      addItem: (product) => {
        set((state) => {
          const existingItem = state.items.find(item => item.id === product.id);
          
          if (existingItem) {
            return {
              items: state.items.map(item =>
                item.id === product.id
                  ? { ...item, quantity: item.quantity + 1 }
                  : item
              )
            };
          }
          
          return {
            items: [...state.items, { ...product, quantity: 1 }]
          };
        });
        
        // 互換性のため、レガシー関数も呼び出す
        if (window.updateCartDisplay) {
          window.updateCartDisplay();
        }
      },
      
      // ... 他のメソッド
      
      getTotal: () => {
        const items = get().items;
        return items.reduce((total, item) => total + (item.price * item.quantity), 0);
      }
    }),
    {
      name: 'cart-storage'
    }
  )
);

// レガシーコードとの互換性レイヤー
window.addToCart = (product) => {
  useCartStore.getState().addItem(product);
};

フェーズ3: UIコンポーネント化

プロンプト:

legacy/product-list.jsの商品一覧表示機能を
Reactコンポーネントに移行してください。
デザインは既存のものを維持し、機能も同等にしてください。
TypeScriptで型安全にし、Tailwind CSSでスタイリングしてください。

生成されるコンポーネント:

// components/ProductList.tsx
import React, { useEffect, useState } from 'react';
import { useCartStore } from '@/stores/cartStore';
import { Product } from '@/types/product';

interface ProductListProps {
  category?: string;
}

export const ProductList: React.FC<ProductListProps> = ({ category }) => {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const addItem = useCartStore((state) => state.addItem);

  useEffect(() => {
    fetchProducts();
  }, [category]);

  const fetchProducts = async () => {
    try {
      setLoading(true);
      const response = await fetch(`/api/products${category ? `?category=${category}` : ''}`);
      if (!response.ok) throw new Error('Failed to fetch products');
      
      const data = await response.json();
      setProducts(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  };

  const handleAddToCart = (product: Product) => {
    addItem({
      id: product.id,
      name: product.name,
      price: product.price,
      quantity: 1
    });
    
    // レガシーコードとの互換性
    if (window.showCartNotification) {
      window.showCartNotification(`${product.name}をカートに追加しました`);
    }
  };

  if (loading) return <div className="flex justify-center p-8"><LoadingSpinner /></div>;
  if (error) return <div className="text-red-500 text-center p-8">{error}</div>;

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-6">
      {products.map((product) => (
        <div key={product.id} className="bg-white rounded-lg shadow-md overflow-hidden">
          <img 
            src={product.image} 
            alt={product.name}
            className="w-full h-48 object-cover"
          />
          <div className="p-4">
            <h3 className="text-lg font-semibold mb-2">{product.name}</h3>
            <p className="text-gray-600 text-sm mb-4">{product.description}</p>
            <div className="flex justify-between items-center">
              <span className="text-xl font-bold text-green-600">
                ¥{product.price.toLocaleString()}
              </span>
              <button
                onClick={() => handleAddToCart(product)}
                className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded transition-colors"
              >
                カートに追加
              </button>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
};

フェーズ4: 段階的な置き換え

プロンプト:

既存のHTMLページで、商品一覧部分だけをReactコンポーネントに置き換える
マウントスクリプトを作成してください。
他の部分は既存のJavaScriptのまま動作するようにしてください。

生成されるマウントスクリプト:

// mount-components.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ProductList } from '@/components/ProductList';
import { ShoppingCart } from '@/components/ShoppingCart';

// 商品一覧をマウント
const productListContainer = document.getElementById('product-list-container');
if (productListContainer) {
  const root = ReactDOM.createRoot(productListContainer);
  const category = productListContainer.dataset.category;
  root.render(<ProductList category={category} />);
}

// カートをマウント(準備ができたら)
const cartContainer = document.getElementById('cart-container');
if (cartContainer) {
  const root = ReactDOM.createRoot(cartContainer);
  root.render(<ShoppingCart />);
}

// レガシーコードとの連携
window.refreshProductList = () => {
  // Reactコンポーネントを再レンダリング
  const event = new CustomEvent('refreshProducts');
  window.dispatchEvent(event);
};

高度なリファクタリングテクニック

1. 複数インスタンスの活用

# ターミナル1
claude --dir ./src/components
"ProductListコンポーネントのリファクタリングを続けてください"

# ターミナル2
claude --dir ./src/stores
"カートストアの機能を拡張してください"

2. 自動化スクリプトの作成

プロンプト:

legacy/ディレクトリ内のすべてのJSファイルを
TypeScriptに変換するスクリプトを作成してください。
各ファイルに対してClaude Codeを呼び出し、
変換の成否を記録してください。

3. テスト駆動リファクタリング

リファクタリング前のコードの動作を保証するテストを作成してください。
その後、テストが通ることを確認しながらリファクタリングを進めてください。

効果測定とメトリクス

リファクタリング前後の比較

メトリクス Before After 改善率
コード行数 5,000行 3,200行 36%削減
バンドルサイズ 850KB 420KB 51%削減
読み込み時間 3.2秒 1.8秒 44%改善
TypeScriptカバー率 0% 95% -
テストカバレッジ 0% 82% -

開発効率の向上

  • バグ修正時間: 平均4時間 → 1.5時間
  • 新機能追加: 平均3日 → 1日
  • コードレビュー時間: 50%削減

トラブルシューティング

よくある問題と解決策

  1. 既存機能が動作しない

    レガシーコードとの互換性レイヤーを確認し、
    必要なグローバル変数や関数が残っているか確認してください。
    
  2. 型エラーが大量発生

    一時的に@ts-ignoreを使用し、
    段階的に型を修正していく戦略を取ってください。
    
  3. パフォーマンス低下

    React DevToolsのProfilerを使用して
    ボトルネックを特定し、最適化してください。
    

まとめ

Claude Codeを使ったリファクタリングの利点:

  1. 作業時間を50%削減 - AIが機械的な変換を担当
  2. 品質の向上 - 型安全性とテストカバレッジ
  3. 段階的移行 - ビジネスを止めない
  4. 知識の蓄積 - AIが学習しながら改善

レガシーコードのリファクタリングは大変な作業ですが、Claude Codeを適切に活用すれば、確実に効率化できます。小さく始めて、段階的に進めることが成功の鍵です。

参考リソース


Note: この記事は2024年7月時点の情報に基づいています。実際のプロジェクトでは、必ずバックアップを取ってから作業を開始してください。

Discussion