MENU

localStorageで学習進捗を保存する方法|回答履歴・正答率をブラウザに残す

HTMLとJavaScriptで学習サイトを作っても、ページを再読み込みするたびに回答状況が消えてしまうと、毎回最初からやり直すことになります。

私がAWS資格用の学習サイトを作ったときも、最初は問題を表示して採点するだけでした。しかし、SOA-C03の問題398問を扱うようになると、「前回どこまで進んだか」「どの問題を間違えたか」「分野別の正答率は何%か」を残せないことが大きな不便になりました。

そこで利用したのがlocalStorageです。localStorageを使えば、サーバーやデータベースを用意しなくても、同じブラウザへ学習進捗を保存できます。

この記事では、localStorageで学習進捗を保存する方法を、保存、読み込み、正答率の計算、不正解問題の抽出、履歴の初期化、書き出し、複数タブの同期まで順番に解説します。

この記事のコードについて
掲載するコードは、学習進捗を保存する仕組みを説明するための最小例です。実際のAWS認定試験問題や、第三者が作成した問題文は使用していません。
また、localStorageは秘密情報を安全に保管する仕組みではありません。パスワード、認証用トークン、AWSアクセスキーなどは保存しないでください。

AWS資格用の学習サイト全体の作り方は、次の記事で解説しています。

AWS資格の学習サイトをHTMLで自作した方法

SOA-C03の398問を5分野へ分類した結果は、以下の記事をご覧ください。

SOA-C03の398問を出題分野別に分類した結果

目次

localStorageで学習進捗を保存する仕組み

localStorageで学習進捗を保存する前に、どこへ、どの単位でデータが残るのかを理解しておきましょう。

localStorageは、Webブラウザが持つ保存領域です。ページ側のJavaScriptから、キーと値の組み合わせとしてデータを保存できます。

localStorageとは何か

localStorageは、同じオリジンに属するページから利用できる保存領域です。

オリジンとは、主に次の3つの組み合わせを指します。

  • 通信方式
  • ホスト名
  • ポート番号

たとえば、次の2つは別のオリジンです。

http://example.com
https://example.com

通信方式がHTTPとHTTPSで異なるため、同じホスト名でもlocalStorageは共有されません。

一方、次のページは通常、同じオリジンとして同じlocalStorageを利用します。

https://example.com/study/
https://example.com/study/result/

学習サイトでaws-study-historyというキーへ進捗を保存すると、同じオリジン内の別ページからも読み込めます。

localStorageへ保存したデータには、通常の利用で自動的に決まる有効期限がありません。ブラウザを閉じて再び開いた後も残るため、学習進捗の保存に向いています。

ただし、利用者がブラウザのデータを削除した場合や、保存を拒否する設定を使用している場合は残りません。シークレットモードやプライベートブラウズでは、最後の専用タブを閉じた時点で削除されることがあります。

参考:Window.localStorage|MDN

localStorageへ保存できる値は文字列

localStorageが保存するキーと値は文字列です。

次のような単純な文字列なら、そのまま保存できます。

localStorage.setItem("study-status", "completed");

しかし、学習履歴は問題番号、正誤、選んだ回答、日時などをまとめたオブジェクトです。

JavaScriptのオブジェクトをそのまま保存すると、期待した内容になりません。

const history = {
  10: {
    isCorrect: false
  }
};

localStorage.setItem("history", history);

console.log(localStorage.getItem("history"));
// [object Object]

そこで、JSON.stringify()を使って文字列へ変換します。

const json = JSON.stringify(history);

localStorage.setItem("history", json);

読み込むときは、JSON.parse()でJavaScriptのオブジェクトへ戻します。

const saved = localStorage.getItem("history");
const restoredHistory = JSON.parse(saved);

この「保存前に文字列へ変換し、読み込み後に元へ戻す」という流れが、localStorageで学習進捗を保存する基本です。

参考:Storage.setItem()|MDN

localStorageとsessionStorageの違い

localStorageと似た仕組みにsessionStorageがあります。

主な違いは保存期間と利用範囲です。

項目localStoragesessionStorage
保存期間ブラウザを閉じても通常は残るページセッションが終わると消える
利用範囲同じオリジン同じオリジンかつ同じタブ単位
向いている用途学習履歴、表示設定入力途中の一時保存
別タブとの共有同じオリジンなら可能基本的にタブごとに分かれる

AWS資格の問題を翌日も続けたい場合は、localStorageが適しています。

反対に、模擬試験の途中経過を「タブを閉じたら消す」仕様にしたいなら、sessionStorageも選択肢になります。

Cookieとの違い

Cookieもブラウザへ情報を残す仕組みですが、localStorageとは用途が異なります。

Cookieは、設定によってHTTPリクエストと一緒にサーバーへ送信されます。一方、localStorageの内容はブラウザから自動送信されません。

今回の学習サイトはサーバーを使わず、ブラウザ内だけで回答履歴を管理します。そのため、CookieよりlocalStorageの方が構成を単純にできます。

ただし、利用者認証や安全なセッション管理が必要な場合は話が別です。認証情報をlocalStorageへ保存するのではなく、サーバー側の設計も含めて検討してください。

学習進捗として保存するデータを決める

localStorageで学習進捗を保存するときは、先にデータの形を決めます。

