1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeでの「仕様駆動開発」完全ガイド — 要件定義から実装までブレないワークフロー

1
Posted at

自己紹介

株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。

Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
コーポレートサイト

はじめに

「Claude Codeに頼んだら、なんか思ってたのと違うものができた」

AIコーディングを始めた人なら一度は経験する現象です。私自身、iOSアプリを複数本Claude Codeで開発していますが、初期の頃はノリで「こんな機能作って」と頼んで、後から「あれ、こんな仕様頼んでない…」と手戻りすることが頻発していました。

原因はシンプルで、仕様が曖昧なまま実装を始めているからです。

この記事では、私が実際の個人開発(学習タイマーアプリ、ランキング投票アプリ等)で運用している 「仕様駆動開発(Spec-Driven Development)」 のワークフローを、Claude Code向けに最適化した形で解説します。

この記事の対象者

  • SES・受託開発のエンジニア(1〜5年目)
  • 個人開発でAIを活用したいエンジニア
  • Claude Codeを使っているが「仕様が曖昧なまま実装してしまう」悩みがある人

この記事を読むとわかること

  • なぜAIコーディングで「仕様」が最重要なのか
  • CLAUDE.md・仕様書・プロンプトをどう組み合わせるか
  • 実装前に行う「仕様の壁打ち」の具体手順
  • 仕様と実装の整合性を担保するチェック方法

1. AIコーディングの落とし穴:仕様なき実装の失敗談

実例から始めます。学習タイマーアプリ(StudyStopwatch)で、こんなやり取りをしたことがあります。

失敗例:曖昧な指示

私: 「タイマーが終わったら通知を出す機能を追加して」
Claude Code: → UNUserNotificationCenterで通知実装、完了

一見うまくいったように見えますが、リリース前に致命的な仕様漏れが発覚しました。

観点 私の意図 Claude Codeの実装
通知タイミング タイマー終了時のみ タイマー終了時のみ ✓
バックグラウンド 動作する想定 動作しない(ローカル通知のスケジュール未対応)
デフォルト音 カスタム音(私は指定していないのに)
バッジ 不要 バッジ +1

「通知を出す」という言葉だけで、5つ以上の暗黙の前提があったのです。AIは"察する"のが得意ですが、察した結果が私の意図と合うとは限りません。

教訓

AIは「仕様の穴」を勝手に埋める。だが、その埋め方は私たちの意図と一致しない。

これを防ぐのが「仕様駆動開発」です。


2. 仕様駆動開発とは何か

仕様駆動開発(Spec-Driven Development, SDD)は、実装前に仕様を文書として確定させ、それをAIに「コンテキスト」として渡す開発手法です。

従来のフロー(ノリ駆動)

ユーザー: 「Xを作って」
  ↓
Claude Code: 即実装
  ↓
ユーザー: 「あ、ここ違う」「あれも欲しい」
  ↓
Claude Code: 修正
  ↓
(無限ループ)

仕様駆動のフロー

ユーザー: 「Xを作りたい。仕様を一緒に詰めて」
  ↓
Claude Code: 質問 → 仕様書ドラフト作成
  ↓
ユーザー: レビュー・確定
  ↓
仕様書(Markdown) → コンテキストとして渡す
  ↓
Claude Code: 仕様書に厳密に従って実装
  ↓
仕様 vs 実装の整合性チェック

ポイントは、仕様書を「会話のメモ」ではなく「実行可能な契約書」として扱うことです。


3. ステップ1:仕様書を書く(CLAUDE.mdとの役割分担)

仕様書とCLAUDE.mdは似ているようで役割が違います。

ファイル 役割 寿命
CLAUDE.md プロジェクト全体の規約・前提(コーディング規約、技術スタック等) 長期(プロジェクト全体)
specs/{機能名}.md 特定機能の仕様(画面遷移、データモデル、エッジケース) 機能単位(実装後はアーカイブ)

仕様書テンプレート(実例)

学習タイマーアプリで実際に使っているテンプレートです。

# [機能名] 仕様書

## 1. 目的
この機能で何を実現したいか(1〜2行)

## 2. ユーザーストーリー
「[ユーザー]は、[目的]のために、[機能]を使う」

## 3. 画面・UI
- 画面構成(Figma URLや画像で補足)
- 主要コンポーネント一覧

## 4. データモデル
- 永続化対象(SwiftData @Model等)
- 一時的な状態(@State, @Observable)

## 5. 動作仕様
| 入力 | 期待出力 |
|------|---------|
| ボタンA タップ | 画面Bに遷移 |
| 入力Cが空 | エラー表示 "..." |

## 6. エッジケース
- バックグラウンド復帰時
- 機内モード時
- データ0件時

## 7. 受入条件(Acceptance Criteria)
- [ ] 条件1
- [ ] 条件2

## 8. 対象外(やらないこと)
- 機能X は今回スコープ外

CLAUDE.md側の責務(参考)

CLAUDE.mdには、仕様書では毎回書きたくない共通ルールを集約します。

# プロジェクト設定

