JavaScript | 1 日 60 分 × 7 日アプリ学習:家計簿編

APP JavaScript
スポンサーリンク

この7日間プランで作る家計簿アプリのゴール

この 7 日間では、ブラウザ上で動く「シンプル家計簿アプリ」を作ります。
機能は次のようなイメージです。

  1. 日付・内容・金額・種別(収入 / 支出)を入力して追加できる
  2. 下に収支のリストが表示される
  3. 合計収入・合計支出・収支(差額)が表示される
  4. ざっくりしたグラフ(棒グラフ風)で支出カテゴリの割合を見る

HTML と CSS は最低限にしつつ、JavaScript で「配列・オブジェクト・合計計算・DOM 操作・簡単なグラフ描画・ローカルストレージ」を体験するのが狙いです。


1日目:土台の画面を作る(入力欄とリストの枠)

フォルダとHTMLの準備

  1. デスクトップなどに「kakeibo-app」というフォルダを作る。
  2. その中に「index.html」を作る。
  3. ブラウザで「index.html」を開けることを確認する。

ここまでは、今までのアプリ学習と同じ流れです。

家計簿画面の骨組みを書く

「入力欄」「合計表示」「リスト」「グラフエリア」の枠だけ用意します。
index.html に次の内容を貼り付けてください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>家計簿アプリ</title>
  <style>
    body {
      font-family: sans-serif;
      background-color: #f5f5f5;
      padding: 20px;
    }
    .container {
      max-width: 800px;
      margin: 0 auto;
      background-color: #fff;
      padding: 16px;
      border-radius: 8px;
      box-shadow: 0 0 8px rgba(0,0,0,0.1);
    }
    h1 {
      text-align: center;
      margin-top: 0;
    }
    .input-area {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-bottom: 16px;
    }
    .input-area input,
    .input-area select {
      padding: 4px;
      font-size: 14px;
    }
    .input-area button {
      padding: 6px 10px;
      font-size: 14px;
    }
    .summary {
      margin-bottom: 16px;
      border-bottom: 1px solid #ddd;
      padding-bottom: 8px;
    }
    .summary span {
      margin-right: 16px;
      font-weight: bold;
    }
    .list-header,
    .list-row {
      display: grid;
      grid-template-columns: 100px 1fr 100px 80px 80px;
      gap: 8px;
      padding: 4px 0;
      border-bottom: 1px solid #eee;
      font-size: 14px;
    }
    .list-header {
      font-weight: bold;
      border-bottom: 2px solid #ccc;
    }
    .list-row button {
      font-size: 12px;
    }
    .income {
      color: #2e7d32;
    }
    .expense {
      color: #c62828;
    }
    .graph-area {
      margin-top: 16px;
    }
    .bar-row {
      display: flex;
      align-items: center;
      margin-bottom: 4px;
      font-size: 13px;
    }
    .bar-label {
      width: 80px;
    }
    .bar {
      height: 16px;
      background-color: #42a5f5;
      margin-left: 4px;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>家計簿アプリ</h1>

    <div class="input-area">
      <input id="date-input" type="date" />
      <input id="desc-input" type="text" placeholder="内容 (例: 食費, 給料)" />
      <input id="amount-input" type="number" placeholder="金額" />
      <select id="type-select">
        <option value="income">収入</option>
        <option value="expense">支出</option>
      </select>
      <button id="add-button">追加</button>
    </div>

    <div class="summary">
      <span id="income-total">合計収入: 0 円</span>
      <span id="expense-total">合計支出: 0 円</span>
      <span id="balance">収支: 0 円</span>
    </div>

    <div class="list-header">
      <div>日付</div>
      <div>内容</div>
      <div>金額</div>
      <div>種別</div>
      <div>操作</div>
    </div>
    <div id="list-body">
      <!-- ここに収支の行が追加される -->
    </div>

    <div class="graph-area">
      <h3>カテゴリ別支出(ざっくり棒グラフ)</h3>
      <div id="graph-body">
        <!-- 棒グラフをここに描画 -->
      </div>
    </div>
  </div>

  <script>
    console.log("家計簿アプリスタート");
  </script>
</body>
</html>

ブラウザで表示し、入力欄と表のヘッダー、グラフの見出しが出ていれば 1 日目は完了です。


2日目:1件のデータ構造(オブジェクト)と追加処理の流れを理解する

家計簿1行をオブジェクトで表現する(重要)

家計簿の1行は、次のような情報を持っています。

  1. date(日付、文字列)
  2. desc(内容、文字列)
  3. amount(金額、数値)
  4. type(収入か支出か、”income” / “expense”)

JavaScript では、1 行をオブジェクトで表現します。

const record = {
  date: "2025-01-01",
  desc: "給料",
  amount: 300000,
  type: "income"
};
JavaScript

ポイントは、
「配列は“順番重視の箱”」「オブジェクトは“項目名付きの箱”」というイメージです。
家計簿1行のように「項目名がはっきりしているデータ」にはオブジェクトが向いています。

すべてのデータは配列でまとめる

家計簿は複数行になります。
これを配列でまとめて管理します。

let records = [];
JavaScript

新しいレコードを追加するときは、push で配列に足します。

records.push(record);
JavaScript

この「配列の中にオブジェクトが並んでいる」構造は、現場でも頻出です。
家計簿だけでなく、ユーザー一覧、商品一覧、TODO リストなど、多くのデータが同じ形で管理されます。

DOM 要素を取得する

script タグの中に、入力欄などを取得するコードを書きます。

const dateInput = document.getElementById("date-input");
const descInput = document.getElementById("desc-input");
const amountInput = document.getElementById("amount-input");
const typeSelect = document.getElementById("type-select");
const addButton = document.getElementById("add-button");

const listBody = document.getElementById("list-body");
const incomeTotalSpan = document.getElementById("income-total");
const expenseTotalSpan = document.getElementById("expense-total");
const balanceSpan = document.getElementById("balance");
const graphBody = document.getElementById("graph-body");

let records = [];
JavaScript

まずは「JavaScript から画面の部品を掴めているか」を確認するのが大事です。
コンソールに console.log で出してみてもいいです。

追加処理の骨組みを作る

まずは「追加ボタンを押したら、入力された値を取り出す」ところまでやります。

addButton.addEventListener("click", function() {
  const date = dateInput.value;
  const desc = descInput.value.trim();
  const amount = Number(amountInput.value);
  const type = typeSelect.value;

  console.log(date, desc, amount, type);
});
JavaScript

ここで大事なのは、
文字列としての入力を Number(...) で数値に変換していることです。
家計簿の合計を計算するときは「数値」でなければならないので、ここで変換しておきます。


3日目:入力値からrecordオブジェクトを作り、リストに1行表示する

入力値のバリデーション(最低限)

いきなり何でも受け入れるとバグりやすいので、
最低限のチェックを入れます。

  1. 日付が空でないか
  2. 内容(desc)が空でないか
  3. 金額が 0 以上か

追加ボタンのイベント内を次のようにします。

addButton.addEventListener("click", function() {
  const date = dateInput.value;
  const desc = descInput.value.trim();
  const amount = Number(amountInput.value);
  const type = typeSelect.value;

  if (!date) {
    alert("日付を入力してください");
    return;
  }
  if (!desc) {
    alert("内容を入力してください");
    return;
  }
  if (!amount || amount <= 0) {
    alert("金額は 1 以上の数値を入力してください");
    return;
  }

  const record = {
    id: Date.now(),  // 簡易なユニークID
    date: date,
    desc: desc,
    amount: amount,
    type: type
  };

  records.push(record);

  descInput.value = "";
  amountInput.value = "";

  renderList();
  renderSummary();
  renderGraph();
});
JavaScript

ここで new なのは「id」というプロパティです。
Date.now() は現在時刻のミリ秒を返すので、簡易的なユニーク ID としてよく使われます。
削除する時にどのレコードかを特定するために、この id が役立ちます。

1行分のDOM要素を作る関数(重要)

records の 1 要素から、画面に表示する 1 行を作る関数を用意します。

function createRow(record) {
  const row = document.createElement("div");
  row.className = "list-row";

  const dateDiv = document.createElement("div");
  dateDiv.textContent = record.date;

  const descDiv = document.createElement("div");
  descDiv.textContent = record.desc;

  const amountDiv = document.createElement("div");
  amountDiv.textContent = record.amount.toLocaleString() + " 円";
  if (record.type === "income") {
    amountDiv.classList.add("income");
  } else {
    amountDiv.classList.add("expense");
  }

  const typeDiv = document.createElement("div");
  typeDiv.textContent = record.type === "income" ? "収入" : "支出";

  const actionsDiv = document.createElement("div");
  const deleteButton = document.createElement("button");
  deleteButton.textContent = "削除";
  actionsDiv.appendChild(deleteButton);

  deleteButton.addEventListener("click", function() {
    deleteRecord(record.id);
  });

  row.appendChild(dateDiv);
  row.appendChild(descDiv);
  row.appendChild(amountDiv);
  row.appendChild(typeDiv);
  row.appendChild(actionsDiv);

  return row;
}
JavaScript

ここは家計簿アプリの「心臓部の1つ」なので少し深掘りします。

  1. record はオブジェクトで、そのプロパティを使って表示を作っている。
  2. row の中に div を複数 appendChild して「1 行の見た目」を構成している。
  3. deleteButton のイベントの中から、外側の record.id を使って削除処理を呼び出している。

この「オブジェクト → DOM」の変換は、どんなアプリでもほぼ必ず出てくるパターンです。

records全体を画面に描画するrenderList

配列 records の中身を、全行描画する関数を作ります。

function renderList() {
  listBody.textContent = "";

  records.forEach(function(record) {
    const row = createRow(record);
    listBody.appendChild(row);
  });
}
JavaScript

重要ポイントは、「一度全部消してから全部描き直す」戦略です。
細かく差分更新するより、初心者のうちはこの方式の方が理解しやすくバグも少ないです。


4日目:削除機能と合計の計算(収入・支出・収支)

削除処理deleteRecordの実装(重要)

3 日目に出てきた deleteRecord 関数を実装します。

function deleteRecord(id) {
  records = records.filter(function(record) {
    return record.id !== id;
  });

  renderList();
  renderSummary();
  renderGraph();
}
JavaScript

ここでは、filter を使って「指定した id 以外のものだけ残す」形で配列を再構築しています。
結果として、削除対象のレコードだけが配列から消えます。

filter は「条件に合う要素だけを残して新しい配列を作る」関数です。
家計簿だけでなく、検索や絞り込みなどにも広く使われます。

合計収入・合計支出・収支の計算

合計計算用の関数 renderSummary を作ります。

function renderSummary() {
  let incomeTotal = 0;
  let expenseTotal = 0;

  records.forEach(function(record) {
    if (record.type === "income") {
      incomeTotal += record.amount;
    } else {
      expenseTotal += record.amount;
    }
  });

  const balance = incomeTotal - expenseTotal;

  incomeTotalSpan.textContent = "合計収入: " + incomeTotal.toLocaleString() + " 円";
  expenseTotalSpan.textContent = "合計支出: " + expenseTotal.toLocaleString() + " 円";
  balanceSpan.textContent = "収支: " + balance.toLocaleString() + " 円";
}
JavaScript

この関数で大事なポイントを整理します。

  1. 配列をループしながら、条件によって違う変数に加算している。
    収入だけ incomeTotal に、支出だけ expenseTotal に足している。
  2. toLocaleString() を使って、3 桁区切りの見やすい形式にしている。
  3. 収支(差額)は「合計収入 − 合計支出」で計算している。

これで、追加・削除のたびに renderSummary を呼び出すことで、合計が自動更新されます。


5日目:カテゴリ別支出の集計と簡易棒グラフの描画

カテゴリをどう扱うか

今の仕様では「desc(内容)」に「食費」「家賃」などを書いてもらっています。
シンプルにするため、「desc をカテゴリとして扱う」というルールにします。

例えば、内容が
「食費 牛丼」「家賃 1月」「光熱費 電気」
のように混ざると集計がややこしくなるので、最初は「純粋にカテゴリ名を入れる」と仮定して進めます。

慣れてきたら「カテゴリ選択のプルダウン+メモ欄」などに発展させるとよいです。

カテゴリごとに支出を集計する(重要)

支出のみを対象に、「カテゴリ → 合計支出」のマップを作ります。
JavaScript のオブジェクトを使って、カテゴリ名をキーにします。

function calcExpenseByCategory() {
  const categoryMap = {};  // 例: { "食費": 15000, "家賃": 60000 }

  records.forEach(function(record) {
    if (record.type !== "expense") {
      return;
    }

    const cat = record.desc;

    if (!categoryMap[cat]) {
      categoryMap[cat] = 0;
    }

    categoryMap[cat] += record.amount;
  });

  return categoryMap;
}
JavaScript

この処理はデータ集計の基本的パターンです。

  1. 空のオブジェクトを用意する。
  2. ループしながら、キーが存在しない場合は 0 で初期化。
  3. 金額を加算していく。

このロジックが理解できると、売上集計やアクセス解析など、いろいろな集計処理に応用できます。

棒グラフ風の表示を作る

グラフといっても、最初は「横長の棒の長さだけ変える」くらいで十分です。
renderGraph 関数を作ります。

function renderGraph() {
  graphBody.textContent = "";

  const categoryMap = calcExpenseByCategory();

  const categories = Object.keys(categoryMap);

  if (categories.length === 0) {
    graphBody.textContent = "支出データがありません。";
    return;
  }

  const maxValue = Math.max.apply(null, categories.map(function(cat) {
    return categoryMap[cat];
  }));

  categories.forEach(function(cat) {
    const amount = categoryMap[cat];

    const row = document.createElement("div");
    row.className = "bar-row";

    const label = document.createElement("div");
    label.className = "bar-label";
    label.textContent = cat;

    const bar = document.createElement("div");
    bar.className = "bar";

    const widthPercent = (amount / maxValue) * 100;
    bar.style.width = widthPercent + "%";

    bar.title = amount.toLocaleString() + " 円";

    row.appendChild(label);
    row.appendChild(bar);
    graphBody.appendChild(row);
  });
}
JavaScript

ここでの大事なポイントを整理します。

  1. categoryMap から keys をとって、カテゴリの配列を作っている。
  2. maxValue を求めて、「一番大きなカテゴリを 100% の長さ」にしている。
  3. 各カテゴリの棒は、maxValue に対する割合で幅を決める。
    例えば、最大 50000 円、あるカテゴリが 25000 円なら幅は 50%。
  4. bar.title に金額を入れておくと、マウスを乗せたときにツールチップで値が見える。

これで、「支出カテゴリのバランス」がひと目で分かる簡易グラフになります。


6日目:データの保存(localStorage)と初期表示

ブラウザにデータを保存するlocalStorageの基本

localStorage は、ブラウザに小さなデータを保存しておく仕組みです。
家計簿を閉じて、次に開いたときにもデータが残っていると、アプリっぽさが一気に増します。

localStorage の基本的な使い方は次の通りです。

  1. 保存
    localStorage.setItem(“キー名”, “文字列”);
  2. 取得
    const value = localStorage.getItem(“キー名”);
  3. 削除
    localStorage.removeItem(“キー名”);

配列やオブジェクトを保存するときは、JSON.stringify と JSON.parse を使います。

records を保存・復元する関数を作る(重要)

  1. 保存用 saveRecords
  2. 読み込み用 loadRecords

この2つを用意します。

const STORAGE_KEY = "kakeibo-records";

function saveRecords() {
  const json = JSON.stringify(records);
  localStorage.setItem(STORAGE_KEY, json);
}

function loadRecords() {
  const json = localStorage.getItem(STORAGE_KEY);
  if (!json) {
    records = [];
    return;
  }
  try {
    const data = JSON.parse(json);
    if (Array.isArray(data)) {
      records = data;
    } else {
      records = [];
    }
  } catch (e) {
    console.error("データの読み込みに失敗しました", e);
    records = [];
  }
}
JavaScript

ポイントをまとめます。

  1. JSON.stringify(records) で配列を文字列にして保存。
  2. JSON.parse(json) で文字列から元の配列にもどす。
  3. 読み込み失敗やフォーマット不正の可能性を考えて、try-catch で囲んでおく。

こうしておくと、もし何かの拍子に localStorage の中身がおかしくなっても、アプリ全体が落ちずに済みます。

保存と読み込みをアプリの流れに組み込む

  1. アプリ起動時に loadRecords してから render を呼ぶ。
  2. records が変わるたびに saveRecords を呼ぶ。

具体的には以下のようにします。

function init() {
  loadRecords();
  renderList();
  renderSummary();
  renderGraph();
}

init();
JavaScript

そして、
追加・削除処理の最後に saveRecords を追加します。

追加処理の最後あたりに次を足す。

saveRecords();

deleteRecord の最後にも。

function deleteRecord(id) {
  records = records.filter(function(record) {
    return record.id !== id;
  });

  renderList();
  renderSummary();
  renderGraph();
  saveRecords();
}
JavaScript

これで、ページをリロードしても家計簿の中身が残るようになります。


7日目:コード全体を通して理解し、自分なりの拡張を考える

完成版の全体イメージ

ここまで紹介したパーツを組み合わせたものが、家計簿アプリの完成形になります。
内容は長いので、ここでは流れだけを整理します。

  1. HTML で「入力欄・合計・リスト・グラフ」の枠を用意。
  2. JavaScript で
    2-1. DOM 要素の取得
    2-2. records 配列とその1要素の構造(オブジェクト)を定義
    2-3. createRow / renderList で「データ → 表示」を実装
    2-4. renderSummary で合計収入・支出・収支を計算
    2-5. calcExpenseByCategory / renderGraph でカテゴリ別支出の集計と簡易グラフを作成
    2-6. saveRecords / loadRecords で localStorage 連携
    2-7. init で起動時に読み込みと描画

それぞれの関数が「何を入力にして、何を出しているか」を意識して読むと、全体の構造が理解しやすくなります。

あなたなりの拡張アイデア

この家計簿から発展させられそうなアイデアをいくつか挙げます。

  1. 月別のフィルター
    上部に「年月選択」の入力を追加し、選択された月のデータだけ表示・集計する。
  2. カテゴリをプルダウンに
    内容欄を「カテゴリ選択+メモ欄」に分ける。
    例えば「食費」「家賃」「光熱費」「交際費」などを select 要素で選べるようにする。
  3. CSV 風のテキスト出力
    「エクスポート」ボタンを作り、records を「日付, 内容, 金額, 種別」の形式でテキストエリアに出力する。
  4. 円グラフに挑戦
    Canvas やすごく簡易な「絵文字グラフ」などで、円グラフっぽい表示に挑戦してみる。

どれも、今日までに学んだ「配列とオブジェクト」「ループ」「条件分岐」「DOM 操作」「イベント」「localStorage」の組み合わせで実現できます。


この家計簿アプリで身についた「本物の基礎」

  1. オブジェクトと配列を使った、現実世界に近いデータ構造の設計。
  2. render 系関数で「データと表示をきっちり分ける」考え方。
  3. 集計(合計・カテゴリ別集計)とグラフ化の基本パターン。
  4. localStorage による「ブラウザ内保存」で、アプリとしての完成度を上げる方法。

タイトルとURLをコピーしました