1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIスロップ:Claude Code と一緒にメモアプリを TDD で作った話

1
Last updated at Posted at 2026-05-17

本当の'はじめに'

これは、私の初めてのClaude codeによるバイブコーディングの記録です。

日頃はテクニカルライターとしてマニュアルばかり書いていて、ほとんどプログラミングはしないのですが、Claude Codeなどによるバイブコーディングの評判があまりにも高く、ちょっと無視できなくなってきたので自分でも試してみることにしました。

作成したメモアプリは、こちらにあります。
メモはブラウザのローカルストレージに保存されます。

post_vibe_codeing_by_claude_code.png

これだけのアプリが2時間くらいでできました。私のプログラミング能力は非常に低いので、ここまでの機能を短時間で作ってくれたのはなんだかすごい。テストコードなど、丁寧に用意できたのも初めてでした。

ただ、開発のスタートはスッキリとはいきませんでした。このあと開発の流れを説明しますが、その前段階としてClaudeCodeとこんなやり取りをしています。

  1. ワタシ:シンプルなメモアプリを作りたい

  2. Claude:わかりました。htmlファイル1つで完結するアプリを作ります

  3. ワタシ:必要な機能をリストアップして

  4. Claude:新規作成、編集、削除です。作成してもいいですか

  5. ワタシ:それをドキュメントに出力して

  6. Claude:design.mdを作成しました。コードを作成してもいいですか

  7. ワタシ:機能がちゃんと動作するかテストしながらやりたい

  8. Claude:わかりました。テストコードを書いてもいいですか

  9. ワタシ:作業手順をドキュメントに出力して

  10. Claude: procedure.mdを作成しました。すべてのテストコードを作成してもいいですか

  11. ワタシ:1機能を作成するたびにテストして、それを繰り返したい

  12. Claude:TDDですね。わかりました。最初のテストを書いてもいいですか

  13. ワタシ:その作業を記録して

  14. Claude: procedure.mdを更新しました。最初のテストコードを作成してもいいですか

  15. ワタシ:コードを記録するタイミングは?

  16. Claude:1機能ができたら、コミットします

  17. ワタシ:その作業を記録して

  18. Claude:procedure.mdを更新しました。最初のテストコードを作成してもいいですか

  19. ワタシ:リポジトリを作成して、現在のドキュメントを保存して

  20. Claude:リポジトリを作成して、現在のドキュメントを保存しました。最初のテストコードを作成してもいいですか

  21. ワタシ:最初のテストコードを作成して

  22. Claude:わかりました。最初のテストコードを作成します!!

という感じで、アプリの設計書作業手順を用意してから、実際のコードを書いてもらいました。なんだか、やる気満点の犬みたいです。

途中でもドキュメントの見直しや、フォルダの分割、テストが妥当かどうかのチェックを指示しています。

さて、「はじめに」以降の文章は、開発の様子を技術ブログの記事としてClaude Codeに出力してもらったテキストをそのまま貼り付けてあります。

すでに世の中にはClaude Codeやってみました的な記事がたくさんありますが、どれも同じようなコンテンツになっています。このテキストもよく似ています。プログラミングする上での良かったことなども書いてあり、これも私ではなくClaude Codeが出力しています。ただし、"気を付けたこと"には私の指示が反映されています。

皆さんの役に立つかどうか分かりませんが、ClaudeCodeでプログラミングを始めるときの参考になれば幸いです。

はじめに

「シンプルなメモアプリを作ってみよう」という動機から始まったこのプロジェクト。フレームワークもサーバーも使わず、HTML・CSS・バニラ JavaScript だけで動くアプリを、テスト駆動開発(TDD)で仕上げた記録です。

開発は Claude Code(AI コーディングアシスタント)と一緒に進めました。コードを書くだけでなく、テスト設計・ドキュメント管理・git 運用まで一貫して Claude に手伝ってもらった体験は、思いのほか学びが多いものでした。


作ったもの

ブラウザで動くメモアプリです。index.html を開くだけで使えます。

実装した機能:

