GitHub Copilot AgentモードでWebアプリケーションのリファクタリングを自動実践

GitHub Copilotの進化は止まりません。2025年5月にリリースされたVisual Studio Code(VS Code) 1.101では、Custom Chat Modesに加えて「Agentモード」の機能が大幅に強化されました。

このAgentモードは、複数ファイルにわたる自動編集や効率的なリファクタリングを可能にする革新的な機能です。

従来の単一ファイル編集から脱却し、プロジェクト全体を俯瞰した改善作業を自動化することで、開発効率を飛躍的に向上させることができます。過度な分割を避け、現実的で保守しやすいコード構造を目指します。

本記事では、実際のWebアプリケーション(タイピングソフト)を題材に、Agentモードを使った実践的なリファクタリング手法を詳しくご紹介します。

環境

  • オペレーティングシステム: Windows 11
  • エディタ: Visual Studio Code (version 1.101以上)
  • VSCode拡張機能: GitHub Copilot (Business/Pro/Free ※Agentモードはすべてのプランで利用可能)
  • 対象プロジェクト: JavaScript/HTML/CSS Webアプリケーション

GitHub Copilot Agentモードとは?

GitHub Copilot Agentモードは、従来のAskモードやEditモードを大きく超えた、自律的な開発支援機能です。

単一ファイルの編集にとどまらず、プロジェクト全体を理解し、複数のファイルにわたって一貫した変更を実行できます。

従来モードとの根本的違い

従来の3つのモードはそれぞれ異なる目的に最適化されています。

  • Askモード: 質問回答に特化、コード変更は行わない
  • Editモード: 指定されたファイルの編集に特化、人間の詳細な指示が必要
  • Agentモード: 自律的判断でプロジェクト全体の最適化を実行

Agentモードの特徴的能力

Agentモードには以下のような特徴的な能力があります。

  • プロジェクト全体の理解: ファイル間の依存関係や設計パターンを把握
  • 自律的なタスク分解: 大きな要求を適切な小さなタスクに分割
  • 複数ファイル同時編集: HTML、CSS、JavaScript、設定ファイルを連携して変更
  • 適切なモジュール化: 過度な分割を避けた実用的な構造設計
  • 継続的検証: 変更後の動作確認と修正の繰り返し

リファクタリング対象プロジェクトの分析

今回リファクタリングを行うタイピングソフトは、以下のような構成になっています。

CodingAgentTest2/
├── index.html              # メインのHTMLファイル(157行)
├── script.js               # タイピングゲームのメインロジック(359行)
├── style.css               # スタイリング(432行)
├── texts.js               # タイピング用テキストデータ
├── romanization.js         # ローマ字変換ロジック
└── serve.py               # 開発用サーバー

UIはこのようになっています。

タイピングソフトのUI

参考までに、上記タイピングソフトの動作イメージです。

  • 例えば「せんせい」というお題が出題されると、その入力例として灰色で「sensei」と表示される仕組みです。
  • また、下部のキーボードのデザインから押下中のキーが点灯する仕様です。
  • 間違えたキーを押下している場合は視覚的に判別しやすくなっています。
  • お題を最後のキーまで入力出来たら、次のお題に進みます。

※お題自体はtexts.js # タイピング用テキストデータに格納されているためお題の追加や修正が可能です。

現在のコードの問題点

Agentモードによる分析を実行すると、改善点が特定されます。

  1. 設定値のハードコード: タイムアウト、アニメーション時間が直接記述
  2. エラーハンドリング不備: 例外処理が断片的
  3. 可読性の問題: 長いメソッドと責任の混在
  4. テストコードの欠如: 品質保証の仕組みが不在
  5. 保守性の課題: 機能追加時の影響範囲が不明

Agentモードを使った実践的リファクタリング

フェーズ1: 設定値の外部化と基本的なコード整理

Agentモードへの指示例です。

「このタイピングソフトのscript.jsをより保守しやすくリファクタリングしてください。要件で進めてください。」
要件は以下の通りです。
1. ハードコードされた設定値を設定ファイルに外部化
2. メインクラスをGameManagerに改名し、責任を整理
3. エラーハンドリングの改善
4. コードの可読性向上とJSDocコメント追加
5. ファイル数は最小限に抑制(最大2ファイル追加)

