壊れないようにアプリを育てる|機能追加・役割分け・動作確認の習慣【入門6】

バイブコーディング

壊れないようにアプリを育てる|機能追加・役割分け・動作確認の習慣【入門6】

最初の小さなアプリが動くと、「あれもこれも足したい」と欲が出てくる。ところが、機能を足すたびに、別の場所が壊れ始める。AIに直させると、また別の所が壊れる。この“もぐら叩き地獄”こそ、未経験者が完成の一歩手前で力尽きる最大の壁だ。この記事は、その壁を越えて「壊れないように育てる」3つの習慣を渡す。

なぜ、足すたびに壊れるのか

理由はシンプルだ。一度に大きく変えすぎているから。AIに「あれもこれも」とまとめて頼むと、コードのあちこちを同時に書き換え、どこかで噛み合わせがズレる。そして壊れたとき、変えた場所が多すぎて原因が特定できない。育て方のコツは、この逆を徹底することに尽きる。

習慣1:機能は「1つ足して、動かして、また1つ」

もう耳にタコかもしれないが、これがすべての土台だ。「削除ボタン」と「並び替え」と「色変更」を一度に頼まない。1つ足したら必ず動かして確かめ、OKなら次の1つへ。

「今うまく動いています。次は1つだけ、メモを保存して、ページを閉じても消えないようにしてください。他の部分は変えないでください。」

「他の部分は変えないで」のひと言が効く。変更範囲を狭く保てば、もし壊れても「今足した1つ」が原因だと即わかる。地味な一言だが、これを付けるかどうかで壊れ方の広がりがまるで違う。

例えば、メモ帳を育てるならこんな順番になる。①保存機能(閉じても消えない)→ ②削除ボタン → ③日付の自動記録 → ④古い順・新しい順の並び替え → ⑤検索欄。1つ終わるたびに動かして確かめる。⑤まで来たころには、最初のメモ帳が「ちゃんとした道具」の顔になっているはずだ。逆に、この5つを最初の1回でまとめて頼むと、どこかが壊れたとき5つ全部が容疑者になる。順番に足せば、容疑者は常に1人。この差が、完成するかどうかを分ける。

足す順番は「使う頻度」で決める

「あれもこれも」の中から次の1つを選ぶ基準も、簡単な物がある。自分が毎日触る機能から先に足す。メモ帳なら、見た目のテーマ切り替えより先に保存機能。理由は2つで、毎日使う機能ほど不具合にすぐ気づけること、そして「便利になった」実感が続けるモチベーションになることだ。逆に、使うか分からない機能(エクスポート、共有、多言語対応…)は、欲しくなった日に足せばいい。想像上の必要のために作った機能は、たいてい一度も使われない。

習慣2:壊れたら戻れるように、「動いた版」を残す

育てる前に、今動いているコードを別名で保存しておく。たとえば memo.html をコピーして memo_ok_0601.html のように日付付きで残す。これだけで、「新しい変更で壊れた」ときにいつでも動く版に戻れる安心が手に入る。

慣れてきたら、プロが使う「Git(ギット)」というバージョン管理の仕組みに進むと、もっと賢く戻せるようになる。でも最初は、“動いた版のコピーを残す”だけで十分だ。命綱を持って崖を登る、と思えばいい。

コピーを取るタイミングにも、簡単な目安がある。「機能が1つ増えて、動くのを確かめた直後」だ。壊れているときに取っても意味がなく、何度も直した後だと、どれが最後に動いた版か分からなくなる。「動いた→即コピー→次の1つへ」をワンセットの癖にすると、戻れる地点が自動的に増えていく。ファイルが溜まってきたら、古い物は old フォルダにでも寄せておけば散らからない。

例えば、復旧の場面はこう回る。並び替え機能を足したら、保存まで壊れた。AIに直させても2回続けて直らない。ここで粘らず、memo_ok_0601.html をコピーして memo.html に戻し、まず「動く状態」を回復する。それからAIに「この動いているコードに、並び替えだけをもう一度、他を変えずに足してください」と仕切り直す。壊れた物を直し続けるより、動く地点まで戻って小さくやり直す方が、結果的に早い。命綱は、持っているだけでは意味がなく、迷わず掴めて初めて役に立つ。

習慣3:大きくなってきたら「役割で分ける」

アプリが育つと、1つのファイルにいろんな処理が混ざって、AIも人も迷い始める。そうなったら、AIにこう頼む。

