この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 },
];
JavaScriptstate オブジェクトを用意する
アプリの状態をひとまとめにしたオブジェクトを作ります。
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」パターン
いつ保存するか?設計の選択肢(重要)
保存タイミングはいくつかパターンがあります。
- 入力のたびに保存(タイトル・本文の input / change)
- 特定の操作のあとだけ保存(新規、削除、フォーカスを離れたときなど)
- 明示的な「保存」ボタンを押したときだけ保存
それぞれの特徴です。
- 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();
}
JavaScriptupdateSelectedMemo も変更します。
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 点です。
- 状態と UI の分離
- 真実は state にだけ置き、UI は常に state から描画する。
- DOM を「状態の元」として考えない。
- JSON と localStorage の境界
- 内部はオブジェクト配列で扱いやすく設計。
- 保存するときだけ JSON.stringify で文字列化。
- 読み込むときだけ JSON.parse で戻す。
- 永続化の仕様変更(例えば IndexedDB にするなど)も、この境界付近を書き換えるだけで済む。
- 保存タイミングの一元化
- 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」「永続化」などの概念もかなり理解しやすくなります。


