プログラミング学習 公開 2026.10.06 更新 2026.10.07

初めてのWebアプリの作り方|追加・削除・保存ができる学習メモを1ファイルで作る。講座が関数で終わった、その次の1本

執筆・監修: 徐 聖博/株式会社シンシア 代表取締役社長

初めての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プログラマーになるにはにまとめています。

参考資料

無料の学習講座で、いまから手を動かす

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の選び方(取ってくるか、変えるか)も解説。講座の確認テストにあった誤りの訂正つき。