「コードが大きくなってきました。『見た目を作る部分』『データを保存する部分』『計算する部分』を分けて、後から直しやすい形に整理してください。動きは変えずに、整理だけしてください。」

専門的には「責務の分離」と呼ぶが、要は“係を分けておく”だけ。台所係・会計係・受付係を分けるイメージだ。こうしておくと、一か所いじっても他を巻き込まず、ずっと壊れにくくなる。

「大きくなってきた」のサインも書いておく。①AIに貼るコードのコピーだけで一苦労になってきた、②直してもらった箇所を自分で見つけられない、③「見た目を変えて」と頼んだだけで保存機能まで壊れた。どれか1つでも出たら、整理の頼みどきだ。整理は機能追加と違って見た目が何も変わらないので後回しにしがちだが、部屋の片付けと同じで、散らかり切ってからでは何倍も大変になる。

習慣4:会話が長くなったら「引っ越し」する

1つのチャットで長く作り続けると、AIの返事の質が下がってくることがある。会話が長くなるほど、AIが一度に覚えていられる範囲からはみ出していくためだ。兆候は分かりやすい。「前に決めたことを忘れる」「直したはずの箇所を古い形に戻してくる」。そうなったら、新しいチャットに引っ越す。

「このアプリ作りを新しい会話で続けたいです。引き継ぎ用に、このアプリが何をする物で、どんな機能があって、いまどこまで出来ているかを箇条書きで整理してください。」

この要約と最新のコード全文を新しいチャットに貼れば、すっきりした状態から再スタートできる。長編を1冊で書き切ろうとせず、章を分ける感覚だ。

引っ越し先の最初のひと言はこうなる。「以下は前の会話の要約と、いま動いているコードの全文です。この続きから開発を手伝ってください。(要約)(コード全文)」。要約とコードの2点セットさえ渡せば、AIは前の会話を読んでいなくても、ほぼ同じ精度で続きに入れる。

おまけの習慣:「どこを変えたか」を毎回言わせる

コードを出し直してもらうとき、頼みの最後にこの一言を足す。「変更した箇所と、その理由を箇条書きで教えてください」。返ってきた説明を全部理解できなくてもいい。「今回は保存まわりだけ触ったんだな」と分かるだけで、壊れたときに疑う場所が絞れる。入門5でやった「直前に何を変えたか」の記録が、これで会話の中に自動的に残っていく。

育てるほど効いてくる、あなたの仕事

機能が増えるにつれ、AIの側から質問される場面が増えてくる。「削除のとき、確認ダイアログを出しますか?」「並び替えの設定は、次に開いたときも維持しますか?」。ここで決めるのはあなただ。コードはAIが書けても、「このアプリはどう振る舞うべきか」はあなたにしか決められない。あなたはこのアプリの、唯一の設計者であり最初のユーザーなのだから。

例えば「確認ダイアログを出すか」なら、消して困るデータかどうか、で決まる。メモ1行なら出さない方が軽快で、日記のような長文なら出す方が安心。迷ったら、自分が毎日使う場面を思い浮かべて答える。この「使う場面から決める」感覚が、育てる段階でいちばん伸びる筋肉になる。

育て切ると、メモ帳が「タスクボード」になる

メモ帳と同じ「1つ足して、動かして、また1つ」を続けると、行き着く先はメモ帳だけではない。下は、同じやり方で育てた小さなタスクボードだ。やることを書いて並べ、手をつけたら「進行中」へ、終わったら「完了」へずらしていく。作りの土台は、今日のメモ帳と地続きになっている。

ToDo・進行中・完了の3列にタスクカードが並ぶWebのタスクボード
「今日やること3つ」から始めたToDoも、この習慣で育て続けると、ToDo・進行中・完了の3列を持つタスクボードになる。カードには日付やラベルも付く。
カードをドラッグして別の列へ移動している最中のタスクボード
カードをつかんで、別の列へ動かしている場面。この「つかんで動かす」操作も、削除ボタンや保存と同じように、1つずつ足していけば自分の手で作れる。
タスク名・カテゴリー・日付を入力してタスクを登録するフォーム
「+ タスクを追加」から、タスク名・カテゴリー・日付を選んでタスクを登録できる。カテゴリーで色分けされ、いらなくなったカードは右上の「×」で削除する。