機能 状態
新規メモの作成 ✅ 完成
メモの一覧表示(左サイドバー) ✅ 完成
タイトルと本文の編集 ✅ 完成
データの自動保存(localStorage) ✅ 完成
メモの削除 ✅ 完成

技術スタック:

  • HTML / CSS(外部ライブラリなし)
  • バニラ JavaScript
  • localStorage(データ永続化)

ファイル構成

my_1st_project/
  index.html              # アプリ本体
  app.js                  # データ操作ロジック(純粋関数)
  ui.js                   # UI ロジック(DOM 操作)
  design.md               # 設計書
  procedure.md            # 作業手順
  README.md               # 利用者向け説明
  test/
    test.html             # 全テスト一括表示(メインランナー)
    test_runner.js        # テストランナー共通ロジック
    test_create.html      # 新規作成テスト(ロジック)
    test_create_ui.html   # 新規作成テスト(UI)
    test_edit.html        # 編集・保存テスト(ロジック)
    test_edit_ui.html     # 編集・保存テスト(UI)
    test_delete.html      # 削除テスト(ロジック)
    test_delete_ui.html   # 削除テスト(UI)
    cases/
      cases_create.js     # 新規作成テストケース(ロジック)
      cases_create_ui.js  # 新規作成テストケース(UI)
      cases_edit.js       # 編集テストケース(ロジック)
      cases_edit_ui.js    # 編集テストケース(UI)
      cases_delete.js     # 削除テストケース(ロジック)
      cases_delete_ui.js  # 削除テストケース(UI)
      ...

開発の流れ

メモアプリを開発した流れ

1. 設計・方針決め

  • 技術スタックを選定(HTML / CSS / バニラ JS / localStorage)
  • 実装する機能を決定(作成・一覧・編集・削除)
  • TDD(テスト駆動開発)で進めることを決定
  • design.md・procedure.md に設計と手順を記録
  • git リポジトリを初期化してコミット

2. テストインフラの整備

  • テストランナー(test_runner.js)をスクラッチで作成
  • test()・assert()・showSummary() の3関数で構成
  • ブラウザで HTML を開くだけで結果が見える仕組み

3. 機能を1つずつ TDD で実装(以下を4回繰り返す)

  • テストコードを先に書く
  • 機能を実装する
  • テストが通ることを確認する
  • コミットする

実装順:新規作成 → 一覧表示 → 編集・保存 → 削除

4. バグ修正

  • Date.now() による ID 衝突 → crypto.randomUUID() に変更
  • 同一ミリ秒内の updated 更新が検知できない → テスト側で古い日時をセット
  • 壊れた JSON で loadMemos() がクラッシュ → try/catch でフォールバック