後から項目を思いつくたびに追加すると、過去の保存データと新しいコードが合わなくなりやすいためです。

問題IDごとに回答結果を保存する

最小構成では、問題IDをキーとして回答結果を保存します。

{
  "10": {
    "isCorrect": false,
    "selectedAnswers": [1],
    "answeredAt": "2026-07-04T10:00:00.000Z"
  }
}

各項目の役割は次のとおりです。

項目保存する内容
問題IDどの問題の履歴か
isCorrect正解か不正解か
selectedAnswers利用者が選んだ回答
answeredAt回答した日時

問題10へ回答した場合は、history["10"]へ結果を入れます。

history["10"] = {
  isCorrect: false,
  selectedAnswers: [1],
  answeredAt: new Date().toISOString()
};

問題IDを文字列にしているのは、JavaScriptのオブジェクトのキーとして扱いやすくするためです。

問題文ではなく問題IDを保存する

学習履歴には、問題文の全文を保存しない方が管理しやすくなります。

たとえば398問分の問題文と選択肢を回答のたびにlocalStorageへ重複保存すると、データ量が増えるだけでなく、問題文を修正した際に古い文章が履歴へ残ります。

問題データはquestions.jsonで管理し、localStorageには問題IDと回答結果だけを入れます。

questions.json
└─ 問題文・選択肢・正答・分野

localStorage
└─ 問題ID・正誤・回答日時

この分け方なら、問題文を修正しても進捗データをそのまま利用できます。

直近結果だけを保存するか、回答回数も残すか

最初は直近の回答結果だけでも十分です。

{
  "isCorrect": true
}

しかし、「一度でも間違えた問題を残したい」「2回連続で正解したら復習対象から外したい」と考える場合は、項目を追加します。

{
  "lastResult": true,
  "attempts": 3,
  "correctCount": 2,
  "consecutiveCorrect": 1,
  "hasEverFailed": true
}

実際に私が398問を管理したときも、最初から複雑な履歴を作るより、直近の正誤だけで動作を確認する方が進めやすくなりました。

まずは必要最低限で完成させ、復習方法が固まってから項目を増やすのがおすすめです。

保存形式の版番号を決める

保存形式を将来変更する可能性があるなら、版番号を持たせます。

{
  "version": 1,
  "answers": {
    "10": {
      "isCorrect": false
    }
  }
}

または、localStorageのキーへ版番号を付ける方法もあります。

const STORAGE_KEY = "aws-study-history-v1";

後から保存形式を変えた場合は、v2へ変更できます。

const STORAGE_KEY = "aws-study-history-v2";

古い形式と新しい形式を区別できるため、「以前のデータが原因で画面が動かない」という問題を減らせます。

localStorageへ学習進捗を保存する

データの形を決めたら、回答直後にlocalStorageへ保存します。

基本の流れは次のとおりです。

  1. 回答を取得する
  2. 正誤を判定する
  3. 履歴オブジェクトを更新する
  4. localStorageへ保存する
  5. 正答率を再計算する

setItemでデータを保存する

setItem()は、指定したキーへ値を保存するメソッドです。

localStorage.setItem(
  "study-status",
  "completed"
);

同じキーがすでに存在する場合は、値が更新されます。

学習履歴では、毎回新しいキーを増やすのではなく、履歴全体を1つのキーへ保存すると扱いやすくなります。

JSON.stringifyで履歴を文字列へ変換する

次の履歴を保存してみます。

const history = {
  "10": {
    isCorrect: false,
    selectedAnswers: [1]
  },
  "11": {
    isCorrect: true,
    selectedAnswers: [0, 2]
  }
};

JSON.stringify()で文字列へ変換してから保存します。

const json = JSON.stringify(history);

localStorage.setItem(
  "aws-study-history-v1",
  json
);

ブラウザの開発者ツールで確認すると、値はJSON形式の文字列として表示されます。

回答した直後に保存する

回答結果を記録する関数を作ります。

function recordAnswer(
  questionId,
  isCorrect,
  selectedAnswers
) {
  history[String(questionId)] = {
    isCorrect,
    selectedAnswers,
    answeredAt: new Date().toISOString()
  };

  saveHistory();
}

問題10で選択肢1を選び、不正解だった場合は次のように呼び出します。

recordAnswer(10, false, [1]);

回答のたびに保存することで、利用者が次の問題へ進む前にページを閉じても、直前の結果まで残せます。

保存処理を関数へまとめる

保存処理は複数箇所へ直接書かず、関数へまとめます。

const STORAGE_KEY = "aws-study-history-v1";

function saveHistory() {
  localStorage.setItem(
    STORAGE_KEY,
    JSON.stringify(history)
  );
}

正答率画面、復習画面、模擬試験画面などから同じ履歴を更新する場合でも、保存処理を1か所だけ直せば済みます。

この段階では短いコードですが、後からエラー処理を加えると関数へ分けた効果が大きくなります。

保存した学習進捗を読み込む

次に、ページを開いたときにlocalStorageから学習進捗を読み込みます。

保存できても復元しなければ、画面上では進捗が消えたように見えてしまいます。

getItemで保存値を取得する

getItem()へ保存時と同じキーを渡します。

const saved = localStorage.getItem(
  "aws-study-history-v1"
);

該当するキーがない場合、戻り値はnullです。

