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

APP JavaScript
スポンサーリンク

7日目のゴールと今日のテーマ

7日目は「このローカル保存対応メモ帳を、設計として“説明できる自分”になる日」です。
もう機能としては十分動いています。今日は、

localStorage・JSON・永続化設計を、一本のストーリーとして整理する。
「このアプリはどういう設計で動いているのか」を言語化してみる。
もし機能を足すなら、どこをどう変えるかを“設計目線”で考えてみる。

ここまで積み上げてきたものを、バラして、眺めて、言葉にして、もう一度自分の中に組み直す日です。


このメモ帳アプリの「全体像」を言葉で説明してみる

アプリの中心は state、その外側に localStorage

このメモ帳アプリの構造を、ざっくりこう言えます。

「アプリの“今”は state に全部入っている。
その state の一部(memos)を、localStorage に JSON として保存している。」

コードでいうと、こんな形です。

const state = {
  memos: loadMemosFromStorage(), // 永続化されたメモ一覧
  editingMemoId: null,           // 今どのメモを編集しているか
  saveStatus: "idle",            // "idle" | "saving" | "saved"
  searchQuery: "",               // 検索キーワード
};
JavaScript

ここで大事なのは、

memos は「ビジネスデータ」(メモそのもの)。
editingMemoId / saveStatus / searchQuery は「UI 状態」(どう見せるか)。

という分離です。
localStorage はあくまで「保存場所」であって、
アプリの“真実”は state にある、という設計になっています。

1 件のメモのデータ構造

メモ 1 件は、こういうオブジェクトです。

const memo = {
  id: 1736400000000,
  title: "今日のアイデア",
  body: "・新しいサービス案\n・UI のラフ\n・やることメモ",
  updatedAt: 1736400000000,
};
JavaScript

id は一意な識別子(編集・削除・特定に使う)。
title は一覧でパッと見て思い出すための短いテキスト。
body は実際のメモ内容(複数行 OK)。
updatedAt は「最後に更新した時刻」(並び順に使う)。

この形にしておくことで、

「最近触ったメモを上に出す」
「タイトルだけ一覧に出す」
「更新日時を表示する」

といった拡張が、設計を壊さずにできます。


localStorage と JSON の関係を“レイヤー”として整理する

永続化レイヤー:load / save / migrate

localStorage まわりは、1 カ所にまとめてあります。

const STORAGE_KEY = "local-memo-app";

function migrateRawMemos(raw) {
  if (!Array.isArray(raw)) return [];

  return raw.map((memo, index) => {
    const now = Date.now() + index;

    const id = memo.id ?? now;
    const title = memo.title ?? "無題のメモ";
    const body = memo.body ?? memo.text ?? "";
    const updatedAt = memo.updatedAt ?? now;

    return { id, title, body, updatedAt };
  });
}

function loadMemosFromStorage() {
  const json = localStorage.getItem(STORAGE_KEY);
  if (!json) return [];

  try {
    const raw = JSON.parse(json);
    return migrateRawMemos(raw);
  } catch (e) {
    console.error("メモの読み込みに失敗しました", e);
    return [];
  }
}

function saveMemosToStorage(memos) {
  const json = JSON.stringify(memos);
  localStorage.setItem(STORAGE_KEY, json);
}
JavaScript

ここでのポイントは、はっきり 3 つに分かれていることです。

loadMemosFromStorage は「文字列 → JSON.parse → migrate → 配列」を返す。
saveMemosToStorage は「配列 → JSON.stringify → 文字列を保存」だけをする。
migrateRawMemos は「古い形式のデータを新しい形に揃える」。

アプリ本体は、「memos は常に { id, title, body, updatedAt } の形だ」と信じてよくて、
localStorage の中身が古くても、migrate が吸収してくれます。
これが「永続化レイヤーを分ける」設計の強さです。


画面更新の流れ:「state → 画面」の一方向

render は「state を読むだけ」の関数

画面更新の入口は、常に render です。