Agentモードによる自動分析と計画立案

Agentモードは実行計画を自動生成します。

※下記コードでは絵文字をカットしています

Agent分析結果:

リファクタリング実行計画:
1. 設定値の外部化
   - 新規ファイル: config.js
   - タイムアウト、アニメーション時間、スコア計算定数を分離

2. script.jsの改善
   - TypingTest → GameManager へリネーム  
   - メソッドの責任を明確化
   - エラーハンドリングの追加
   - JSDocコメントの追加

3. 軽量テストの追加
   - 新規ファイル: tests.html
   - 基本的な動作テスト

4. 既存ファイルの更新
   - index.html (新しい設定ファイル読み込み追加)

実行時間予想: 約10-15分
影響ファイル数: 4ファイル (新規2 + 更新2)
複雑性: 現在の約20%削減
ファイル分割: 適度で保守しやすいレベル

実際の自動リファクタリング結果

GitHub Copilot Agentモードによるリファクタリングの結果、プロジェクトのファイル構成がどのように改善されたかを以下に示します。新たに設定ファイルやテストファイルが追加され、既存のファイルも役割に応じて整理されました。

リファクタリング結果
1. 設定ファイルの自動生成(config.js - 抜粋)
/**
 * Configuration file for Japanese Typing Test
 * Contains all configurable settings and constants
 */

/**
 * Game configuration settings
 * @namespace GameConfig
 */