初回利用時は保存データがないため、nullを前提に処理する必要があります。

JSON.parseでオブジェクトへ戻す

保存値がある場合は、JSON.parse()でオブジェクトへ戻します。

const history = saved
  ? JSON.parse(saved)
  : {};

保存値がなければ、空のオブジェクトを使います。

この1行だけでも動きますが、実際のサイトでは保存データが壊れている可能性も考え、後ほどtry...catchを追加します。

ページを開いたときに進捗を復元する

読み込み処理を関数にします。

function loadHistory() {
  const saved = localStorage.getItem(
    STORAGE_KEY
  );

  return saved ? JSON.parse(saved) : {};
}

let history = loadHistory();

ページを開いた後は、読み込んだ履歴を次の表示へ反映します。

  • 回答済み問題の印
  • 正解・不正解の状態
  • 全体の正答率
  • 分野別の正答率
  • 不正解問題一覧
  • 最後に回答した日時

たとえば問題10の履歴が存在するかは、次のように確認できます。

const savedResult = history["10"];

if (savedResult) {
  console.log("問題10は回答済みです。");
}

保存データがない場合は空の履歴を使う

初回利用者にとって、保存データがないのは正常な状態です。

「履歴が見つかりません」というエラーを表示する必要はありません。

return {};

空の履歴を返し、回答数0問、正答率0%として画面を表示します。

保存と読み込みで起こるエラーに対応する

localStorageは主要ブラウザで広く利用できますが、必ず保存できるとは限りません。

ブラウザの設定、保存容量、プライベートブラウズ、壊れたJSONなどを考慮しておきましょう。

JSON.parseの失敗を考える

保存値がJSONとして正しくない場合、JSON.parse()は例外を発生させます。

たとえば利用者が開発者ツールから値を書き換えたり、以前のコードが不正な値を保存したりした場合です。

function loadHistory() {
  try {
    const saved = localStorage.getItem(
      STORAGE_KEY
    );

    return saved ? JSON.parse(saved) : {};
  } catch (error) {
    console.error(
      "学習履歴を読み込めませんでした。",
      error
    );

    return {};
  }
}

読み込みに失敗しても、空の履歴で学習を開始できるようにします。

読み込んだデータの形を確認する

JSONとして正しくても、期待するオブジェクトとは限りません。

function isHistoryObject(value) {
  return (
    typeof value === "object" &&
    value !== null &&
    !Array.isArray(value)
  );
}

読み込み処理へ組み込みます。

function loadHistory() {
  try {
    const saved = localStorage.getItem(
      STORAGE_KEY
    );

    if (!saved) {
      return {};
    }

    const parsed = JSON.parse(saved);

    return isHistoryObject(parsed)
      ? parsed
      : {};
  } catch (error) {
    console.error(
      "学習履歴を読み込めませんでした。",
      error
    );

    return {};
  }
}

本格的な公開サイトでは、問題ID、isCorrectselectedAnswersなどの項目まで検証すると安全です。

保存処理もtry…catchで囲む

保存容量の上限へ達した場合や、ブラウザが保存を拒否した場合、setItem()が例外を発生させる可能性があります。

function saveHistory() {
  try {
    localStorage.setItem(
      STORAGE_KEY,
      JSON.stringify(history)
    );

    return true;
  } catch (error) {
    console.error(
      "学習進捗を保存できませんでした。",
      error
    );

    return false;
  }
}

回答処理側で結果を確認します。

const saved = saveHistory();

if (!saved) {
  resultElement.textContent =
    "回答は採点しましたが、進捗を保存できませんでした。";
}

localStorageへ書き込めるか確認する

ブラウザがlocalStorageの機能を持っていても、設定、プライベートブラウズ、容量不足などにより、実際の書き込みが失敗する場合があります。

学習進捗を保存できるか確認するなら、テスト用の値を書き込み、すぐ削除する方法が分かりやすいでしょう。

function canWriteLocalStorage() {
  const testKey = "__storage_test__";

  try {
    localStorage.setItem(testKey, testKey);
    localStorage.removeItem(testKey);
    return true;
  } catch (error) {
    console.error(
      "localStorageへ書き込めません。",
      error
    );

    return false;
  }
}

初期化時に確認します。

if (!canWriteLocalStorage()) {
  alert(
    "この環境では学習進捗を保存できません。"
  );
}

この関数が確認するのは、その時点でテスト書き込みに成功するかです。機能自体が未対応なのか、保存が禁止されているのか、容量上限へ達したのかまでは判別しません。

原因まで区別したい場合は、例外名や既存データの有無を追加で確認してください。

参考:ウェブストレージAPIの使用|MDN

ページを開いたときに回答状態を復元する

履歴を読み込んだら、問題画面へ回答状態を反映します。

ただし、前回の正答をすぐ見せると復習にならない場合があります。どこまで復元するかは、学習方法に合わせて決めてください。

回答済み問題へ印を付ける

問題一覧がある場合は、状態を表示します。

問題10 × 前回不正解
問題11 ○ 前回正解
問題12 - 未回答

状態を返す関数は次のように作れます。

function getQuestionStatus(questionId) {
  const result = history[String(questionId)];

  if (!result) {
    return "unanswered";
  }

  return result.isCorrect
    ? "correct"
    : "incorrect";
}

