自己紹介
株式会社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日合計)ですか?
- タイマー一時停止中は経過時間に含めますか?
- アプリがバックグラウンドの間も計測は続きますか?
- 通知はタイマー画面を開いていない時のみ?画面を開いている時はどうしますか?
- 目標未達でタイマーを止めた場合、達成扱いにしますか?
エッジケース:
- 機内モードで通知が届かないケース
- 目標時間を達成後、さらに継続した場合の振る舞い
- 複数日にまたがるセッション
この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の応答品質が、別物に変わるはずです。
参考
- Claude Code 公式ドキュメント
- Claude Code Best Practices (Anthropic)
- CLAUDE.md について(公式)
- SwiftUI 公式ドキュメント
- UserNotifications フレームワーク
- Swift API Design Guidelines
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!