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,
};
JavaScriptid は一意な識別子(編集・削除・特定に使う)。
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 で古いデータを救ってあげるところかもしれないし、
保存ステータスで「保存しました」と出る、あの一瞬かもしれません。
その「好きだと思えたところ」が、あなたの設計センスの核です。
そこを意識して伸ばしていくと、次に作るアプリは、もう一段“あなたらしい設計”になります。


