初めてのWebアプリの作り方|追加・削除・保存ができる学習メモを1ファイルで作る。講座が関数で終わった、その次の1本
執筆・監修: 徐 聖博/株式会社シンシア 代表取締役社長
初めてのWebアプリを、追加・削除・保存(localStorage)だけの学習メモで作ります。index.html 1ファイルで、空の入力・保存の失敗・壊れた保存データまでわざと起こして確かめるテスト表つき。ナミノリのJavaScript初級講座は関数で終わり、画面を持つものを作る回がありません。その次に作る1本です。
目次
初めてのWebアプリの作り方|追加・削除・保存ができる学習メモを1ファイルで作る
入門教材を終えたあと、最初に作るWebアプリは機能を少なくして、完成を自分で確かめられるものにしてください。この記事では、学んだことを1行ずつ書き留める「学習メモ」を作ります。できるのは3つだけです。
- メモを追加する
- メモを1件ずつ削除する
- ページを閉じても、同じブラウザで開き直せばメモが残っている
ファイルは index.html の1つだけ。ログインも、サーバーのデータベースも使いません。そのかわり、入力が空のとき、保存に失敗したとき、保存データが壊れていたときにどうなるかまで、自分で試して確かめます。
なぜ「次の1本」が要るのか:講座は関数で終わっている
先に、ナミノリ自身の講座の話をします。
ナミノリのJavaScript初級講座は全15回で、最後の3回は「関数について」「様々な関数(前半・後半)」です。修了試験の10問も、コードの出力を予想する問題と、関数を書く問題でできています。HTMLの画面を操作する問題はありません。入力欄があって、ボタンを押すと画面が変わり、閉じても結果が残る——そういう画面を1つ作り切る回は、講座の中にありません。画面の要素を操作する話(DOM操作やイベント)は、3・4回目「JavaScriptを実際に動かしてみよう①②」で扱うだけです。
受講の記録を見ると、こうなっていました。
| 項目 | 人数 |
|---|---|
| 講座に登録した社外の受講者 | 16人 |
| 15回すべてを完了した人 | 4人 |
| そのうち修了試験を提出した人 | 1人 |
| 15回を終えたあと、別の講座に登録した人 | 1人(3人は登録なし) |
いちばん最近15回を終えた人は、2026年10月1日でした。
4人のうち3人は、15回を終えたあと、ナミノリの別の講座に登録していません。3人のうち1人は講座4より前に別の3講座を修了済み、1人は講座4より前に別の2講座に登録済みで、残る1人は講座4以外の記録がありません。登録しなかった理由は、記録からは分かりません。ただ、講座の側には「関数を習ったあとに、画面を持つものを1本作る」段階が用意されていなかった。これは事実です。
この記事は、その段階を埋めるために作りました。
※ 社内の確認用アカウントを除いて集計しました。集計は2026年10月6日。人数が少ないので、割合ではなく人数で載せています。
作るものの仕様を、先に決める
コードを書く前に、何ができれば完成かを決めます。あとで作るテスト表の「期待する結果」は、ここから決まります。
| 項目 | 決めたこと |
|---|---|
| 1件のメモ | 1〜80文字。空白だけは受け付けない |
| 件数 | 最大100件 |
| 削除 | 1件ずつ |
| 保存先 | 同じブラウザの localStorage。外部のサーバーへは送らない |
| 保存に失敗したとき | 画面上では使い続けられる。ただし、再読み込みで消えるかもしれないと表示する |
| 保存データが読めないとき | 空の状態で始め、その旨を表示する |
| 作らないもの | ログイン、複数人での共有、別の端末への同期 |
localStorage は、サイトごとにブラウザの中へ文字列を保存できる仕組みです。同じブラウザで同じサイトを開けば読み出せますが、別のブラウザや別の端末には届きません。バックアップでもありません。
コード(index.html 1ファイル)
新しいフォルダ(例:memo-app)を作り、次の内容を index.html という名前で保存してください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>学習メモ</title>
</head>
<body>
<main>
<h1>学習メモ</h1>
<p>このブラウザだけに保存します。機密情報は入力しないでください。</p>
<form id="form">
<label for="text">学んだこと(80文字まで)</label>
<input id="text" maxlength="80" required autocomplete="off">
<button type="submit">追加</button>
</form>
<p id="message" role="status"></p>
<ul id="list"></ul>
</main>
<script>
const KEY = "naminori-practice-notes-v1";
const form = document.querySelector("#form");
const input = document.querySelector("#text");
const list = document.querySelector("#list");
const message = document.querySelector("#message");
let notes = [];
try {
const raw = localStorage.getItem(KEY);
if (raw !== null) {
const saved = JSON.parse(raw);
const valid = Array.isArray(saved) && saved.length <= 100 &&
saved.every(text => typeof text === "string" &&
text.trim().length > 0 && text.length <= 80);
if (!valid) throw new Error("保存データの形式が不正です");
notes = saved;
}
} catch {
message.textContent = "保存済みデータを読めませんでした。空の状態で開始します。";
}
function persist() {
try {
localStorage.setItem(KEY, JSON.stringify(notes));
message.textContent = `${notes.length}件をこのブラウザに保存しました。`;
} catch {
message.textContent = "保存できません。画面上では使えますが、再読込で失われる可能性があります。";
}
}
function render() {
list.replaceChildren();
notes.forEach((text, index) => {
const li = document.createElement("li");
const label = document.createElement("span");
label.textContent = text;
const remove = document.createElement("button");
remove.type = "button";
remove.textContent = "削除";
remove.setAttribute("aria-label", `${text}を削除`);
remove.addEventListener("click", () => {
notes.splice(index, 1);
persist();
render();
});
li.append(label, " ", remove);
list.append(li);
});
}
form.addEventListener("submit", event => {
event.preventDefault();
const text = input.value.trim();
if (text.length === 0 || text.length > 80) {
message.textContent = "1〜80文字で入力してください。";
return;
}
if (notes.length >= 100) {
message.textContent = "最大100件です。不要なメモを削除してください。";
return;
}
notes.push(text);
persist();
render();
input.value = "";
input.focus();
});
render();
</script>
</body>
</html>
講座で習ったことは、どこに使われているか
このコードは、新しい文法ばかりでできているわけではありません。
| コードの部分 | 使っている知識 | 講座の回 |
|---|---|---|
document.querySelector("#form") |
CSSセレクター(# はid) |
2回目 |
let notes = []、notes.push(text)
|
変数と配列 | 6・8回目 |
if (notes.length >= 100) |
条件分岐 | 8・9回目 |
notes.forEach((text, index) => { ... }) |
繰り返し、アロー関数 | 10・11・14回目 |
function persist() { ... } |
関数 | 13回目 |
addEventListener("submit", ...) |
イベント、コールバック関数 | 3・4・15回目 |
講座に出てこないのは、次の4つです。
-
localStorage.setItem/getItem:文字列を保存する・読み出す -
JSON.stringify/JSON.parse:配列を文字列に変える・文字列から配列に戻す(localStorageは文字列しか保存できないため) -
try { ... } catch { ... }:失敗したときの処理を分ける -
textContent:入力された文字を、HTMLとしてではなく文字のまま表示する
新しいことは4つだけです。残りは、15回の中で一度は見たものの組み合わせです。
起動する:サーバー経由で開く
フォルダの中で次のコマンドを実行します(Python 3 が必要です)。
python3 -m http.server 8000 --bind 127.0.0.1
ブラウザで http://127.0.0.1:8000/ を開きます。終わったら、ターミナルで Ctrl + C を押して止めます。
--bind 127.0.0.1 は、自分のパソコンからしか開けないようにする指定です。付けないと、同じネットワークの他の端末からフォルダの中身が見える状態になります。練習用の専用フォルダで起動し、仕事の資料があるフォルダでは起動しないでください。
index.html をダブルクリックして開く(アドレスが file:// で始まる)こともできます。2026年10月6日に Chrome で試したところ、file:// で開いても再読み込み後にメモは残りました。ただ、file:// のページの保存の扱いはブラウザによって違い、同じ扱いになる保証がありません。この記事では、結果がそろうようにサーバー経由で開きます。
テスト表を埋める
仕様から決めた「期待する結果」と、実際の結果を比べます。次の表は、2026年10月6日に Chrome で、この記事のコードをそのまま動かして確かめたものです。
| 操作 | 期待する結果 | 実際の表示 |
|---|---|---|
| 「変数を復習」を追加 | 一覧に1件出る | 1件をこのブラウザに保存しました。 |
| スペースだけを入れて追加 | 追加されない | 1〜80文字で入力してください。 |
| 2件追加して、2件目を削除 | その1件だけ消える | 残りの件数で「保存しました」 |
| ページを再読み込み | 同じメモが残っている | 残っていた |
<b>学習</b> と入力して追加 |
太字にならず、文字のまま出る | 文字のまま(太字の要素は0個) |
| 100件ある状態で追加 | 追加されない | 最大100件です。不要なメモを削除してください。 |
スペースだけの入力は、入力欄の required(空欄を止める)を通り抜けます。スペースは「空欄」ではないからです。止めているのは、JavaScriptの text.trim().length === 0 のほうです。ブラウザの入力チェックだけに頼らない理由が、ここで1つ分かります。
100件の確認は、手で100回入力しなくてもできます。検証画面(右クリック →「検証」)の Console で次を実行し、ページを再読み込みしてください。
localStorage.setItem("naminori-practice-notes-v1", JSON.stringify(Array.from({ length: 100 }, (_, i) => "メモ" + i)));
わざと失敗させる
成功する操作だけでは、完成とは言えません。仕様で決めた「失敗したとき」も確かめます。1つ試したら、Consoleで localStorage.clear() を実行して元に戻してください。
保存データが壊れている
Console で次を実行し、ページを再読み込みします。
localStorage.setItem("naminori-practice-notes-v1", '["壊れ');
「保存済みデータを読めませんでした。空の状態で開始します。」と表示され、一覧は空になります。JSON.parse が失敗して catch に進んだからです。["壊れ の代わりに {"a":1}(配列ではない)を入れても、同じ表示になりました。形を確かめる valid の判定が、配列でないデータを止めています。
このとき、開いただけでは保存データは上書きされません。検証画面の「Application」→「Local Storage」で見ると、["壊れ が残っています。ただし、ここでメモを1件追加すると、保存が成功して新しいメモだけの内容に置き換わります。壊れたデータを取っておきたいなら、追加する前に写しておく必要があります。これも、このアプリの仕様です。
保存に失敗する
ブラウザの容量がいっぱいのときなどは、localStorage.setItem が失敗します。Console で次を実行すると、その状態をまねできます(再読み込みすると元に戻ります)。
Storage.prototype.setItem = () => { throw new Error("test"); };
この状態でメモを追加すると、「保存できません。画面上では使えますが、再読込で失われる可能性があります。」と表示されます。メモは一覧に出て、続けて追加も削除もできます。再読み込みすると、そのメモは消えていました。画面に出ていることと、保存されていることは別だと分かります。
この作品でできていないこと
- ログインも権限もない。誰のメモかを区別しない
- メモはこのブラウザの中にしかない。別の端末から見られず、ブラウザのデータを消せば消える
- 同じサイトで動く他のプログラムからも読める。パスワードや個人情報は保存しない
- 自分のパソコンで動かす練習用。インターネットに公開するサービスではない
「できていないこと」を言えることは、作ったものを説明するときに役に立ちます。何を作って、何を作らなかったのか。その線引きも設計の一部です。
次に足す機能は、1つだけ
次の3つから1つだけ選んで、足してください。
- 完了チェック(済んだメモに印を付ける)
- 作成日時(いつ書いたメモかを表示する)
- 検索(入力した文字を含むメモだけを表示する)
足す前に、テスト表に「その機能の操作」と「期待する結果」を1行以上書き足します。足したあとは、元のテスト表も上から全部やり直します。新しい機能が、追加・削除・保存を壊していないかを見るためです。
完了チェックと作成日時は、保存するデータの形が「文字列の配列」から「{ text: "...", done: false } のようなオブジェクトの配列」に変わります。すると、いまの valid の判定は、新しい形のデータを「不正」とみなします。判定を書き換えるのと一緒に、保存の名前(KEY)を v2 に変えておくと、古い形のデータと混ざりません。データの形を変えると、保存・読み込み・形の確認の3か所に手が入る。これが、画面を持つアプリを作るときに初めてぶつかる種類の問題です。
よくある質問
ToDoアプリと同じものですか?
作りは同じです。題材を「学んだことのメモ」にしたのは、作ったあとも自分で使えるからです。ToDoアプリとして作ってもかまいません。
ReactやVue.jsで作ったほうがいいですか?
最初の1本は、ライブラリなしで作ることをおすすめします。画面を書き換える処理(この記事の render)を自分で書いておくと、Reactが何を代わりにやってくれているのかが分かるからです。
インターネットに公開できますか?
ファイル1つで動くので、静的なファイルを置けるサービスに置けば開けます。ただし、メモは見た人それぞれのブラウザに保存されます。誰かが書いたメモを、ほかの人が見られるようにはなりません。それをするには、サーバーとデータベースが必要です。
エラーが出て動きません
検証画面の Console に出ている赤い文字を読みます。読み方はJavaScriptのエラーの読み方にまとめています。よくあるのは、querySelector に渡したidと、HTMLのidの書き間違いです。
まとめ
- 最初のWebアプリは、追加・削除・保存の3つだけにして、完成を自分で確かめられる大きさにする
- 仕様を先に決めると、テスト表の「期待する結果」が決まる
- 入力が空、保存の失敗、保存データの破損まで試して、はじめて完成と言える
- ナミノリのJavaScript初級講座は関数で終わり、画面を持つものを作る回が無い。この記事はその次の1本として作った
- 次は機能を1つだけ足し、テスト表を全部やり直す
コードの意味が追えない行があれば、JavaScript初級講座(全15回・無料)の該当する回に戻ってください。上の対応表に回の番号を載せています。保存されたデータを検証画面で確かめる方法は、Chrome DevTools入門講座の9回目「Applicationパネル」で扱っています。
文法の練習が足りないと感じたらJavaScript練習問題10問へ。メモを保存する先をサーバーに変えたくなったら、その入口はAPIとは?(JSONの取得と失敗したとき)です。
作ったメモアプリを、応募で見せられる形に整える手順(READMEに書く六つの項目、AIを使った範囲の書き分け)は未経験からWebプログラマーになるにはにまとめています。
参考資料
- MDN「Window: localStorage プロパティ」 https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage
- MDN「JSON.parse()」 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
- MDN「Node: textContent プロパティ」 https://developer.mozilla.org/ja/docs/Web/API/Node/textContent
- MDN「try...catch」 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/try...catch
- Python ドキュメント「http.server」 https://docs.python.org/ja/3/library/http.server.html
無料の学習講座で、いまから手を動かす
AI開発からWeb開発まで、未経験でも進められる講座を無料で公開しています。
会員登録(無料) すると、気になる求人の保存と講座の受講履歴の記録ができます。
この順番で合っているか、確認しませんか
学習の順番は、目指す職種によって変わります。いま考えている進め方を書いて送ってもらえれば、無料講座の内容と実際の求人要件に照らして返答します。
相談を書いて送る(無料・会員登録なし)返答までお時間をいただくことがあります。
新着記事
社会人のプログラミング独学は1日30分で足りる?受講ログで見た「2日目」と再開のルール
社会人のプログラミング独学は1日30分で足りるのか。ナミノリの受講ログ(登録124件・完了698件)では、1日に学ぶ量の中央値は目安16分で8割が30分以内。止まる原因は時間ではなく、7割が2日目に戻らないことでした。最初の講座の選び方と、休んだあとの再開のルールをまとめます。
初めてのWebアプリの作り方|追加・削除・保存ができる学習メモを1ファイルで作る。講座が関数で終わった、その次の1本
初めてのWebアプリを、追加・削除・保存(localStorage)だけの学習メモで作ります。index.html 1ファイルで、空の入力・保存の失敗・壊れた保存データまでわざと起こして確かめるテスト表つき。ナミノリのJavaScript初級講座は関数で終わり、画面を持つものを作る回がありません。その次に作る1本です。
APIとは?初心者向けに、JSONの取得と「失敗したとき」を動かして解説。ナミノリの求人検索にも抜けていた response.ok
APIは、プログラム同士がデータ(JSON)をやり取りする窓口です。ナミノリの求人検索の裏で動く件数APIをNetworkタブで確かめ、自分のパソコンだけでJSONを取得するページを作ります。fetchは404でも失敗にならないため、response.ok・データの形の確認・catch・finallyで失敗に備える書き方を、わざと壊して確かめます。
Webの仕組みを初心者向けに解説|URLを打ってから画面が出るまでを、実際の通信で確かめる。GETとPOSTはデータ量で選ばない
URLを打ってから画面が出るまでの「DNS・リクエスト・レスポンス・表示」を、ナミノリのサイトとChromeのNetworkタブで実際に確かめます。ステータスコード200・301・404の見方と、GETとPOSTの選び方(取ってくるか、変えるか)も解説。講座の確認テストにあった誤りの訂正つき。