## 技術スタック
- Swift 6 / SwiftUI / SwiftData
- iOS 18.0+

## コーディング規約
- force unwrap(!)禁止
- ViewModel は `@Observable @MainActor final class`
- print() 禁止 → Logger(os.log)

## ファイル構成
App/, Models/, ViewModels/, Views/, Services/

こうすると、仕様書が「機能固有のこと」だけに集中できて軽くなります。


4. ステップ2:Claude Codeとの「壁打ち」で仕様を詰める

仕様書を最初から完璧に書く必要はありません。Claude Codeに壁打ち相手になってもらうのがコツです。

プロンプト例(仕様の壁打ちモード)

これから新機能の仕様を一緒に詰めたい。
あなたはシニアiOSエンジニアとして振る舞ってほしい。

【作りたい機能】
学習タイマーに「目標時間」を設定して、達成したら通知する機能

【現在の私の理解】
- ユーザーが目標時間(例: 1時間)を設定
- タイマー開始
- 目標達成で通知

【お願い】
1. 仕様の曖昧な点を質問形式で挙げてほしい
2. 私が見落としているエッジケースを指摘してほしい
3. 質問が出尽くしたら、仕様書テンプレート(specs/timer_goal.md)に
   まとめてほしい

Claude Codeの返答(実際にあったやり取り)

仕様の曖昧な点について、以下を確認させてください。

  1. 目標時間は1セッション単位ですか?それとも累計(1日合計)ですか?
  2. タイマー一時停止中は経過時間に含めますか?
  3. アプリがバックグラウンドの間も計測は続きますか?
  4. 通知はタイマー画面を開いていない時のみ?画面を開いている時はどうしますか?
  5. 目標未達でタイマーを止めた場合、達成扱いにしますか?

エッジケース:

  • 機内モードで通知が届かないケース
  • 目標時間を達成後、さらに継続した場合の振る舞い
  • 複数日にまたがるセッション

この5つの質問に答えるだけで、仕様の95%は固まります。 実装前に1〜2往復だけ壁打ちすると、後の手戻りが圧倒的に減ります。

コツ:質問させるプロンプト

仕様の壁打ちでは「実装する前に質問してほしい」と明示するのが重要です。Claude Codeは指示がないと即実装に走りがちなので、以下のような枕詞を添えます。

まだ実装はしないでください。
仕様の確認だけお願いします。
不明点があれば質問してください。

5. ステップ3:仕様をコンテキストとして渡して実装させる

仕様書が固まったら、いよいよ実装フェーズです。

プロンプト例(実装モード)

specs/timer_goal.md を読んで、仕様に従って実装してください。

制約:
- CLAUDE.md のコーディング規約に厳密に従うこと
- 仕様書の「対象外」セクションの機能は実装しないこと
- 仕様で曖昧な点があれば、勝手に決めずに質問すること
- 実装後、仕様書の「受入条件」に対する対応箇所をコメントで示すこと

ここで重要なのは、「仕様書のどの項目を満たすコードか」をコメントで明示させる点です。

実装例(完全形)

仕様書「目標時間達成で通知」を満たすSwiftUI実装の例を示します。

import Foundation
import SwiftUI
import UserNotifications
import os.log

// MARK: - Logger
private let logger = Logger(subsystem: "com.happyboy1002.StudyStopwatch", category: "TimerGoal")

// MARK: - 仕様書 specs/timer_goal.md 受入条件:
// [✓] AC1: 目標時間に達したら通知を発火する
// [✓] AC2: バックグラウンド時もスケジュール済み通知が届く
// [✓] AC3: タイマー停止時は通知をキャンセルする

@MainActor
@Observable
final class TimerGoalViewModel {
    /// 目標時間(秒)
    var goalSeconds: TimeInterval = 3600
    /// タイマー稼働中フラグ
    private(set) var isRunning: Bool = false
    /// 通知識別子(キャンセル用)
    private let notificationId = "timer.goal.reached"

    /// タイマー開始(仕様書 4.動作仕様: 開始ボタン押下時)
    func start() async {
        guard !isRunning else { return }
        do {
            try await requestAuthorizationIfNeeded()
            try await scheduleGoalNotification(after: goalSeconds)
            isRunning = true
            logger.info("タイマー開始 目標=\(self.goalSeconds, privacy: .public)秒")
        } catch {
            logger.error("通知スケジュール失敗 error=\(error.localizedDescription, privacy: .public)")
        }
    }

    /// タイマー停止(仕様書 4.動作仕様: 停止ボタン押下時)
    func stop() {
        guard isRunning else { return }
        UNUserNotificationCenter.current()
            .removePendingNotificationRequests(withIdentifiers: [notificationId])
        isRunning = false
        logger.info("タイマー停止")
    }

    /// 通知許可リクエスト(仕様書 6.エッジケース: 初回起動時)
    private func requestAuthorizationIfNeeded() async throws {
        let center = UNUserNotificationCenter.current()
        let settings = await center.notificationSettings()
        if settings.authorizationStatus == .notDetermined {
            _ = try await center.requestAuthorization(options: [.alert, .sound])
        }
    }