このアプリの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>
  *{ box-sizing:border-box; }
  body{ margin:0; font-family:"Hiragino Kaku Gothic ProN","Yu Gothic",sans-serif;
        background:#f4f6fa; color:#2b2f38; padding:30px 26px; }
  h1{ font-size:23px; margin:0 0 4px; }
  .sub{ color:#8a92a3; font-size:13px; margin:0 0 24px; }
  .board{ display:flex; gap:18px; align-items:flex-start; }
  .col{ flex:1; background:#eef1f6; border-radius:14px; padding:12px;
        min-height:150px; display:flex; flex-direction:column; }
  .col.drag-over{ background:#e3ecff; outline:2px dashed #9db8ff; }
  .col-head{ display:flex; align-items:center; gap:8px; font-weight:bold;
        font-size:15px; padding:6px 6px 12px; }
  .dot{ width:10px; height:10px; border-radius:50%; }
  .c-todo .dot{ background:#3b82f6; } .c-doing .dot{ background:#22c55e; }
  .c-done .dot{ background:#f472b6; }
  .count{ margin-left:auto; background:#fff; color:#8a92a3; border-radius:20px;
        font-size:12px; padding:2px 10px; }
  .cards{ display:flex; flex-direction:column; gap:10px; flex:1; min-height:20px; }
  .card{ position:relative; background:#fff; border-radius:10px; padding:12px 34px 12px 14px;
        box-shadow:0 1px 3px rgba(20,30,60,.10); cursor:grab; }
  .card.dragging{ opacity:.4; }
  .card .title{ font-size:14px; margin-bottom:8px; line-height:1.4; word-break:break-word; }
  .chips{ display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
  .chip{ font-size:11px; padding:2px 8px; border-radius:20px; }
  .chip.date{ background:#eef2f7; color:#6b7688; }
  .chip.label{ color:#fff; }
  .del{ position:absolute; top:7px; right:7px; width:22px; height:22px; border:none;
        border-radius:6px; background:transparent; color:#b3bcca; font-size:17px;
        line-height:1; cursor:pointer; padding:0; }
  .del:hover{ background:#fbe7ee; color:#e0567b; }
  .add{ margin-top:10px; border:none; background:transparent; color:#6b7688;
        text-align:left; padding:8px 6px; border-radius:8px; cursor:pointer; font-size:13px; }
  .add:hover{ background:#e3e8f0; }
  .form{ margin-top:10px; background:#fff; border-radius:10px; padding:12px;
        box-shadow:0 1px 3px rgba(20,30,60,.10); display:flex; flex-direction:column; gap:8px; }
  .form input, .form select{ font-size:13px; padding:8px; border:1px solid #d7deea;
        border-radius:8px; width:100%; background:#fff; color:#2b2f38; }
  .form-row{ display:flex; gap:8px; }
  .form-row > *{ flex:1; min-width:0; }
  .form-btns{ display:flex; gap:8px; }
  .btn-add{ flex:1; border:none; background:#3b82f6; color:#fff; border-radius:8px;
        padding:9px; font-size:13px; font-weight:bold; cursor:pointer; }
  .btn-add:hover{ background:#2f6fe0; }
  .btn-cancel{ border:none; background:#eef1f6; color:#6b7688; border-radius:8px;
        padding:9px 14px; font-size:13px; cursor:pointer; }
</style>
</head>
<body>
  <h1>タスクボード</h1>
  <p class="sub">タスクを登録し、カードをドラッグして列を移動できます</p>
  <div class="board" id="board"></div>
<script>
  const KEY = 'task-board';
  const COLS = [
    { id:'todo',  name:'ToDo',   cls:'c-todo' },
    { id:'doing', name:'進行中', cls:'c-doing' },
    { id:'done',  name:'完了',   cls:'c-done' },
  ];
  const LABELS = { '買い物':'#f59e0b', '仕事':'#3b82f6', '勉強':'#8b5cf6', '家事':'#22c55e' };
  const seed = {
    todo:[ {t:'牛乳と卵を買う', d:'6/12', l:'買い物'},
           {t:'請求書を送る', d:'6/13', l:'仕事'} ],
    doing:[ {t:'ブログ記事を書く', d:'6/11', l:'仕事'},
            {t:'英単語を10個おぼえる', d:'6/11', l:'勉強'} ],
    done:[ {t:'洗濯物をたたむ', d:'6/10', l:'家事'} ],
  };
  let data = JSON.parse(localStorage.getItem(KEY) || 'null') || seed;
  let adding = null; // 入力フォームを開いている列のid(null=閉じている)
  function save(){ localStorage.setItem(KEY, JSON.stringify(data)); }
  const board = document.getElementById('board');
  let dragInfo = null;

  function fmtDate(v){ // "2026-06-12" → "6/12"(空なら空文字)
    if(!v) return '';
    const p = v.split('-');
    return p.length === 3 ? (Number(p[1]) + '/' + Number(p[2])) : v;
  }
  function render(){
    board.innerHTML = '';
    COLS.forEach(col => {
      const items = data[col.id] || [];
      const el = document.createElement('div');
      el.className = 'col ' + col.cls;
      el.innerHTML = `<div class="col-head"><span class="dot"></span>${col.name}` +
        `<span class="count">${items.length}</span></div>`;
      const cards = document.createElement('div'); cards.className = 'cards';
      items.forEach((it, i) => cards.appendChild(makeCard(col.id, i, it)));
      el.appendChild(cards);
      if (adding === col.id) {
        el.appendChild(makeForm(col.id));
      } else {
        const add = document.createElement('button');
        add.className = 'add'; add.textContent = '+ タスクを追加';
        add.onclick = () => { adding = col.id; render(); };
        el.appendChild(add);
      }
      el.addEventListener('dragover', e => { e.preventDefault(); el.classList.add('drag-over'); });
      el.addEventListener('dragleave', () => el.classList.remove('drag-over'));
      el.addEventListener('drop', e => {
        e.preventDefault(); el.classList.remove('drag-over');
        if (!dragInfo) return;
        const moved = data[dragInfo.col].splice(dragInfo.idx, 1)[0];
        data[col.id].push(moved); save(); render();
      });
      board.appendChild(el);
    });
  }
  function makeCard(colId, idx, it){
    const c = document.createElement('div'); c.className = 'card'; c.draggable = true;
    const chips = [];
    if (it.d) chips.push(`<span class="chip date">${it.d}</span>`);
    if (it.l && LABELS[it.l]) chips.push(`<span class="chip label" style="background:${LABELS[it.l]}">${it.l}</span>`);
    c.innerHTML = `<button class="del" title="削除">×</button>` +
      `<div class="title"></div><div class="chips">${chips.join('')}</div>`;
    c.querySelector('.title').textContent = it.t; // ユーザー入力は textContent で安全に表示
    const del = c.querySelector('.del');
    del.draggable = false;
    del.addEventListener('click', e => {
      e.stopPropagation();
      data[colId].splice(idx, 1); save(); render();
    });
    c.addEventListener('dragstart', () => { dragInfo = { col: colId, idx }; c.classList.add('dragging'); });
    c.addEventListener('dragend', () => { dragInfo = null; c.classList.remove('dragging'); });
    return c;
  }
  function makeForm(colId){
    const f = document.createElement('div'); f.className = 'form';
    const opts = ['<option value="">カテゴリーなし</option>']
      .concat(Object.keys(LABELS).map(k => `<option value="${k}">${k}</option>`)).join('');
    f.innerHTML =
      `<input class="f-title" type="text" placeholder="タスク名" />` +
      `<div class="form-row">` +
        `<select class="f-label">${opts}</select>` +
        `<input class="f-date" type="date" />` +
      `</div>` +
      `<div class="form-btns">` +
        `<button class="btn-add">追加</button>` +
        `<button class="btn-cancel">キャンセル</button>` +
      `</div>`;
    const titleEl = f.querySelector('.f-title');
    const submit = () => {
      const t = titleEl.value.trim();
      if (!t) { titleEl.focus(); return; }
      const l = f.querySelector('.f-label').value;
      const d = fmtDate(f.querySelector('.f-date').value);
      data[colId].push({ t, d, l });
      adding = null; save(); render();
    };
    f.querySelector('.btn-add').onclick = submit;
    titleEl.addEventListener('keydown', e => { if (e.key === 'Enter') submit(); });
    f.querySelector('.btn-cancel').onclick = () => { adding = null; render(); };
    setTimeout(() => titleEl.focus(), 0);
    return f;
  }
  render();
</script>
</body>
</html>

「動いているはず」を「動いていると確認できる」に

機能が増えるほど、手で全部を確認するのは無理になる。少し進んだら、AIに「ちゃんと動くか自動でチェックする仕組み(テスト)も一緒に書いて」と頼んでみよう。最初は完璧でなくていい。「壊れたらすぐ気づける」状態を少しずつ作るのが、長く育てるコツだ。実例7本でまとめた バイブコーディングのやり方では、この“動作確認”をさらに一歩進め、「配った先でも動くか」まで確かめている。

ここまで来れば、あなたのアプリはもう「自分専用の道具」と呼べる完成度だ。最後の記事で、それを自分のパソコンの外へ、人に使ってもらえる場所へ公開する。


👉 前へ: 【入門5】エラーが出たら ・ 次へ: 【入門7】作ったアプリを公開する記事一覧

エラーが出たらどうする?AIへの聞き方とデバッグの基本【入門5】エラーが出たらどうする?AIへの聞き方とデバッグの基本【入門5】前のページ

作ったアプリを公開する|ローカルから人に使ってもらうまでの最短ルート【入門7】次のページ作ったアプリを公開する|ローカルから人に使ってもらうまでの最短ルート【入門7】

ピックアップ記事

  1. 競艇予想AIの作り方 — LightGBMで「当たる順位」を学習させる実装ガイド…

  2. 高精度OCRデスクトップアプリの作り方 — PaddleOCR-VLとPyIns…

  3. New Eden Intelligence Hub の作り方 — EVE Onl…

  4. Claude Mission Control の作り方 — Tauri+Pyth…

関連記事

  1. 背景除去アプリを作る②|AIなしでここまで抜ける — FloodFill・クロマキー・Color-to-Alphaの古典CVエンジン【背景除去Studio制作】

    アプリの作り方

    背景除去アプリを作る②|AIなしでここまで抜ける — FloodFill・クロマキー・Color-t…

    白背景の商品写真はAIより古典CVの方が速くて正確。四隅FloodFi…

  2. 都市開発シミュを作る⑥|静的ビルドをWordPressへ公開+連載総括【Aurum City制作・完結】

    アプリの作り方

    都市開発シミュを作る⑥|静的ビルドをWordPressへ公開+連載総括【Aurum City制作・完…

    完成した都市開発シミュを、バックエンド無しの静的ビルドにしてWordP…

  3. バイブコーディングの始め方|AIツールの選び方と最初のセットアップ【入門2】

    バイブコーディング

    バイブコーディングの始め方|AIツールの選び方と最初のセットアップ【入門2】

    最初に必要な道具はたった1つ。ブラウザのAIチャット(Claude/C…

  4. 背景除去アプリを作る③|「囲まれた背景」と「アイコンの穴」— 相反する2つの難所を実測で解く【背景除去Studio制作】
  5. バイブコーディングのやり方 — AIで「動くアプリ」を最後まで完成させる進め方を、実例7本で全部見せる
  6. 背景除去アプリを作る⑤|5スライダー後処理+消去/復元ブラシ+20段Undoで「自動9割・手で1割」を仕上げる【背景除去Studio制作】

    アプリの作り方

    背景除去アプリを作る⑤|5スライダー後処理+消去/復元ブラシ+20段Undoで「自動9割・手で1割」…

    完全自動を狙うより、自動+微修正の方が実用品質に早く届く。しきい値・ソ…

注目

AIで、ここまで作れる

AIで作った2D RPGを、ブラウザでそのまま遊べます。その「作り方=最後まで完成させる進め方」も実例つきで公開中。

▶ ゲームを遊ぶやり方を読む

PR

お名前.com 独自ドメイン取得(PR)

独自ドメイン:お名前.com(本サイトで使用・PR)

  1. アプリの作り方

    Claude Codeのトークン消費を減らす実践ガイド — 公式ドキュメントで裏…
  2. GitHub Copilot CLI が大型更新 — auto allow-all の是非を Claude Code の権限モデルと比べる

    AI・テック動向

    GitHub Copilot CLI が大型更新 — auto allow-al…
  3. Inkling は手元では動かない — 1bit 量子化で 270GB という現実と、個人開発者の入口

    AI・テック動向

    Inkling は手元では動かない — 1bit 量子化で 270GB という現…
  4. AIアセットでゲームマップを作る — 生成スケッチの透過切り出しからレベルセレクトまで

    アプリの作り方

    AIアセットでゲームマップを作る — 生成スケッチの透過切り出しからレベルセレク…
  5. Gemini 3.6 Flash は出力トークンが約17%減る — 単価ではなく「実出力トークン×単価」で比べる

    AI・テック動向

    Gemini 3.6 Flash は出力トークンが約17%減る — 単価ではなく…
PAGE TOP

TAG CLOUD

ドラッグで回転・クリックでそのタグの記事一覧へ