5. コード構造のリファクタリング

  • ロジック(app.js)と UI(ui.js)を分離
  • テストファイルを test/ フォルダに移動
  • テストケースを test/cases/*.js に切り出し(ロジック・UI それぞれ)
  • test.html 1ページで全テスト結果を一覧表示できるように整備

6. テストの妥当性監査

  • 新規作成・一覧表示の UI テストが抜けていたことを発見 → 追加
  • localStorage の後処理(cleanup)が不統一だったことを発見 → 統一
  • 壊れた JSON のエラーハンドリングテストを追加

7. ドキュメント整備

  • design.md:機能状態・UI 構成・未実装機能の仕様を記録
  • procedure.md:TDD 手順・チェックリスト・ブランチ運用・テスト失敗時の切り分け手順などを整備
  • README.md:利用者向けの使い方を記録
  • セッション再開方法を procedure.md に追記

8. 未実装(次のステップ)

  • レスポンシブデザイン(モバイル対応)
  • Undo 機能(Ctrl+Z・履歴スタック)
  • ミニマルデザイン

TDD で進めた開発フロー

機能ごとにこのサイクルを繰り返しました:

  1. テストを書く
  2. 機能を実装する
  3. テストが通ることを確認する
  4. ドキュメントを更新する
  5. コミットする

最初はテストを書かずに実装から始めたくなりますが、「テストを先に書く」という制約がコードの設計をよくしてくれました。「この関数、テストしやすい形になっているか?」という問いが、自然と責任の分離を促します。


ロジックとUIの分離

このプロジェクトで一番こだわったのが、データ操作ロジック(app.js)と UI ロジック(ui.js)の明確な分離です。

app.js — 純粋関数のみ

function createMemo() {
  return {
    id: crypto.randomUUID(),
    title: '',
    body: '',
    updated: new Date().toISOString(),
  };
}

function updateMemo(memos, id, changes) {
  return memos.map(m =>
    m.id === id ? { ...m, ...changes, updated: new Date().toISOString() } : m
  );
}

function deleteMemo(memos, id) {
  return memos.filter(m => m.id !== id);
}

function loadMemos() {
  try {
    return JSON.parse(localStorage.getItem('memos') || '[]');
  } catch {
    return [];
  }
}

app.js の関数は DOM に一切触れません。入力と出力が明確な純粋関数(または副作用が局所的な関数)として書いています。これにより、ブラウザなしでもロジックを単体テストできます。

ui.js — DOM 操作はここだけ

function onNewMemo() {
  const memo = createMemo();
  memos = addMemo(memos, memo);
  saveMemos(memos);
  selectMemo(memo.id);
}

function onInput() {
  if (!currentId) return;
  memos = updateMemo(memos, currentId, {
    title: document.getElementById('title-input').value,
    body: document.getElementById('body-input').value,
  });
  saveMemos(memos);
  renderList();
}

UI 側は app.js の関数を呼ぶだけ。ロジックの重複がなく、変更が局所化されます。


テストの設計

ライブラリなしのテストランナー

外部のテストフレームワークは使わず、スクラッチで作りました。

// test_runner.js
let passed = 0;
let failed = 0;

function test(description, fn) {
  const div = document.createElement('div');
  try {
    fn();
    div.className = 'pass';
    div.textContent = `✓ ${description}`;
    passed++;
  } catch (e) {
    div.className = 'fail';
    div.textContent = `✗ ${description}${e.message}`;
    failed++;
  }
  output.appendChild(div);
}

function assert(condition, message) {
  if (!condition) throw new Error(message || 'assertion failed');
}

test() 関数が例外をキャッチして結果を DOM に書き出す、というシンプルな仕組みです。ブラウザでテスト HTML を開くだけで結果が見えます。

ロジックテストとUIテストの2種類

種類 テスト対象
ロジックテスト app.js の関数 updateMemo() を呼んだ結果を確認
UIテスト DOM の動作 ボタンを .click() して表示を確認

UIテストでは display: none にした DOM を用意し、実際にイベントを発火させて検証します。

// cases_create_ui.js の例
test('新規メモボタンをクリックするとメモが1件増える', () => {
  setup();
  document.getElementById('new-memo-btn').click();
  assert(memos.length === 1, `memos.length が ${memos.length}`);
});

テストケースを cases/ に分離する工夫

最初は各テスト HTML ファイルにテストコードを直書きしていましたが、「test.html 1ページで全テスト結果を一覧したい」という要件が出てきたときに問題になりました。

解決策として、テストケースを .js ファイルとして cases/ フォルダに切り出しました。

<!-- test.html(全テスト一括) -->
<script src="cases/cases_create.js"></script>
<script src="cases/cases_list.js"></script>
<script src="cases/cases_edit.js"></script>
<script src="cases/cases_delete.js"></script>
<script src="cases/cases_create_ui.js"></script>
<!-- ... -->
<!-- test_create.html(新規作成だけ) -->
<script src="cases/cases_create.js"></script>

同じ cases_create.js を両方から読み込むため、テストコードの重複がなくなりました。


遭遇したバグと解決策

バグ1:id が同じ テストが失敗

✗ 2つのメモの id は異なる — id が同じ

原因: 最初は Date.now() を ID に使っていたため、高速に2つ作ると同じ値になった。

解決: crypto.randomUUID() に切り替え。ブラウザ標準 API で衝突確率が無視できるほど低い UUID を生成できます。

// Before
id: Date.now()

// After
id: crypto.randomUUID()

バグ2:updated が変わっていない テストが失敗

✗ updateMemo() で updated が更新される — updated が変わっていない

原因: createMemo()updateMemo() が同一ミリ秒内に実行されたため、updated の値が変わらなかった。

解決: テスト側で updated を意図的に過去の日時にセットしてから updateMemo() を呼ぶ。

test('updateMemo() で updated が更新される', () => {
  let memos = addMemo(initMemos(), createMemo());
  const id = memos[0].id;
  // 既知の古い日時にセット
  memos[0] = { ...memos[0], updated: '2000-01-01T00:00:00.000Z' };
  memos = updateMemo(memos, id, { title: '変更後' });
  assert(memos[0].updated !== '2000-01-01T00:00:00.000Z', 'updated が変わっていない');
});

バグ3:壊れた JSON で loadMemos() がクラッシュ

✗ localStorage に壊れた JSON があっても loadMemos() は空配列を返す

原因: JSON.parse('INVALID_JSON') が例外を投げるが、ハンドリングしていなかった。

解決: try/catch で空配列にフォールバック。

function loadMemos() {
  try {
    return JSON.parse(localStorage.getItem('memos') || '[]');
  } catch {
    return [];
  }
}

git 運用

コミットメッセージは prefix を使って種類を明示しました。

prefix 意味
feat: 新機能
test: テスト追加・修正
fix: バグ修正
docs: ドキュメント
refactor: リファクタリング

実際のコミット履歴:

ab1f9da docs: 作業再開手順を procedure.md に追記
184a2df refactor: テストケースを cases/ に分離し test.html で一括表示
685c96f fix: loadMemos() の壊れた JSON に対するエラー処理を追加
f28d709 feat: メモの削除を実装
09ed10a feat: メモ編集UIを実装し、UIテストを追加
b9d51fd feat: メモの編集・保存を実装
cd66218 feat: メモの一覧表示を実装
22c9295 feat: メモの新規作成を実装
6cce732 test: メモの新規作成テストを追加

「テストを追加」「機能を実装」がコミット単位で分かれていて、TDD のリズムがログから読み取れます。


Claude Code と一緒に作業して気づいたこと

よかった点

  • 設計の言語化が早い。 「ロジックとUIを分ける」「テストケースを共有化する」といった方針を言葉で伝えると、即座に構造に落とし込んでくれる。
  • バグ修正の説明が的確。 テストの失敗メッセージを貼り付けるだけで、原因と解決策をセットで提案してくれる。
  • ドキュメントも同時進行できる。 コードを書きながら design.mdprocedure.md を更新する習慣を自然に組み込めた。

気をつけたこと

  • 「手順のたびに確認して」と明示した。 自動で先へ進みすぎることがあるため、確認ポイントを明確に設定した。
  • コミットは自分で承認する。 「commit して」と言うまで Claude はコミットしない運用にした。

次のステップ

設計書(design.md)には未実装の機能が3つ残っています。

  1. レスポンシブデザイン — モバイル(600px未満)では一覧 or 編集を切り替え表示
  2. Undo 機能Ctrl+Z または UNDO ボタンで直前の操作を取り消し(最大20件の履歴スタック)
  3. ミニマルデザイン — 余白を広め・ボーダー最小限・統一されたボタンスタイル

これらも TDD で、テストを先に書いてから実装する予定です。


まとめ

  • 外部ライブラリなしでも、ロジックと UI を分離すればテストは書ける
  • テストランナーもスクラッチで作れるtest()assert() の2関数があれば十分
  • ロジックテスト + UIテストの2層が、バグを早期に発見する
  • TDD のリズム(テスト → 実装 → 確認 → コミット)がコードの品質を自然に高める
  • Claude Code は「実装者」ではなく「ペアプログラマー」として使うと効果的

シンプルなアプリでも、設計・テスト・git 運用を丁寧にやると、学べることが多いと感じました。

1
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?