    /// 目標達成通知のスケジュール(仕様書 AC1, AC2)
    private func scheduleGoalNotification(after seconds: TimeInterval) async throws {
        let content = UNMutableNotificationContent()
        content.title = "目標達成"
        content.body = "目標時間に到達しました"
        content.sound = .default

        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: seconds, repeats: false)
        let request = UNNotificationRequest(identifier: notificationId,
                                            content: content,
                                            trigger: trigger)
        try await UNUserNotificationCenter.current().add(request)
    }
}

// MARK: - View
struct TimerGoalView: View {
    @State private var viewModel = TimerGoalViewModel()

    var body: some View {
        VStack(spacing: 24) {
            Text(viewModel.isRunning ? "計測中" : "停止中")
                .font(.title)

            Stepper(value: $viewModel.goalSeconds, in: 60...7200, step: 60) {
                Text("目標: \(Int(viewModel.goalSeconds / 60))分")
            }
            .disabled(viewModel.isRunning)

            HStack {
                Button("開始") {
                    Task { await viewModel.start() }
                }
                .disabled(viewModel.isRunning)

                Button("停止") {
                    viewModel.stop()
                }
                .disabled(!viewModel.isRunning)
            }
        }
        .padding()
    }
}

#Preview {
    TimerGoalView()
}

ポイント:

  • 各メソッドのコメントに「仕様書のどの項目を満たすか」を明記
  • 受入条件AC1〜AC3をファイル冒頭にチェックリストで記載
  • CLAUDE.md規約準拠(force unwrap無し、@Observable @MainActor、Logger使用)

6. ステップ4:仕様と実装の整合性チェック

実装が終わったら、仕様書を再度Claude Codeに渡して、抜け漏れチェックをさせます

プロンプト例(整合性チェックモード)

specs/timer_goal.md と、実装ファイル(TimerGoalViewModel.swift, TimerGoalView.swift)
を読み比べて、以下を表形式で出力してください。

| 仕様書の項目 | 実装の対応箇所 | 状態(OK / 部分対応 / 未対応) | 備考 |

部分対応・未対応がある場合は、なぜそうなったか推測も書いてください。

出力例

| 仕様書の項目 | 実装の対応箇所 | 状態 | 備考 |
|-------------|--------------|------|------|
| AC1: 目標時間達成で通知 | scheduleGoalNotification() | OK | - |
| AC2: バックグラウンド対応 | UNTimeIntervalNotificationTrigger使用 | OK | ローカル通知のためバックグラウンド動作OK |
| AC3: 停止時の通知キャンセル | stop() / removePendingNotificationRequests | OK | - |
| エッジケース: 機内モード | - | 未対応 | スケジュール自体は成功するが、配信時の状態に依存 |
| 対象外: 累計時間モード | - | OK(対象外) | 仕様通り実装していない |

このチェックを毎機能で行うと、「仕様書に書いたのに実装漏れしている」事故がほぼゼロになります

Gitコミットとの連携

整合性チェックをパスしたら、コミットメッセージにも仕様書を紐付けます。

git commit -m "feat: 目標時間達成通知機能を追加 (specs/timer_goal.md AC1-AC3)"

こうしておくと、後から「この機能はどの仕様書で決めたんだっけ?」を即座に追跡できます。


7. 実プロジェクトでのディレクトリ構成例

私が運用しているiOSアプリの構成は以下の通りです。

StudyStopwatch/
├── CLAUDE.md                    # プロジェクト全体規約
├── specs/                       # 機能別仕様書
│   ├── _template.md             # テンプレート
│   ├── archive/                 # 実装済み(履歴)
│   │   └── timer_goal.md
│   └── in_progress/             # 仕様策定中
│       └── widget_support.md
├── App/
├── Models/
├── ViewModels/
├── Views/
└── Services/

実装が完了したら in_progress/ から archive/ に移動するだけのシンプルな運用です。


8. まとめ:仕様駆動開発のメリット

仕様駆動開発をClaude Codeと組み合わせると、以下のメリットがあります。

1. 手戻りが激減する

仕様の壁打ちで5分使うと、実装後の修正で1〜2時間使う分が削減できます。

2. AIの「察し」に依存しなくなる

暗黙の前提を文書化することで、AIの解釈ブレが消えます。

3. レビュー・引き継ぎが楽

仕様書があれば、自分自身の半年後(人間の記憶力は信用できない)や、チームメンバーへの引き継ぎが圧倒的にスムーズです。

4. テストケースの素材になる

受入条件(AC)はそのまま単体テスト・UIテストの題材になります。

5. AIが「仕様逸脱」しにくくなる

「specs/xxx.md に従って」という制約があるだけで、AIの暴走確率が体感的に大幅に下がります。

最後に

「ノリでAIに頼む」フェーズから一歩進みたい人にとって、仕様駆動開発は最も投資対効果が高いプラクティスのひとつです。

明日からの開発で、ぜひ「実装前に仕様書1ファイル」を試してみてください。Claude Codeの応答品質が、別物に変わるはずです。


参考


@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?