398問を一覧表示するときも、未回答と不正解を見分けやすくなります。

前回選んだ回答を再表示する

前回の選択肢を復元する場合は、保存したselectedAnswersを使います。

function restoreSelectedAnswers(question) {
  const savedAnswer =
    history[String(question.id)];

  if (!savedAnswer) {
    return;
  }

  savedAnswer.selectedAnswers.forEach(
    value => {
      const input = choicesForm.querySelector(
        `input[value="${value}"]`
      );

      if (input) {
        input.checked = true;
      }
    }
  );
}

問題の選択肢を画面へ作成した後で、この関数を呼び出します。

前回の正誤を表示するか決める

復元方法には次の選択肢があります。

  • 前回の正誤と解説をすぐ表示する
  • 選んだ回答だけ復元し、正誤は隠す
  • 「前回不正解」とだけ表示する
  • 復習モードでは前回の回答を一切表示しない

私なら、通常の確認画面では前回結果を表示し、復習モードでは隠します。

正解を覚えているか確認したいのに、画面を開いた瞬間に答えが見えてしまうと、localStorageで学習進捗を保存した意味が薄れてしまうためです。

localStorageの履歴から正答率を計算する

保存した履歴を使えば、全体と分野別の正答率を計算できます。

計算式は次のとおりです。

正答率 = 正解数 ÷ 回答数 × 100

未回答問題は分母へ含めません。

回答済み問題だけを集計する

問題データをquestions、履歴をhistoryとします。

const answeredQuestions = questions.filter(
  question =>
    history[String(question.id)]
);

この処理では、履歴が存在する問題だけを残します。

正解数を数える

回答済み問題から、isCorrecttrueの問題を抽出します。

const correctQuestions =
  answeredQuestions.filter(
    question =>
      history[String(question.id)]
        .isCorrect === true
  );

正答率を計算する

回答数が0問の場合は、0で割らないように分岐します。

const rate =
  answeredQuestions.length === 0
    ? 0
    : (
        correctQuestions.length /
        answeredQuestions.length
      ) * 100;

表示時に小数点以下1桁へ整えます。

const rateText = rate.toFixed(1);

集計処理を関数にする

function calculateStats(targetQuestions) {
  const answered = targetQuestions.filter(
    question =>
      history[String(question.id)]
  );

  const correct = answered.filter(
    question =>
      history[String(question.id)]
        .isCorrect === true
  );

  const rate = answered.length === 0
    ? 0
    : (
        correct.length /
        answered.length
      ) * 100;

  return {
    answered: answered.length,
    correct: correct.length,
    rate
  };
}

全問題の正答率は次のように取得します。

const overallStats =
  calculateStats(questions);

分野別の正答率を表示する

SOA-C03の問題には、1から5の分野番号を付けてあります。

const domain3Questions =
  questions.filter(
    question => question.domain === 3
  );

const domain3Stats =
  calculateStats(domain3Questions);

表示例は次のとおりです。

分野回答数正解数正答率
分野1302480.0%
分野2201365.0%
分野315746.7%

全体の正答率が高くても、分野3だけ低いことがあります。

この偏りを見つけられる点が、localStorageで学習進捗を保存する大きな利点です。

不正解問題だけを復習する

履歴へ正誤を保存しておけば、不正解問題だけを抽出できます。

398問を毎回解き直すより、直近で間違えた問題へ絞る方が復習しやすくなります。

isCorrectがfalseの問題を抽出する

const incorrectQuestions =
  questions.filter(
    question =>
      history[String(question.id)]
        ?.isCorrect === false
  );

?.を使うことで、未回答問題の履歴が存在しなくてもエラーになりません。

直近の回答結果を使う

最小構成では、最後の回答が不正解なら復習対象にします。

問題10を最初に間違え、その後に正解した場合は、不正解一覧から外れます。

この方法は単純で分かりやすい反面、偶然正解した問題も外れる可能性があります。

一度でも間違えた問題を残す

一度間違えた問題を残したい場合は、hasEverFailedを追加します。

function recordAnswer(
  questionId,
  isCorrect,
  selectedAnswers
) {
  const key = String(questionId);
  const previous = history[key];

  history[key] = {
    isCorrect,
    selectedAnswers,
    answeredAt: new Date().toISOString(),
    hasEverFailed:
      previous?.hasEverFailed === true ||
      isCorrect === false
  };

  saveHistory();
}

最後に正解しても、過去に間違えた問題として検索できます。

連続正解で復習対象から外す

2回連続で正解したら復習対象から外す場合は、consecutiveCorrectを更新します。

const consecutiveCorrect = isCorrect
  ? (previous?.consecutiveCorrect ?? 0) + 1
  : 0;

履歴へ保存します。

history[key] = {
  isCorrect,
  selectedAnswers,
  answeredAt: new Date().toISOString(),
  consecutiveCorrect
};

復習対象は次の条件で抽出できます。

const reviewQuestions =
  questions.filter(question => {
    const result =
      history[String(question.id)];

    return (
      result &&
      result.consecutiveCorrect < 2
    );
  });

学習方法に正解はありません。まずは直近結果で始め、必要になったら連続正解へ広げると無理なく作れます。

学習履歴を初期化する方法

学習を最初からやり直したい場合や、テスト用データを消したい場合は、履歴を初期化します。

