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

APP JavaScript
スポンサーリンク

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

6日目は「このメモ帳アプリの“永続化まわり”を、設計としてきれいに整理する日」です。
機能としてはもう十分動いていますが、今日は

  • localStorage まわりの処理を“ひとまとまりのモジュール”として整理する
  • JSON 変換・読み込み・マイグレーション(古いデータの変換)を意識して設計する
  • 「どこで何をしているか」が読みやすいコード構造にする

という、「中身を磨く」方向に進めます。
新機能を増やすというより、「責務を分けて、設計として説明しやすい形」にしていきます。


永続化まわりを「1カ所に集約する」発想

まずは“永続化専用の関数群”を決める

今まで、保存や読み込みの処理は app.js の中に散らばっていました。
6日目では、これを意識的に 3 つに分けて整理します。

保存する関数(save)。
読み込む関数(load)。
古いデータを新しい形に変換する関数(migrate)。

これを「永続化レイヤー」として 1 カ所にまとめておくと、
「localStorage のことはここを見ればわかる」という状態になります。

永続化レイヤーの骨組みを作る

app.js の上の方に、こんなブロックを作ります。

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

ここでの重要ポイントは、
「localStorage に触るのはこの 2 関数だけ」という形にしていることです。
アプリ本体は、loadMemosFromStorage()saveMemosToStorage(memos) を呼ぶだけで済みます。


migrateRawMemos を“設計として”理解する

なぜマイグレーション関数が必要なのか

永続化されたデータは、アプリのバージョンをまたいで残ります。
つまり、こういうことが起こり得ます。

最初は { id, text } だけ保存していた。
途中から { id, title, body, updatedAt } に変えた。
でも、localStorage には「古い形」のデータが残っている。

このとき、「古い形のままでは扱いづらい」ので、
読み込み時に「新しい形に変換してから使う」必要があります。
それを担当するのが migrateRawMemos です。

migrateRawMemos の中身をかみ砕いて読む

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 };
  });
}
JavaScript

やっていることを言葉にすると、

raw が配列じゃなかったら、もう諦めて空配列にする。
配列なら 1 件ずつ見ていく。
id がなければ「今の時刻+index」で一意な id を作る。
title がなければ「無題のメモ」にする。
body がなければ、古い text プロパティを使う(それもなければ空文字)。
updatedAt がなければ「今の時刻」を入れる。

つまり、「足りないものを補って、必ず { id, title, body, updatedAt } の形に揃える」関数です。

ここで深掘りしたいのは、
「永続化されたデータは、必ずしも完璧ではない」という前提で設計していることです。
だからこそ、「読み込み時に整える」というレイヤーを用意しているわけです。


state と永続化レイヤーの関係をシンプルにする

state の初期化を「load の結果」で行う

今までは、load 関数の中で state を直接書き換えていました。
6日目では、「load は配列を返すだけ」にして、
state の初期化はこう書きます。

const state = {
  memos: loadMemosFromStorage(),
  editingMemoId: null,
  saveStatus: "idle",
  searchQuery: "",
};
JavaScript

ここでのポイントは、

state の初期値が「一目でわかる」こと。
「memos は localStorage から読み込んだ結果なんだな」とすぐ理解できること。

load 関数が「state を直接いじらない」ことで、
state の管理が app.js の中で完結するようになります。

保存時も「state を渡すだけ」にする

メモ追加・編集・削除の関数では、
state.memos を更新したあと、こう呼びます。

saveMemosToStorage(state.memos);
JavaScript

例えば、追加はこうなります。

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);
  saveMemosToStorage(state.memos);
  render();
}
JavaScript

ここでの重要ポイントは、
「永続化レイヤーは state を知らない」「state 側が永続化レイヤーを使う」という一方向になっていることです。
これにより、「どこがアプリ本体で、どこが保存担当か」がはっきり分かれます。


保存ステータスと永続化レイヤーの関係を整理する

saveMemosToStorage の責務を“保存だけ”にする

前回は saveMemosToStorage の中で saveStatus をいじっていましたが、
6日目では責務を分けてみます。

永続化レイヤーの saveMemosToStorage は「保存するだけ」。
saveStatus の更新は「アプリ側の関数」がやる。

例えば、こんな関数を用意します。

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

  saveMemosToStorage(state.memos);

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

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

そして、追加・編集・削除では

state.memos = ...;
persistMemosWithStatus();
render();
JavaScript

のように呼びます。

ここでの深掘りポイントは、

「保存そのもの」と「保存状態の UI 表示」を分けていること。
永続化レイヤーは「保存の技術的な部分」だけを担当し、
UX に関わる部分(ステータス表示)はアプリ側に残していること。

責務を分けることで、「どこを読めば何がわかるか」がクリアになります。


コード全体を“説明しやすい構造”に並べ替える

並び順も設計の一部だと意識する

app.js の中身を、ざっくりこんな順番に並べると読みやすくなります。

定数・DOM 取得。
永続化レイヤー(STORAGE_KEY, migrateRawMemos, load, save)。
state の定義。
描画系(render, renderMemoItem, renderMemoItemDisplay, renderMemoItemEditing, renderSaveStatus)。
ロジック系(addMemo, updateMemo, deleteMemo, startEditMemo, finishEditMemo, getVisibleMemos)。
イベント登録(ボタン・入力欄・検索欄など)。
初期化(render の初回呼び出し)。

この順番にしておくと、
上から読んでいくだけで「このアプリがどう動いているか」が自然と見えてきます。

「自分で読んでみて、引っかかるところ」を探す

6日目で一番大事なのは、
「自分のコードを、自分で“設計として”読み直してみること」です。

例えば、こんな視点で眺めてみてください。

この関数の名前だけで、何をしているかイメージできるか。
この変数は、役割がはっきりしているか。
localStorage に触っている場所は、本当に永続化レイヤーだけか。
state を変えている場所は、追いやすい数に収まっているか。

引っかかるところがあれば、それは「設計をもう一歩よくできる場所」です。
そこに気づけるようになっている時点で、もう中級の入り口は越えています。


6日目のまとめと、明日へのつなぎ

6日目であなたがやったのは、「永続化まわりを“モジュールっぽく”整理すること」でした。

localStorage に触る処理を load / save に集約し、「ここを見れば永続化がわかる」状態にしたこと。
migrateRawMemos で、古い形式のデータを新しい形に揃えるマイグレーションを設計したこと。
state の初期化を「load の結果」で行い、初期状態が一目でわかるようにしたこと。
保存そのもの(saveMemosToStorage)と、保存ステータス表示(persistMemosWithStatus)を分けたこと。
ファイル内の関数の並び順を意識して、「上から読んで理解しやすい構造」に近づけたこと。

明日(7日目)はいよいよ総仕上げとして、

このメモ帳アプリの設計を「自分の言葉で説明できるか」を整理する。
もし機能を 1 つ足すなら、どこをどう変えるかを設計目線で考えてみる。
localStorage・JSON・永続化設計で身につけた“型”を振り返る。

という 1 日にしていきます。

今日のコード、ぜひ一度「他人になったつもり」で読んでみてください。
「ここ、ちょっと気持ちいいな」と思えるところと、「ここはまだモヤっとするな」というところ——
その両方が、あなたの設計センスの輪郭になっていきます。

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