function render() {
  memosContainerEl.innerHTML = "";

  const memos = getVisibleMemos();

  if (memos.length === 0) {
    const emptyEl = document.createElement("div");

    if (state.searchQuery.trim()) {
      emptyEl.textContent = "検索条件に一致するメモがありません。";
    } else {
      emptyEl.textContent = "メモはまだありません。最初のメモを書いてみましょう。";
    }

    emptyEl.style.color = "#777";
    emptyEl.style.fontSize = "12px";
    memosContainerEl.appendChild(emptyEl);
    renderSaveStatus();
    return;
  }

  memos.forEach((memo) => {
    const memoEl = renderMemoItem(memo);
    memosContainerEl.appendChild(memoEl);
  });

  renderSaveStatus();
}
JavaScript

ここでのルールはシンプルです。

render は state を読むだけ。
state を書き換えるのは、addMemo / updateMemo / deleteMemo / startEditMemo / finishEditMemo などの“ロジック関数”だけ。

このルールのおかげで、

「画面がおかしいときは state を疑えばいい」
「state が正しければ、render は正しい画面を作るはず」

という前提が成り立ちます。
これはデバッグするときの大きな支えになります。

表示用メモ一覧を作る getVisibleMemos

並び順と検索は、getVisibleMemos に閉じ込めています。

function getVisibleMemos() {
  const sorted = [...state.memos].sort((a, b) => b.updatedAt - a.updatedAt);

  const query = state.searchQuery.trim().toLowerCase();
  if (!query) {
    return sorted;
  }

  return sorted.filter((memo) => {
    const title = memo.title.toLowerCase();
    const body = memo.body.toLowerCase();
    return title.includes(query) || body.includes(query);
  });
}
JavaScript

ここでの設計のキモは、

「何を表示するか」は getVisibleMemos が決める。
「どう表示するか」は render / renderMemoItem が決める。

という役割分担です。
これにより、「検索ロジックを変えたい」「並び順を変えたい」と思ったとき、
getVisibleMemos だけを見ればよくなります。


メモ追加・編集・削除と「保存タイミング」の設計

追加:state を変える → 保存 → 再描画

メモ追加はこうでした。

function addMemo(title, body) {
  const trimmedTitle = title.trim();
  const trimmedBody = body.trim();

  if (!trimmedTitle && !trimmedBody) return;

  const now = Date.now();

  const newMemo = {
    id: now,
    title: trimmedTitle || "無題のメモ",
    body: trimmedBody,
    updatedAt: now,
  };

  state.memos.unshift(newMemo);
  persistMemosWithStatus();
  render();
}
JavaScript

ここでの流れは、いつも同じです。

state.memos を更新する。
保存用の関数(persistMemosWithStatus)を呼ぶ。
render で画面を描き直す。

「状態を変えたら、保存と再描画をセットで行う」というパターンを徹底しています。

編集:更新と updatedAt の更新

編集はこういう形です。

function updateMemo(id, newTitle, newBody) {
  const trimmedTitle = newTitle.trim();
  const trimmedBody = newBody.trim();

  if (!trimmedTitle && !trimmedBody) {
    finishEditMemo();
    return;
  }

  const now = Date.now();

  state.memos = state.memos.map((memo) => {
    if (memo.id !== id) return memo;
    return {
      ...memo,
      title: trimmedTitle || "無題のメモ",
      body: trimmedBody,
      updatedAt: now,
    };
  });

  persistMemosWithStatus();
  finishEditMemo();
}
JavaScript

ここでのポイントは、

配列を直接いじらず、map で新しい配列を作っていること。
編集したメモだけ updatedAt を更新していること。
編集完了後に保存とモード解除(→ render)をしていること。

これにより、「最近編集したメモが上に来る」という自然な挙動になります。

削除:filter で残すものだけを残す

削除はシンプルです。

function deleteMemo(id) {
  state.memos = state.memos.filter((memo) => memo.id !== id);
  persistMemosWithStatus();
  render();
}
JavaScript

ここでも、

「削除したいものを消す」のではなく、「残したいものだけを残す」
という発想で filter を使っています。
この「追加=push / 更新=map / 削除=filter」という配列操作の 3 パターンは、
ToDo アプリでも出てきた、かなり強力な“型”です。


保存ステータスと UX の設計

保存ステータスを state で持つ意味

保存状態は、こう持っています。

state.saveStatus = "idle"; // "idle" | "saving" | "saved"
JavaScript

表示はこうです。