削除処理は元に戻せないため、確認画面を表示しましょう。

removeItemで学習履歴だけを削除する

localStorage.removeItem(
  STORAGE_KEY
);

これで指定したキーだけを削除できます。

clearは安易に使わない

localStorageにはclear()もあります。

localStorage.clear();

ただし、clear()は同じオリジンに保存されたlocalStorageの項目をすべて削除します。

同じサイトでテーマ設定、表示設定、別資格の履歴などを保存している場合、それらも消えてしまいます。

学習履歴だけを削除するなら、removeItem()を使用してください。

削除前に確認する

function resetHistory() {
  const confirmed = window.confirm(
    "すべての学習履歴を削除しますか?"
  );

  if (!confirmed) {
    return;
  }

  localStorage.removeItem(STORAGE_KEY);
  history = {};

  renderQuestion();
  renderStats();
}

「キャンセル」を押した場合は何もしません。

初期化後に画面も更新する

localStorageの値を削除しても、JavaScriptの変数historyに古い内容が残っている場合があります。

そのため、次の3つをセットで行います。

  1. localStorageのキーを削除する
  2. historyを空にする
  3. 問題画面と集計画面を再表示する

保存領域だけでなく、画面上の回答数や正答率も0へ戻ることを確認しましょう。

学習履歴をJSONファイルとして書き出す

localStorageのデータは、そのブラウザの中にあります。

ブラウザデータを削除する前や、別のパソコンへ移す場合に備え、JSONファイルとして書き出せると便利です。

学習履歴を書き出す

function exportHistory() {
  const exportData = {
    version: 1,
    exportedAt: new Date().toISOString(),
    answers: history
  };

  const json = JSON.stringify(
    exportData,
    null,
    2
  );

  const blob = new Blob(
    [json],
    { type: "application/json" }
  );

  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");

  link.href = url;
  link.download =
    "aws-study-history.json";

  document.body.append(link);
  link.click();
  link.remove();

  setTimeout(() => {
    URL.revokeObjectURL(url);
  }, 0);
}

一時的なURLは、ダウンロード処理を開始した後で破棄します。この処理では、履歴、版番号、書き出し日時をJSONへまとめています。

書き出した履歴を読み込む

HTMLへファイル選択欄を用意します。

<label for="history-file">
  学習履歴を読み込む
</label>
<input
  type="file"
  id="history-file"
  accept="application/json"
>

読み込み処理の例です。

async function importHistory(file) {
  const text = await file.text();
  const imported = JSON.parse(text);

  if (
    imported.version !== 1 ||
    !isHistoryObject(imported.answers)
  ) {
    throw new Error(
      "対応していない履歴ファイルです。"
    );
  }

  history = imported.answers;

  if (!saveHistory()) {
    throw new Error(
      "読み込んだ履歴を保存できません。"
    );
  }

  renderQuestion();
  renderStats();
}

読み込む前にデータを確認する

JSONファイルだから安全とは限りません。

最低でも次の点を確認してください。

  • JSONとして正しいか
  • 版番号が対応しているか
  • answersがオブジェクトか
  • 問題IDが現在の問題集に存在するか
  • 回答配列が想定した形か
  • 不要な項目が混ざっていないか

読み込んだ文字列をinnerHTMLへ直接入れるのも避けます。画面へ表示するときは、原則としてtextContentを使用してください。

複数タブで学習進捗を同期する

同じ学習サイトを2つのタブで開くと、一方のタブで保存した結果が、もう一方の画面へすぐ反映されない場合があります。

このときはstorageイベントを利用できます。

storageイベントを使う

window.addEventListener(
  "storage",
  event => {
    if (event.key !== STORAGE_KEY) {
      return;
    }

    history = loadHistory();
    renderQuestion();
    renderStats();
  }
);

別のタブでSTORAGE_KEYが変更されると、履歴を読み直して画面を更新します。

変更したタブ自身では発生しない

storageイベントは、localStorageを変更したページ自身では発生しません。

たとえばタブAで回答した場合は次のようになります。

タブA
保存処理の直後に通常の関数で画面更新

タブB
storageイベントを受けて画面更新

そのため、回答したタブでは従来どおりrenderStats()を呼び出し、別タブだけstorageイベントで同期します。

参考:Window storageイベント|MDN

同じオリジンである必要がある

別のドメインや別の通信方式では、同じlocalStorageへアクセスできません。

次の2つは保存領域が分かれます。

http://example.com
https://example.com

開発中にHTTPの環境からHTTPSの公開サイトへ移したとき、履歴が消えたように見える場合があります。実際には、別のオリジンへ保存されている可能性があります。

localStorageの容量と制限を理解する

localStorageは、問題IDや正誤のような小さな文字データへ向いています。

一方、問題文、画像、動画などを大量に保存する用途には適していません。

保存できる容量には上限がある

MDNでは、Web Storageについて、オリジンごとにlocalStorageが5MiB、sessionStorageが5MiB、合計10MiBの上限があると説明しています。

上限へ達すると、setItem()QuotaExceededErrorが発生する可能性があります。

参考:ブラウザーストレージの容量と削除基準|MDN

398問分の問題ID、正誤、回答日時だけなら、通常は大きなデータにはなりません。

しかし、問題文や画像を毎回重複保存すると、容量を無駄に使います。

