JavaScript | 1 日 120 分 × 7 日アプリ学習:ローカル保存対応メモ帳

APP JavaScript
スポンサーリンク

この7日間のゴールと全体像

この 7 日間は「ローカル保存対応メモ帳」を題材に、ブラウザだけで完結する“ちゃんとした”アプリを作りながら、次の3つを体に入れることがゴールです。

  • localStorage を使ってブラウザにデータを保存する感覚
  • オブジェクト配列との JSON 変換(JSON.stringify / JSON.parse
  • 「いつ保存するか」「どこから読み込むか」という永続化設計の考え方

実装する機能はこうです。

  • 複数メモの追加・編集・削除
  • ページを再読み込みしてもメモが残る
  • 保存タイミングを意識した実装(入力のたび?保存ボタン?削除時?)

HTML と CSS は最低限にして、「JavaScript の設計とロジック」に集中します。


1日目:メモ帳アプリの骨組みと「状態(state)」の設計

アプリの状態を言葉にしてみる

メモ帳アプリが「今どうなっているか」をデータとして表現します。
必要な情報を先に言語化すると、設計がブレません。

持ちたい状態は最低でも次のとおりです。

  • 複数メモの一覧
  • メモごとの情報(id, title, body, updatedAt など)
  • 今選択しているメモ(id か null)

これを JavaScript のデータ構造に落とし込みます。

メモ1件のデータ構造(重要)

メモ 1 件をオブジェクトで表現します。

const memo = {
  id: 1,
  title: "買い物リスト",
  body: "牛乳\n\nパン",
  updatedAt: 1710000000000, // Date.now() で取得したミリ秒
};
JavaScript

各プロパティの意味です。

  • id
    各メモを一意に区別するための番号。編集・削除・選択で使うので必須。
  • title
    一覧に表示するときの見出し。最初は本文の先頭数文字から自動生成してもよいし、明示的に入力してもよい。
  • body
    メモの本文。複数行のテキスト。
  • updatedAt
    最終更新日時。ソートや「更新日時を表示」に使えます。中級なら持たせておくと設計の幅が広がります。

複数のメモは配列にします。

let memos = [
  { id: 1, title: "買い物リスト", body: "牛乳\n\nパン", updatedAt: 1710000000000 },
  { id: 2, title: "学習メモ", body: "JavaScript\nlocalStorage\nJSON", updatedAt: 1710001000000 },
];
JavaScript

state オブジェクトを用意する

アプリの状態をひとまとめにしたオブジェクトを作ります。

const state = {
  memos: [],
  selectedId: null,
};
JavaScript

ここでのポイントは、「画面に何がどう表示されているか」は state から計算できる形にしておくことです。
DOM 自体を状態としては持たず、「あくまで state が真実、DOM はそのコピー」と考えます。


2日目:HTML・CSSの土台と「メモ一覧+編集エリア」のレイアウト

最低限の HTML を用意する

index.html を次のようにします(CSS はシンプル)。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>ローカル保存メモ帳</title>
  <style>
    body { font-family: sans-serif; margin: 0; }
    .container { display: flex; height: 100vh; }
    .sidebar {
      width: 240px;
      border-right: 1px solid #ccc;
      padding: 8px;
      box-sizing: border-box;
      overflow-y: auto;
    }
    .main {
      flex-grow: 1;
      display: flex;
      flex-direction: column;
      padding: 8px;
      box-sizing: border-box;
    }
    .memo-item {
      padding: 6px;
      border-radius: 4px;
      cursor: pointer;
      margin-bottom: 4px;
    }
    .memo-item.active {
      background-color: #e0f2f1;
    }
    .memo-title {
      font-weight: bold;
      font-size: 14px;
    }
    .memo-updated {
      font-size: 11px;
      color: #666;
    }
    .memo-actions {
      margin-top: 8px;
    }
    textarea {
      width: 100%;
      flex-grow: 1;
      resize: none;
      font-family: inherit;
      font-size: 14px;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="sidebar">
      <button id="new-button">新規メモ</button>
      <div id="memo-list"></div>
    </div>
    <div class="main">
      <input id="title-input" type="text" placeholder="タイトル" />
      <textarea id="body-textarea" placeholder="ここにメモを入力..."></textarea>
      <div class="memo-actions">
        <button id="delete-button">このメモを削除</button>
        <span id="status-text"></span>
      </div>
    </div>
  </div>

  <script src="app.js"></script>
</body>
</html>

この時点では JavaScript は何もしていませんが、

  • 左にメモ一覧(今は空)+新規ボタン
  • 右にタイトル入力+本文エリア+削除ボタン

という構造ができています。

JavaScript で DOM と state を準備する

app.js に次を書きます。

const state = {
  memos: [],
  selectedId: null,
};

const memoListEl = document.getElementById("memo-list");
const newButtonEl = document.getElementById("new-button");
const titleInputEl = document.getElementById("title-input");
const bodyTextareaEl = document.getElementById("body-textarea");
const deleteButtonEl = document.getElementById("delete-button");
const statusTextEl = document.getElementById("status-text");

let nextId = 1;
JavaScript

今日はここまでで OK です。
次の日から「state → 画面」の描画ロジックを作り始めます。


3日目:描画ロジック(render)とメモ追加の基本

render を設計する(重要)

render 関数は「state を読んで、画面全体を作り直す」役割を持たせます。
部分的に書き換えるより、この方が単純で設計がキレイになります。

function render() {
  renderMemoList();
  renderEditor();
}
JavaScript

リストと編集エリアをそれぞれ別関数に分けます。

function renderMemoList() {
  memoListEl.textContent = "";

  // updatedAt 降順でソートした新しい配列
  const sorted = [...state.memos].sort((a, b) => b.updatedAt - a.updatedAt);

  sorted.forEach((memo) => {
    const itemEl = document.createElement("div");
    itemEl.className = "memo-item";
    itemEl.dataset.id = String(memo.id);

    if (memo.id === state.selectedId) {
      itemEl.classList.add("active");
    }

    const titleEl = document.createElement("div");
    titleEl.className = "memo-title";
    titleEl.textContent = memo.title || "(タイトルなし)";

    const updatedEl = document.createElement("div");
    updatedEl.className = "memo-updated";
    updatedEl.textContent = formatUpdatedAt(memo.updatedAt);

    itemEl.appendChild(titleEl);
    itemEl.appendChild(updatedEl);

    memoListEl.appendChild(itemEl);
  });
}

function renderEditor() {
  const memo = state.memos.find((m) => m.id === state.selectedId);
  if (!memo) {
    titleInputEl.value = "";
    bodyTextareaEl.value = "";
    deleteButtonEl.disabled = true;
    statusTextEl.textContent = "メモが選択されていません。";
    return;
  }

  titleInputEl.value = memo.title;
  bodyTextareaEl.value = memo.body;
  deleteButtonEl.disabled = false;
  statusTextEl.textContent = "最終更新: " + formatUpdatedAt(memo.updatedAt);
}

function formatUpdatedAt(timestamp) {
  const d = new Date(timestamp);
  return d.toLocaleString();
}
JavaScript

深掘りします。

  • renderMemoList
    state.memos をソートしたコピーから DOM を作る。
    state はそのまま、ビュー用に「表示順を決める」のは render 側の責務。
  • dataset.id に id を埋め込んでおく
    クリックされたメモから id を取り出して state.selectedId を変えるため。
  • renderEditor
    state.selectedId から該当メモを探し、入力欄に値を反映。
    null の場合は「何も選択されていない状態」を表現。

状態(state)から一方向に UI を作っていることを意識してください。

メモ追加関数と「選択+描画」

新規メモを追加する関数を定義します。

function createNewMemo() {
  const now = Date.now();
  const memo = {
    id: nextId++,
    title: "",
    body: "",
    updatedAt: now,
  };

  state.memos.push(memo);
  state.selectedId = memo.id;
}
JavaScript

新規ボタンにイベントを付けます。

newButtonEl.addEventListener("click", () => {
  createNewMemo();
  render();
});
JavaScript

この時点で

  • 新規を押すと空メモが作られ、右側に表示
  • 左に一覧が 1 件出る(タイトルは空)

という状態になっているはずです(まだ編集しても state は変わりません)。


4日目:メモ選択・編集(title/body)と「状態更新 → render」の流れ

メモを選択するロジック

左の一覧をクリックしたら、そのメモを選択する必要があります。
イベント委譲を使います。

memoListEl.addEventListener("click", (event) => {
  const itemEl = event.target.closest(".memo-item");
  if (!itemEl) return;

  const id = Number(itemEl.dataset.id);
  state.selectedId = id;
  render();
});
JavaScript

ここでのポイントは、

  • DOM イベントから data-id を通して state を変えている
  • DOM 自体は「状態を持たない」、あくまで state のコピーという考え方

です。

タイトル・本文の変更を state に反映する(重要)

タイトルと本文の入力に応じて、state を更新し、updatedAt も更新します。

function updateSelectedMemo(partial) {
  const memo = state.memos.find((m) => m.id === state.selectedId);
  if (!memo) return;

  if (partial.title !== undefined) {
    memo.title = partial.title;
  }
  if (partial.body !== undefined) {
    memo.body = partial.body;
  }
  memo.updatedAt = Date.now();
}
JavaScript

タイトル入力欄にイベントを付けます。

titleInputEl.addEventListener("input", () => {
  if (state.selectedId == null) return;

  updateSelectedMemo({ title: titleInputEl.value });
  render();
});
JavaScript

本文の textarea にも。

bodyTextareaEl.addEventListener("input", () => {
  if (state.selectedId == null) return;

  updateSelectedMemo({ body: bodyTextareaEl.value });
  render();
});
JavaScript

ここで重要なのは、「入力イベントのたびに state を更新し、その結果を render で画面に反映する」流れです。
render の中でまた入力欄の値をセットしているので、「画面の真実は常に state 側にある」状態を保てます。


5日目:localStorage と JSON の基本、保存・復元の関数を作る

localStorage とは何か

localStorage は、ブラウザにキーと文字列を保存しておける仕組みです。
とても単純な API です。

localStorage.setItem("key", "value");      // 保存
const value = localStorage.getItem("key"); // 取得(なければ null)
localStorage.removeItem("key");           // 削除
JavaScript

注意点は「文字列しか保存できない」ことです。
配列やオブジェクトを保存したいときは JSON に変換する必要があります。

JSON.stringify と JSON.parse(重要)

配列やオブジェクトを文字列にする:

const obj = { a: 1, b: 2 };
const json = JSON.stringify(obj);
JavaScript

文字列から配列やオブジェクトに戻す:

const loaded = JSON.parse(json);
JavaScript

メモ一覧(state.memos)は配列+オブジェクトなので、このパターンで保存します。

保存用関数と読み込み用関数を作る

保存用:

const STORAGE_KEY = "memo-app-data";

function saveToStorage() {
  const data = {
    memos: state.memos,
    selectedId: state.selectedId,
    nextId: nextId,
  };

  const json = JSON.stringify(data);
  localStorage.setItem(STORAGE_KEY, json);
  statusTextEl.textContent = "保存しました: " + new Date().toLocaleTimeString();
}
JavaScript

復元用:

function loadFromStorage() {
  const json = localStorage.getItem(STORAGE_KEY);
  if (!json) {
    return;
  }

  try {
    const data = JSON.parse(json);
    if (!data || !Array.isArray(data.memos)) {
      return;
    }

    state.memos = data.memos;
    state.selectedId = data.selectedId;
    nextId = data.nextId || 1;
  } catch (e) {
    console.error("データの読み込みに失敗しました", e);
  }
}
JavaScript

ここでの大事なポイントは、

  • 「アプリの状態そのもの(memos, selectedId, nextId)」をまとめて保存している
  • JSON.parse には try/catch を付けて、不正なデータでもアプリが落ちないようにしている
  • 保存のフォーマットを data オブジェクトにしておくことで、後からフィールドを追加しやすくなる

という設計の部分です。

起動時に復元してから render

app.js の最後あたりで次を呼び出します。

loadFromStorage();

if (state.memos.length > 0 && state.selectedId == null) {
  state.selectedId = state.memos[0].id;
}

render();
JavaScript

これで、ページを再読み込みしてもメモが復元されるようになります(保存タイミングはまだ考え中)。


6日目:保存タイミングの設計と、「どこからでも呼べる save」パターン

いつ保存するか?設計の選択肢(重要)

保存タイミングはいくつかパターンがあります。

  1. 入力のたびに保存(タイトル・本文の input / change)
  2. 特定の操作のあとだけ保存(新規、削除、フォーカスを離れたときなど)
  3. 明示的な「保存」ボタンを押したときだけ保存

それぞれの特徴です。

  • 1 は「常に最新」、ただし保存回数が多くパフォーマンス的にやや重い可能性
  • 2 はバランス型。今回のメモ帳なら「新規・削除・入力の終了時」で十分
  • 3 はユーザー任せになるので、初心者が使うツールではミスが起きやすい

中級の練習として、ここでは 2 の「操作のあとに自動保存」を採用します。

状態変更関数の中から saveToStorage を呼ぶ

設計としてキレイなのは、

  • state を変える関数(add / update / delete)
    → そこから saveToStorage を呼ぶ

という形です。
UI 側(イベントハンドラ)が「保存のこと」を意識しなくてよくなるからです。

例:新規作成

function createNewMemo() {
  const now = Date.now();
  const memo = {
    id: nextId++,
    title: "",
    body: "",
    updatedAt: now,
  };

  state.memos.push(memo);
  state.selectedId = memo.id;
  saveToStorage();
}
JavaScript

updateSelectedMemo も変更します。

function updateSelectedMemo(partial) {
  const memo = state.memos.find((m) => m.id === state.selectedId);
  if (!memo) return;

  if (partial.title !== undefined) {
    memo.title = partial.title;
  }
  if (partial.body !== undefined) {
    memo.body = partial.body;
  }
  memo.updatedAt = Date.now();
  saveToStorage();
}
JavaScript

削除も同様です。

function deleteSelectedMemo() {
  if (state.selectedId == null) return;

  state.memos = state.memos.filter((m) => m.id !== state.selectedId);

  if (state.memos.length > 0) {
    state.selectedId = state.memos[0].id;
  } else {
    state.selectedId = null;
  }

  saveToStorage();
}
JavaScript

削除ボタンにはこれを紐付けます。

deleteButtonEl.addEventListener("click", () => {
  deleteSelectedMemo();
  render();
});
JavaScript

この設計のメリットは、

  • 「state を変えるところ=保存タイミング」がはっきりしている
  • UI 側は「どの関数を呼ぶか」だけ考えればよい
  • 保存や永続化の仕様を変えたいとき、state 操作系だけ見ればよい

という点です。
これが「永続化設計」を意識した書き方です。


7日目:コード全体を設計目線で振り返り、応用を考える

このメモ帳アプリの構造を整理する

ざっくりと役割ごとに見てみます。

状態(state)とデータ構造:

  • state: memos 配列と selectedId
  • memos: id / title / body / updatedAt を持つオブジェクトの配列
  • nextId: 新しい id を発行するカウンタ

状態を操作する関数:

  • createNewMemo
  • updateSelectedMemo
  • deleteSelectedMemo

永続化(localStorage・JSON):

  • saveToStorage
  • loadFromStorage

状態から UI を描画する関数:

  • render
  • renderMemoList
  • renderEditor
  • formatUpdatedAt

イベントハンドラ(UI と state の橋渡し):

  • newButton の click
  • memoList の click(選択)
  • titleInput の input
  • bodyTextarea の input
  • deleteButton の click

この「誰が何を担当しているか」を意識できていれば、設計力はかなり上がっています。

設計として特に大事だったポイント

今回の中級編で特に深掘りしたいのは次の 3 点です。

  1. 状態と UI の分離
    • 真実は state にだけ置き、UI は常に state から描画する。
    • DOM を「状態の元」として考えない。
  2. JSON と localStorage の境界
    • 内部はオブジェクト配列で扱いやすく設計。
    • 保存するときだけ JSON.stringify で文字列化。
    • 読み込むときだけ JSON.parse で戻す。
    • 永続化の仕様変更(例えば IndexedDB にするなど)も、この境界付近を書き換えるだけで済む。
  3. 保存タイミングの一元化
    • state を変える関数の中から saveToStorage を呼ぶ。
    • UI イベントから直接 localStorage を触らない。
    • 「どこで保存しているか」が、コード上で一目でわかる。

これらは、規模が大きくなってもそのまま通用する設計の考え方です。

応用アイデア:この設計を崩さずに機能を足す

例えば、次のような発展が考えられます。

  • 検索ボックスを追加して、「タイトル・本文にキーワードが含まれるメモだけ表示」
    → renderMemoList で state.memos にフィルタをかけるだけで実現できる。
  • 「お気に入り」フラグを追加
    → memo に isFavorite を追加し、state 操作系に toggleFavorite を足す。
    → render 側で、星マークなどを表示。
  • メモをタグで分類
    → memo に tags: string[] を追加して、タグ入力 UI とタグフィルタを作る。
    → 保存は今の JSON 仕組みがそのまま使える。

どれも、「state をどう設計するか」「state をどう描画するか」の延長線上で考えられます。


さいごに

この 7 日間で、あなたは次を経験しました。

  • メモアプリの状態をオブジェクト配列+選択IDで表現する
  • render 関数で「状態から UI を一方向に作る」
  • 入力イベントのたびに state を更新し、再描画する
  • localStorage と JSON を使って状態を永続化する
  • 保存タイミングを意識して、state 操作と保存処理を一元化する

ここまで来ると、React や Vue の「state」「props」「永続化」などの概念もかなり理解しやすくなります。

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