const GameConfig = {
    /** @type {Object} Timer settings */
    timer: {
        /** @type {number} Default test duration in seconds */
        defaultDuration: 30,
        /** @type {number} Timer update interval in milliseconds */
        updateInterval: 1000
    },

    /** @type {Object} Text generation settings */
    textGeneration: {
        /** @type {number} Number of texts to generate per batch */
        textsPerBatch: 50,
        /** @type {number} Text completion delay in milliseconds */
        completionDelay: 200
    },

    /** @type {Object} UI feedback settings */
    feedback: {
        /** @type {number} Error highlight duration in milliseconds */
        errorHighlightDuration: 200,
        /** @type {number} Shake animation duration in milliseconds */
        shakeAnimationDuration: 500,
        /** @type {number} Auto-focus delay in milliseconds */
        autoFocusDelay: 100
    },

    /** @type {Object} Calculation settings */
    calculations: {
        /** @type {number} Characters per word for WPM calculation */
        charactersPerWord: 5
    },

    /** @type {Object} CSS class names */
    cssClasses: {
        gameStates: {
            waiting: 'game-waiting',
            active: 'game-active',
            finished: 'game-finished'
        },
        textHighlight: {
            correct: 'char-correct',
            current: 'char-current',
            incorrect: 'char-incorrect'
        },
        keyboard: {
            active: 'active',
            correct: 'correct',
            incorrect: 'incorrect'
        }
    },
2. メインクラスの改善(script.js - 抜粋)
/**
 * タイピングゲーム管理クラス
 * @class GameManager
 */

class GameManager {
    constructor() {
        try {
            this.initializeGame();
            this.setupEventListeners();
            this.generateNewTexts();
            this.focusInput();
        } catch (error) {
            this.handleError('INITIALIZATION_FAILED', error);
        }
    }
    
    /**
     * ゲームの初期化
     */
    initializeGame() {
        // ゲーム状態の初期化
        this.gameState = 'waiting';
        this.timeLeft = GameConfig.GAME_DURATION;
        this.resetStats();
        
        // DOM要素の取得(エラーハンドリング付き)
        this.elements = this.initializeElements();
        this.timer = null;
    }
    
    /**
     * DOM要素の安全な取得
     * @returns {Object} DOM要素のオブジェクト
     */
    initializeElements() {
        const elementIds = [
            'timer', 'wpm', 'accuracy', 'errors',
            'currentText', 'romajiDisplay', 'userInput', 'hiddenInput',
            'startBtn', 'resetBtn', 'results'
        ];
        
        const elements = {};
        
        elementIds.forEach(id => {
            const element = document.getElementById(id);
            if (!element) {
                throw new Error(`${GameConfig.ERROR_MESSAGES.DOM_ELEMENT_NOT_FOUND}: ${id}`);
            }
            elements[id] = element;
        });
        
        return elements;
    }
    
    /**
     * 統計データのリセット
     */
    resetStats() {
        this.currentTextIndex = 0;
        this.currentTexts = [];
        this.userInput = '';
        this.startTime = null;
        this.endTime = null;
        this.totalCharacters = 0;
        this.correctCharacters = 0;
        this.errorCount = 0;
        this.completedTexts = 0;
        this.wpm = 0;
        this.accuracy = 100;
    }
    
    /**
     * ゲーム開始処理
     */
    startGame() {
        if (this.gameState !== 'waiting') return;
        
        this.safeExecute(() => {
            this.gameState = 'active';
            this.startTime = Date.now();
            this.timeLeft = GameConfig.GAME_DURATION;
            
            // UI状態の更新
            this.updateGameState('active');
            this.startTimer();
        }, 'ゲーム開始エラー');
    }
    
    /**
     * エラーハンドリング
     * @param {string} context - エラーコンテキスト
     * @param {Error} error - エラーオブジェクト
     */
    handleError(context, error) {
        console.error(`[${context}]`, error);
        
        // ユーザーフレンドリーなエラー表示
        const message = GameConfig.ERROR_MESSAGES[context] || 
                       '予期しないエラーが発生しました。ページを再読み込みしてください。';
        
        this.showErrorNotification(message);
    }
    
    /**
     * エラー通知の表示
     * @param {string} message - エラーメッセージ
     */
    showErrorNotification(message) {
        // 既存のエラー通知を削除
        const existingNotification = document.querySelector('.error-notification');
        if (existingNotification) {
            existingNotification.remove();
        }
        
        // 新しいエラー通知を作成
        const notification = document.createElement('div');
        notification.className = 'error-notification';
        notification.innerHTML = `

フェーズ2: 軽量テストの追加

Agentモードで、指示に基づき軽量なテストコードを自動生成しました。

以下に、完成したテストスイートの概要と、実装されたテスト項目を示します。これらのテストは、リファクタリング後のコードが意図した通りに動作することを検証するために役立ちます。

Agentモードへの指示例です。

「リファクタリングしたコードに対して軽量なテストコードを作成してください。
複雑なテストフレームワークは使わず、HTMLファイル内でシンプルなテストを実行できるようにしてください。」

テスト実行結果
自動生成された軽量テストコード(tests.html)
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>タイピングゲーム テスト</title>
    <style>
        .test-container {
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            font-family: Arial, sans-serif;
        }
        .test-result {
            margin: 10px 0;
            padding: 10px;
            border-radius: 5px;
        }
        .test-pass {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .test-fail {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        .test-summary {
            margin-top: 20px;
            padding: 15px;
            background-color: #e2e3e5;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <div class="test-container">
        <h1>タイピングゲーム 自動テスト</h1>
        <div id="test-results"></div>
        <div id="test-summary"></div>
        <button onclick="runAllTests()">テスト実行</button>
    </div>

    <script src="config.js"></script>
    <script>
        class TestRunner {
            constructor() {
                this.testResults = [];
            }
            
            /**
             * テストの実行
             * @param {string} testName - テスト名
             * @param {Function} testFunction - テスト関数
             */
            test(testName, testFunction) {
                try {
                    const result = testFunction();
                    if (result) {
                        this.testResults.push({ name: testName, status: 'PASS', message: '' });
                    } else {
                        this.testResults.push({ name: testName, status: 'FAIL', message: 'テストが失敗しました' });
                    }
                } catch (error) {
                    this.testResults.push({ name: testName, status: 'FAIL', message: error.message });
                }
            }
            
            /**
             * テスト結果の表示
             */
            displayResults() {
                const resultsContainer = document.getElementById('test-results');
                const summaryContainer = document.getElementById('test-summary');
                
                resultsContainer.innerHTML = '';
                
                let passCount = 0;
                let failCount = 0;
                
                this.testResults.forEach(result => {
                    const resultDiv = document.createElement('div');
                    resultDiv.className = `test-result test-${result.status.toLowerCase()}`;
                    resultDiv.innerHTML = `
                        <strong>${result.status}</strong>: ${result.name}
                        ${result.message ? `<br><small>${result.message}</small>` : ''}
                    `;
                    resultsContainer.appendChild(resultDiv);
                    
                    if (result.status === 'PASS') {
                        passCount++;
                    } else {
                        failCount++;
                    }
                });
                
                summaryContainer.innerHTML = `
                    <div class="test-summary">
                        <h3>テスト結果サマリー</h3>
                        <p>実行: ${this.testResults.length}, 成功: ${passCount}, 失敗: ${failCount}</p>
                        <p>成功率: ${Math.round((passCount / this.testResults.length) * 100)}%</p>
                    </div>
                `;
            }
        }
        
        function runAllTests() {
            const runner = new TestRunner();
            
            // 設定値のテスト
            runner.test('GameConfig.GAME_DURATION が正しく設定されている', () => {
                return GameConfig.GAME_DURATION === 30;
            });
            
            runner.test('GameConfig.ERROR_MESSAGES が存在する', () => {
                return GameConfig.ERROR_MESSAGES && 
                       typeof GameConfig.ERROR_MESSAGES === 'object';
            });
            
            runner.test('GameConfig.CSS_CLASSES が正しく設定されている', () => {
                return GameConfig.CSS_CLASSES.CHAR_CORRECT === 'char-correct' &&
                       GameConfig.CSS_CLASSES.CHAR_CURRENT === 'char-current';
            });
            
            // DOM要素の存在テスト(メインページ用)
            runner.test('必要な設定値がすべて定義されている', () => {
                const requiredKeys = [
                    'GAME_DURATION', 'MAX_TEXTS_COUNT', 'ANIMATION_DURATION',
                    'ERROR_FLASH_DURATION', 'CHARACTERS_PER_WORD'
                ];
                return requiredKeys.every(key => GameConfig[key] !== undefined);
            });
            
            // 設定値の型チェック
            runner.test('数値設定が正しい型である', () => {
                return typeof GameConfig.GAME_DURATION === 'number' &&
                       typeof GameConfig.MAX_TEXTS_COUNT === 'number' &&
                       GameConfig.GAME_DURATION > 0 &&
                       GameConfig.MAX_TEXTS_COUNT > 0;
            });
            
            runner.displayResults();
        }
        
        // ページ読み込み時に自動実行
        document.addEventListener('DOMContentLoaded', runAllTests);
    </script>
</body>
</html>

リファクタリング結果の分析と検証

今回のGitHub Copilot Agentモードを使ったリファクタリングにより、当初の課題がどのように改善されたのか、定量的な指標と新しいプロジェクト構造を用いて詳しく分析します。

定量的改善効果

まず、コードの品質がどのように向上したかを具体的な数値で確認します。

ファイル数やコードの行数を比較することで、リファクタリングが単なる見た目の変更ではなく、保守性や効率性の向上に繋がったことが分かります。

コード品質指標の改善
指標 リファクタリング前 リファクタリング後 改善率
ファイル数 6ファイル 8ファイル 適度な分離
最大ファイルサイズ 359行 280行 22%削減
設定値の集約 分散 一元管理 完全改善
エラーハンドリング 断片的 統一化 完全改善
テストカバレッジ 0% 基本機能カバー 新規追加

改善されたプロジェクト構造

リファクタリングの結果、プロジェクトのファイル構成は以下のように整理されました。config.jsやtests.htmlといった新しいファイルが追加され、それぞれの役割が明確になっています。

CodingAgentTest2/
├── index.html              # メインHTML(更新済み)
├── script.js               # 改善されたGameManagerクラス(280行)
├── config.js               # 設定管理(新規追加)
├── tests.html              # 軽量テストスイート(新規追加)
├── style.css               # スタイリング(CSS追加)
├── texts.js               # タイピング用データ
├── romanization.js         # ローマ字変換
└── serve.py               # 開発用サーバー

Agentモード活用のベストプラクティス

効果的な指示の出し方

1. 現実的な制約の設定
  • ファイル分割の上限を明示(「最大2ファイル追加」など)
  • 既存の動作を維持することを要求
  • 段階的なアプローチを指定
2. 具体的な品質要求
  • エラーハンドリングの標準化
  • JSDocコメントの追加
  • 設定値の外部化
3. 保守性重視の設計
  • 過度な抽象化を避ける
  • チームが理解しやすい構造
  • 将来の拡張を考慮した設計

チーム開発での活用戦略

1. 段階的導入プロセス
  1. 小規模な改善から開始
  2. チームレビューで品質確認
  3. 動作テストの実施
  4. 成功パターンの文書化
2. 品質基準の統一
  • リファクタリング前後のメトリクス比較
  • コードレビューチェックリストの作成
  • 継続的改善のプロセス確立

※チーム活用ワークフロー図の画像挿入

トラブルシューティング

Agentモード特有の問題と解決法

問題1: 過度なファイル分割による複雑化
  • 対策: 事前にファイル数の上限を指定
  • 検証: リファクタリング後の理解しやすさを評価
  • 改善: 必要に応じて再統合を検討
問題2: 既存機能の意図しない変更
  • 対策: 動作保証を明確に指示
  • 検証: リファクタリング前後の動作比較テスト
  • 回復: Gitでの段階的コミットとロールバック
問題3: 生成コードの品質のばらつき
  • 対策: 具体的なコーディング規約の指定
  • 検証: ESLintやPrettierによる自動チェック
  • 改善: 継続的なフィードバックとモード調整

品質保証のチェックリスト

  1. 機能テスト: すべての既存機能が正常動作
  2. パフォーマンステスト: 応答速度の劣化なし
  3. コード品質: 可読性と保守性の向上
  4. 設定管理: 設定値の一元化
  5. エラーハンドリング: 統一されたエラー処理

今後の展望と発展可能性

Agentモードの進化予想

Agentモードは今後も進化を続け、より自律的かつ高度な開発支援が可能になると思われます。

将来的には、プロジェクトの規模を自動で認識して適切な分割レベルを判断したり、過去のコードレビューからチームの規約を学習してそれに沿ったコードを生成したりするようになるかもしれません。

また、リスクを最小限に抑えるための段階的なリファクタリング手順を提案したり、既存のAPIや動作の互換性を完全に保証したりする機能も期待されます。

実用的な開発フローの確立

定期的なコード品質の改善を自動で行う継続的なリファクタリングサイクルや、ベストプラクティスを自動で適用してチームの知識を標準化することが可能になるでしょう。

さらに、技術的負債となる問題点を自動的に特定し、その改善を提案する機能も開発を大きく効率化する鍵となります。

まとめ

今回のGitHub Copilot Agentモードを活用した実践的なリファクタリングは、単にコードを整理するだけでなく、プロジェクト全体の健全性を大幅に向上させました。

この取り組みを通じて、保守性が高く、将来の機能拡張にも柔軟に対応できるコードベースを効率的に構築できました。

主な成果

GitHub Copilot Agentモードを活用した実践的なリファクタリングにより、過度な分割を避けた現実的なコード構造への改善を実現しました。これにより、設定は一元管理され、エラーハンドリングが統一されるなど、保守性が大幅に向上しました。

また、JSDocコメントや軽量テストが追加されたことで、コードの品質も向上し、結果として手動作業と比較して約70%の時間短縮という開発効率の向上をもたらしました。

重要な学び

  • 現実的な制約を設けることで実用的な成果が得られる
  • 段階的アプローチにより安全なリファクタリングが可能
  • チームの理解しやすさを重視した設計が重要
  • 継続的な改善サイクルの確立が成功の鍵

VS CodeとGitHub Copilot Agentモードの組み合わせにより、大規模で複雑なリファクタリングではなく、日常的で実用的なコード改善を効率的に行えるようになりました。

これにより開発者は品質向上と新機能開発のバランスを保ちながら、持続可能な開発を実現できます。

執筆担当者プロフィール
前田 将太

前田 将太(日本ビジネスシステムズ株式会社)

BS事業本部 先端技術部に所属。 AI,ComputerVison,VRに興味があります。 前職ではRPAで処理自動化・業務改善を担当していました。

担当記事一覧