問題文を重複保存しない

次のように役割を分けます。

questions.json
├─ 問題文
├─ 選択肢
├─ 正答
└─ 分野

localStorage
├─ 問題ID
├─ 正誤
├─ 選択回答
└─ 回答日時

画像や長い解説は、localStorageへ入れず、問題データ側で管理してください。

localStorageは同期処理

localStorageの読み書きは同期処理です。

つまり、保存や読み込みが終わるまで、同じJavaScriptのほかの処理は待ちます。

MDNでも、localStorageとsessionStorageの操作は同期的に実行され、処理が完了するまで、ほかのJavaScript処理が止まる可能性があると説明されています。

問題IDや正誤だけを保存する小規模な学習サイトでは、影響を感じにくいでしょう。しかし、長い問題文や大量の回答履歴を頻繁に保存する構成では、画面の反応が遅くなる可能性があります。

扱うデータが増えた場合は、大量の構造化データを扱え、処理の多くが非同期で行われるIndexedDBへの移行を検討してください。

参考:Web Storage API|MDN

参考:IndexedDB API|MDN

ブラウザや設定によって保存できない場合がある

localStorageが利用できない主な例は次のとおりです。

  • 利用者が保存を拒否している
  • プライベートブラウズで制限されている
  • 保存容量の上限へ達した
  • ブラウザデータが削除された
  • file:形式でHTMLを直接開いている
  • ブラウザのポリシーで永続化が禁止されている

特にfile:形式でのlocalStorageの扱いは、仕様上の動作が定められていません。

手元で学習サイトを試す場合は、簡易Webサーバーを使い、http://localhostから開く方が安定します。

複数端末では共有されない

パソコンのlocalStorageとスマートフォンのlocalStorageは別です。

localStorageの仕様には、端末間でデータを同期する機能がありません。ブラウザや拡張機能が独自の同期機能を提供する場合を除き、通常のWebページが保存した学習履歴は、別の端末へ自動では引き継がれません。

複数端末で学習進捗を共有したい場合は、次の仕組みが必要です。

  • サーバー側の保存先
  • データベース
  • 利用者認証
  • 通信の暗号化
  • バックアップ

個人用サイトであれば、JSONの書き出しと読み込みで移行する方法も現実的です。

localStorageへ保存してはいけない情報

localStorageは、JavaScriptから読み書きできる保存領域です。

便利ですが、機密情報を安全に保管する場所ではありません。

パスワードを保存しない

学習サイトへログイン機能を追加しても、パスワードをlocalStorageへ保存しないでください。

暗号化したように見えても、復号に必要な情報を同じブラウザ側へ置けば、安全性を十分に確保できない場合があります。

認証が必要なら、サーバー側を含めて適切に設計します。

AWSアクセスキーを保存しない

次の情報はlocalStorageへ保存しないでください。

  • AWSアクセスキーID
  • AWSシークレットアクセスキー
  • セッショントークン
  • 認証用トークン
  • セッション識別子
  • パスワード
  • 決済情報
  • 重要な個人情報

AWS資格の学習サイトでAWS APIを試したくなっても、ブラウザのlocalStorageへ長期的なアクセスキーを置く構成は避けます。

ブラウザへ配信されるJavaScriptやlocalStorageは、利用者側の開発者ツールから確認できます。そのため、AWSアクセスキーを安全に隠す保管場所としては利用できません。

AWS公式ドキュメントでも、可能な限り長期的なアクセスキーを作成せず、IAMロールなどが発行する一時的な認証情報を使うよう推奨しています。一時的な認証情報には有効期限があるため、誤って公開された場合の影響を長期的なアクセスキーより抑えやすくなります。

学習サイトからAWSリソースへアクセスさせる必要がある場合も、長期的なアクセスキーをコードやlocalStorageへ直接保存せず、用途に応じてIAMロール、一時的な認証情報、Amazon Cognitoなどを検討してください。

参考:プログラムによるアクセスで使用するAWS認証情報|AWS

参考:SEC02-BP02 一時的な認証情報を使用する|AWS Well-Architected Framework

JavaScriptから読み取れる点を理解する

OWASPは、localStorageへ機密情報やセッション識別子を保存しないよう注意しています。

ページにクロスサイトスクリプティングの弱点がある場合、不正なJavaScriptからlocalStorageを読み書きされる可能性があるためです。

また、同じオリジン内の複数のページは同じlocalStorageへアクセスできます。

参考:HTML5 Security Cheat Sheet|OWASP

学習履歴だけを保存する

今回の用途では、次のデータに限定します。

  • 問題ID
  • 正解・不正解
  • 選んだ回答
  • 回答日時
  • 回答回数
  • 連続正解数
  • 復習予定日

これらも利用者にとっては大切な記録です。絶対に消えてはいけない場合は、localStorageだけに頼らず、書き出しや別の保存先も用意してください。

localStorageの保存処理を確認するテスト項目

コードが完成したら、正常な操作だけでなく、保存できない場合も確認します。

初回表示でエラーにならないか

localStorageに何も保存されていない状態でページを開きます。

確認する内容は次のとおりです。

  • 回答数が0問になる
  • 正答率が0%になる
  • 問題を選べる
  • 開発者ツールにエラーが出ない

回答後に履歴が保存されるか