function renderSaveStatus() {
  if (state.saveStatus === "idle") {
    saveStatusEl.textContent = "";
  } else if (state.saveStatus === "saving") {
    saveStatusEl.textContent = "保存中...";
  } else if (state.saveStatus === "saved") {
    saveStatusEl.textContent = "保存しました";
  }
}
JavaScript

ここでの本質は、

「今保存がどういう状態か」も UI 状態の一部だ、という考え方です。
ユーザーにとっては、「ちゃんと保存されたのか?」が気になるので、
それを state として持ち、画面に出してあげています。

保存処理とステータス更新をまとめた関数

保存+ステータス更新は、こうまとめています。

function persistMemosWithStatus() {
  state.saveStatus = "saving";
  renderSaveStatus();

  saveMemosToStorage(state.memos);

  state.saveStatus = "saved";
  renderSaveStatus();

  setTimeout(() => {
    if (state.saveStatus === "saved") {
      state.saveStatus = "idle";
      renderSaveStatus();
    }
  }, 1500);
}
JavaScript

やっていることは、

保存前に “saving” にする。
保存後に “saved” にする。
少し時間が経ったら “idle” に戻す。

という流れです。

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

保存そのもの(saveMemosToStorage)は永続化レイヤーに任せている。
UX に関わる「ステータス表示」はアプリ側の関数で扱っている。

という責務の分離です。
これにより、「保存の技術的な部分」と「ユーザーにどう見せるか」がきれいに分かれています。


「もし機能を足すなら?」を設計目線で考える

例1:タグ機能を足すなら

やりたいことをイメージしてみます。

メモに「仕事」「プライベート」などのタグを付けたい。
タグで絞り込みたい。

設計として必要になるのは、

メモ 1 件に tags: string[] を追加する。
migrateRawMemos で、tags がなければ [] を入れる。
state に selectedTag を追加する。
getVisibleMemos で「タグ条件」も加味する。
UI にタグ選択の部分を足す。

という感じです。

ここで大事なのは、「どこを触ればいいか」がもう見えていることです。
それは、この 7 日間で「責務ごとにコードが分かれている」設計にしてきたからです。

例2:メモをアーカイブする機能を足すなら

やりたいこと:

「もう使わないけど消したくはない」メモをアーカイブしたい。
通常の一覧には出さず、「アーカイブ一覧」でだけ見たい。

設計としては、

メモ 1 件に archived: boolean を追加する(デフォルト false)。
migrateRawMemos で archived がなければ false を入れる。
getVisibleMemos で !memo.archived のものだけを対象にする。
アーカイブボタンを追加して、押したら archived: true に更新する。
別画面(または別フィルタ)で memo.archived === true のものを表示する。

これも、「データ構造」「永続化レイヤー」「表示ロジック」「UI」のどこを触るかが、
自然に分かれて考えられるようになっています。


7日目のまとめ:あなたが手に入れた“永続化の型”

この 7 日間で、ローカル保存対応メモ帳を通して、あなたが身につけたのは——
「localStorage の使い方」だけではありません。

state を「アプリの真実」として設計する感覚。
配列+オブジェクトでデータをモデル化し、追加・更新・削除をパターンとして扱う力。
localStorage を「文字列だけ保存できる箱」として理解し、JSON で橋渡しする感覚。
load / save / migrate という永続化レイヤーを分けて考える設計。
保存タイミング(操作のたび保存)を意識的に選び、必要なら自動保存も設計できる視点。
保存ステータスを state に持たせ、「今どうなっているか」をユーザーに伝える UX の感覚。

どれも、メモ帳だけの話ではなく、
これから作るあらゆる「ちょっとした Web アプリ」にそのまま持ち込める“型”です。

最後に一つだけ、あえて聞きたい。

このメモ帳の設計の中で、
「ここは自分、ちょっと好きかも」と思えた部分はどこでしたか?

localStorage の扱い方かもしれないし、
migrate で古いデータを救ってあげるところかもしれないし、
保存ステータスで「保存しました」と出る、あの一瞬かもしれません。

その「好きだと思えたところ」が、あなたの設計センスの核です。
そこを意識して伸ばしていくと、次に作るアプリは、もう一段“あなたらしい設計”になります。

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