問題へ回答し、開発者ツールの保存領域を確認します。

ChromeやEdgeでは、開発者ツールのApplication画面からlocalStorageを確認できます。FirefoxではStorage画面が用意されています。

ブラウザの画面構成は更新される場合があるため、名称が多少異なることがあります。

ページを再読み込みしても残るか

回答後にページを再読み込みします。

次の項目が復元されるか確認してください。

  • 回答済み問題
  • 正解・不正解
  • 全体の正答率
  • 分野別正答率
  • 不正解一覧

不正なJSONでも画面が止まらないか

開発者ツールから保存値を次のように変更します。

{broken-json

再読み込み後、空の履歴として開始できれば、try...catchが機能しています。

保存できない場合に案内されるか

保存処理が失敗したとき、開発者ツールだけでなく、画面にも案内が出るか確認します。

回答は採点しましたが、
学習進捗を保存できませんでした。

利用者が「保存できた」と誤解しない表現が必要です。

履歴初期化後に表示も戻るか

初期化ボタンを押した後、次の状態へ戻ることを確認します。

  • localStorageのキーが消える
  • 回答数が0問になる
  • 正答率が0%になる
  • 不正解一覧が空になる
  • 選択肢のチェックが外れる

複数タブで同期されるか

同じ学習サイトを2つのタブで開きます。

片方で回答した後、もう一方の正答率が更新されれば、storageイベントが動いています。

変更したタブ自身ではstorageイベントが発生しないため、回答直後の通常更新も忘れないでください。

localStorageで学習進捗を保存するときの注意点

localStorageは、個人用の小さな学習サイトには便利です。

しかし、利用者が増えたり、端末間同期が必要になったりすると、localStorageだけでは対応しにくくなります。

localStorageをデータベース代わりにしすぎない

少量の進捗データなら、localStorageで十分です。

一方、次の条件へ広がった場合は、別の保存方法を検討します。

  • 複数利用者が使う
  • ログイン機能がある
  • 複数端末で同期する
  • 大量の問題データを保存する
  • 履歴を長期間分析する
  • 管理者が進捗を確認する

localStorageはブラウザ単位の保存領域であり、利用者管理用のデータベースではありません。

学習履歴はバックアップできるようにする

ブラウザデータを削除すると、学習履歴も消える可能性があります。

398問を何週間もかけて進めた後に記録が消えると、かなり困ります。

JSONの書き出し機能を用意し、定期的にバックアップできるようにしておくと安心です。

ブラウザへ保存されるサイトデータは、通常は「ベストエフォート」として扱われます。保存に成功しても、端末の空き容量、ブラウザの管理方針、利用者によるデータ削除などによって、将来も必ず残り続けるとは限りません。

Storage APIには、保存領域を永続的に扱うようブラウザへ要求するnavigator.storage.persist()が用意されています。ただし、要求が必ず認められるわけではなく、許可するかどうかはブラウザが判断します。

したがって、消失すると困る学習記録はlocalStorageだけへ依存せず、JSONファイルへの書き出しや、サーバー側の保存と組み合わせてください。

参考:Storage API|MDN

参考:StorageManager.persist()|MDN

問題IDを途中で変更しない

問題IDは、問題データと履歴を結び付ける大切な値です。

問題10のIDを後から110へ変更すると、過去のhistory["10"]を対応させられなくなります。

問題を削除する場合も、別の問題へ同じIDを再利用しない方が安全です。

保存形式を変更するときは版を分ける

次のように、保存する項目を変えることがあります。

旧形式
isCorrectだけ保存

新形式
isCorrect、回答回数、連続正解数を保存

古いデータを新形式へ変換する処理を作るか、保存キーをv2へ変更して初期化するかを決めます。

既存利用者がいる公開サイトでは、何の案内もなく履歴を消さないようにしてください。

正答をlocalStorageで隠すことはできない

正答をlocalStorageへ保存しなくても、questions.jsonやJavaScriptに正答を含めてブラウザへ配信している場合、利用者は開発者ツールから確認できます。

個人学習用であれば問題になりにくいものの、有料試験サービスや厳密な試験環境には向いていません。

正答を利用者の端末へ渡したくない場合は、サーバー側で採点する構成が必要です。

localStorageで学習進捗を保存するメリット

localStorageで学習進捗を保存する方法は、個人用の学習サイトと相性がよい仕組みです。

サーバーを準備せずに使える

HTML、CSS、JavaScriptだけで進捗保存を追加できます。

データベースの契約や利用者登録が不要なため、まず自分だけで試したい場合に向いています。

ページを閉じても進捗が残る

通常のブラウザ利用では、ページを閉じてもデータが残ります。

昨日の続きから問題を解けるため、398問のような長い問題集でも進めやすくなります。

実装するコードが比較的少ない

基本は次の4つです。

  • setItem()で保存
  • getItem()で取得
  • JSON.stringify()で文字列化
  • JSON.parse()で復元

最初は問題IDと正誤だけを保存すれば、短いコードで動かせます。

正答率と不正解問題をすぐ集計できる

保存した履歴と問題データを組み合わせれば、全体や分野別の正答率を計算できます。

不正解だけを抽出できるため、限られた学習時間を弱点へ集中できます。

localStorageで学習進捗を保存するデメリット

便利な一方で、localStorageには制限があります。

別の端末へ自動で共有できない

パソコンで保存した進捗は、スマートフォンへ自動では移りません。

複数端末で使う場合は、JSONによる移行か、サーバー側の保存が必要です。

ブラウザデータを削除すると消える

利用者が閲覧データやサイトデータを削除すると、履歴が失われる場合があります。

大切な学習記録は、localStorageだけに任せず書き出せるようにしてください。

保存できる容量に上限がある

小さな進捗データには十分ですが、問題文、画像、長い解説を大量に入れる用途には向きません。

データ量が増えた場合は、IndexedDBやサーバー側の保存を検討します。

秘密情報を安全に保管する場所ではない

localStorageはJavaScriptから読み取れます。

パスワード、トークン、AWSアクセスキーなどは保存しないでください。

複数利用者の管理には向かない

ブラウザ内の1つの保存領域なので、利用者ごとの進捗管理には向きません。

ログイン機能を作る場合は、localStorageだけで認証や権限管理を行わないようにします。

localStorageでの進捗保存についてよくある質問

localStorageのデータはいつまで残りますか

localStorageには、通常の利用で自動的に決まる有効期限がありません。

ただし、利用者によるブラウザデータの削除、ブラウザの設定、保存領域の制限などにより消える場合があります。

絶対に失ってはいけない記録は、別の場所にも保存してください。

ブラウザを閉じても学習履歴は残りますか

通常のブラウザ利用では残ります。

sessionStorageはページセッション終了時に消えますが、localStorageはブラウザを閉じて再度開いた後も利用できます。

プライベートブラウズでは、最後の専用タブを閉じた後に削除されるのが一般的です。

パソコンとスマートフォンで共有できますか

localStorageだけでは共有できません。

端末ごと、ブラウザごとに保存領域が分かれます。

JSONの書き出しと読み込みを使うか、サーバーとデータベースを用意します。

localStorageにはどのくらい保存できますか

MDNでは、Web Storageの上限をオリジンごとに合計10MiBとし、その内訳をlocalStorageが5MiB、sessionStorageが5MiBと説明しています。

ただし、プライベートブラウズ、利用者の設定、組織のブラウザーポリシーなどによって、上限へ達していなくても保存できない場合があります。したがって、setItem()の例外処理は必要です。

localStorageの中身は利用者から見えますか

見えます。

ブラウザの開発者ツールを使えば、キーと値を確認・変更できます。

そのため、正答を隠す仕組みや秘密情報の保存場所としては使えません。

シークレットモードでも保存できますか

ブラウザによって扱いが異なる場合があります。

保存できたとしても、プライベートブラウズのセッション終了時に削除されるのが一般的です。

進捗を長く残したい場合は通常モードを利用してください。

保存した回答履歴を削除できますか

removeItem()で学習履歴のキーだけを削除できます。

localStorage.removeItem(
  "aws-study-history-v1"
);

同じオリジン内のすべてのlocalStorageを消すclear()は、ほかの設定も削除するため注意が必要です。

sessionStorageとの違いは何ですか

localStorageはブラウザを閉じても通常は残ります。

sessionStorageはタブ単位で管理され、ページセッションが終了すると消えます。

翌日も学習を続ける用途では、localStorageの方が適しています。

Cookieを使う必要はありますか

今回のようにブラウザ内だけで学習進捗を保存する場合、Cookieは必須ではありません。

CookieはHTTP通信でサーバーへ送られる場合があり、認証など別の用途で使われます。

GitHub PagesでもlocalStorageは使えますか

使えます。

GitHub Pagesで公開した静的なHTMLとJavaScriptからも、localStorageを利用できます。

ただし、公開URLのオリジン単位で保存されるため、別のドメインへ移転した場合は自動で引き継がれません。

localStorageで学習進捗を保存する方法まとめ

localStorageを使えば、サーバーやデータベースを用意せず、ブラウザへ学習進捗を保存できます。

今回のポイントをまとめます。

  • localStorageは同じオリジン単位で利用する
  • ブラウザを閉じても通常はデータが残る
  • 保存できるキーと値は文字列
  • オブジェクトはJSON.stringify()で保存する
  • 読み込み時はJSON.parse()で元へ戻す
  • 問題文ではなく問題IDと回答結果を保存する
  • 保存と読み込みはtry...catchで保護する
  • 未回答、正解、不正解を分けて集計する
  • 分野別正答率を計算できる
  • 不正解問題だけを復習できる
  • removeItem()で学習履歴だけを削除する
  • JSONの書き出しでバックアップできる
  • 複数タブはstorageイベントで同期できる
  • 保存データは必ず残り続けるとは限らない
  • localStorageは同期処理である
  • 大量の構造化データにはIndexedDBを検討する
  • パスワードやAWSアクセスキーを保存しない
  • AWSへ接続する場合は一時的な認証情報を検討する
  • 複数端末で共有する場合は別の仕組みが必要

最初から回答回数、連続正解数、復習予定日まで作り込む必要はありません。

まずは問題IDと正解・不正解だけをlocalStorageへ保存し、ページを再読み込みしても結果が残るところまで作ってみましょう。

基本の保存と復元が動いてから、正答率、不正解抽出、書き出し機能を追加する方が、エラーの原因を切り分けやすくなります。

参考にした公式情報

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次