MENU

AWS資格の学習サイトをHTMLで自作した方法|問題管理・正答率・絞り込み機能を実装

AWS資格の勉強を続けていると、「間違えた問題だけを解き直したい」「分野ごとの正答率を確認したい」と感じることがあります。

紙の問題集や一覧形式のデータでも学習はできますが、問題数が増えるほど管理が難しくなります。私がSOA-C03の問題398問を整理したときも、番号順に解くだけでは、監視、自動化、セキュリティなどのうち、どこが苦手なのかを把握しにくい状態でした。

そこで、HTML・CSS・JavaScriptを使い、AWS資格用の学習サイトを自作しました。問題を分野別に表示し、回答結果をブラウザへ保存して、不正解の問題だけを復習できる構成です。

この記事では、AWS資格の学習サイトを自作する方法を、初心者にも分かるように順番に解説します。利用者情報を処理するサーバーやデータベースを使わない小さな構成から始めるため、Webサイト作りに慣れていない方でも取り組めます。

▼作成した学習サイト

この記事のコードについて
掲載している問題文や選択肢は、仕組みを説明するために作成した架空の例です。実際のAWS認定試験問題や、第三者が作成した問題集の文章は使用していません。
また、掲載コードは学習サイトの仕組みを再現するための最小例です。実際に作成したサイトの全コードを、そのまま掲載したものではありません。

SOA-C03の出題範囲を確認したい方は、先に以下の記事をご覧ください。

SOA-C03の出題範囲と勉強方法|5分野の配点と対策を解説

398問を5分野へ分類した結果は、次の記事にまとめています。

SOA-C03の398問を出題分野別に分類|問題集の偏りと勉強への活かし方

目次

AWS資格用の学習サイトを自作した理由

AWS資格の学習サイトを自作した一番の理由は、問題を解くことよりも、弱点を探すことに時間がかかっていたためです。

問題集を最初から解き直せば復習はできます。しかし、398問すべてを毎回やり直す方法では、すでに理解している問題にも時間を使います。

自分に必要な機能だけを備えた学習サイトがあれば、不正解問題や苦手分野へ学習時間を集中できます。

既存の問題集では弱点を把握しにくかった

問題集を番号順に解くだけでは、分野ごとの得意・不得意が見えにくくなります。

たとえば、100問中75問に正解した場合、総合正答率は75%です。一見すると順調に見えますが、内訳が次のようになっているかもしれません。

分野正答率
監視・ログ82%
信頼性74%
自動化48%
セキュリティ80%
ネットワーク78%

この例では、自動化だけが大きな弱点です。

総合正答率だけを見ていると、「あと少し復習すれば大丈夫」と考えてしまいます。しかし、分野3のCloudFormationやSystems Managerを補わなければ、知識の偏りは残ったままです。

AWS資格の学習サイトを自作すると、回答結果を分野別に集計できます。何となく苦手だと思っていた分野を、数字で確認できるようになりました。

SOA-C03の398問を分野別に管理したかった

SOA-C03の398問を公式試験ガイドの5分野へ分類したことが、学習サイトを作る直接のきっかけでした。

分類データには、次のような項目を持たせます。

  • 問題番号
  • 主な出題分野
  • 関連するAWSサービス
  • 単一選択か複数選択か
  • 回答結果
  • 復習が必要か

表計算ソフトでも一覧管理はできます。ただし、問題を表示して回答し、採点して履歴を残す操作には向いていません。

「分野3だけを表示する」「前回不正解だった問題へ進む」といった操作を簡単にするため、HTMLでAWS資格の学習サイトを作ることにしました。

自分に必要な機能だけを使いたかった

自作の良さは、必要な機能だけを追加できることです。

今回のAWS資格学習サイトでは、次の機能を実装できる構成にしました。

  • 問題を1問ずつ表示する
  • 単一選択と複数選択に対応する
  • 出題分野で絞り込む
  • AWSサービス名で検索する
  • 回答結果をブラウザへ保存する
  • 分野別の正答率を表示する
  • 不正解問題だけを復習する
  • 問題番号を指定して移動する

最初からすべてを作ったわけではありません。

まずは「1問表示して採点する」ところまで作り、正常に動いた後で絞り込みや保存機能を加えました。この順番にしたことで、エラーが出ても原因を探しやすくなります。

作成したAWS資格学習サイトの完成イメージ

完成した画面では、上部に学習状況と絞り込み条件を表示し、その下へ問題文と選択肢を配置します。

大まかな構成は次のとおりです。

学習状況
├─ 回答数
├─ 正解数
├─ 正答率
└─ 分野別正答率

絞り込み
├─ すべて
├─ 分野1:監視・ログ
├─ 分野2:信頼性
├─ 分野3:自動化
├─ 分野4:セキュリティ
├─ 分野5:ネットワーク
└─ 不正解のみ

問題画面
├─ 問題番号
├─ 出題分野
├─ 関連サービス
├─ 問題文
├─ 選択肢
├─ 回答ボタン
├─ 正誤結果
├─ 解説
└─ 前へ・次へ

画面を豪華にすることより、復習したい問題へすぐ移動できることを優先しました。

問題を分野別に表示できる

分野を選ぶと、指定した分野の問題だけを表示します。

たとえば「分野3:自動化」を選んだ場合、CloudFormation、Systems Manager、EventBridgeなどに関係する問題へ絞り込めます。

398問の中から該当問題を手作業で探す必要がなくなり、短い学習時間でも苦手分野へ取り組みやすくなりました。

回答後に正誤と解説を確認できる

問題へ回答すると、正解または不正解を表示します。

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

  1. 問題と選択肢を表示する
  2. 回答を選ぶ
  3. 「回答する」を押す
  4. 選択内容を正答と比較する
  5. 正解・不正解を表示する
  6. 解説や確認先を表示する
  7. 回答結果を保存する

未回答のままボタンを押した場合は、採点せずに「回答を選択してください」と案内します。

複数選択問題では、必要な回答数も確認します。2つ選ぶ問題で1つしか選ばれていない場合、そのまま不正解にせず、もう1つ選ぶよう促す形です。

分野別の正答率を確認できる

回答履歴から、分野ごとの回答数と正解数を集計します。

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

分野回答数正解数正答率
監視・ログ604981.7%
信頼性402972.5%
自動化301550.0%
セキュリティ453680.0%
ネットワーク503876.0%

この表の数字は、画面説明用の架空例です。

正答率を見ると、自動化を優先して復習すべきだと分かります。「次に何を勉強するか」で迷う時間を減らせる点が、AWS資格の学習サイトを自作した大きな利点でした。

不正解の問題だけを復習できる

「不正解のみ」を選ぶと、直近の回答で間違えた問題だけを表示します。

398問を最初から解き直す代わりに、間違えた40問へ絞れば、限られた時間を弱点へ使えます。

ただし、一度正解しただけで完全に理解したとは限りません。今回の構成では直近の回答結果を使いますが、後から「2回連続で正解したら復習対象から外す」といった条件へ変更することも可能です。

AWS資格学習サイトの全体構成

今回のAWS資格学習サイトは、HTML・CSS・JavaScript・JSONの4種類のファイルで構成します。

回答履歴は、ブラウザが持つlocalStorageへ保存します。

HTML・CSS・JavaScriptで作成した

それぞれの役割は次のとおりです。

技術役割
HTML問題文、選択肢、ボタンなどの画面部品を作る
CSS文字サイズ、余白、配置などの見た目を整える
JavaScript問題表示、採点、絞り込み、集計を行う
JSON問題データを一定の形式で保存する
localStorage回答履歴をブラウザ内へ保存する

HTMLは画面の骨組み、CSSは見た目、JavaScriptは動作を担当します。

JSONには問題文や選択肢などのデータだけを入れます。画面と問題データを分けることで、問題を追加するときにHTMLを毎回書き換える必要がなくなります。

参考:HTMLの概要|MDN

データベースを使わない構成から始めた

今回の構成には、利用者情報を管理する処理やデータベースがありません。

必要なのは、静的なHTML、CSS、JavaScript、JSONだけです。手元のパソコンで作成でき、データベースの準備も不要となります。

ただし、JSONをfetch()で読み込む場合、HTMLファイルをダブルクリックして直接開くと、ブラウザの安全上の制限によって読み込みに失敗する場合があります。

そのため、動作確認時は簡易Webサーバーを使用します。Pythonが導入済みなら、対象フォルダで次のコマンドを実行できます。

python -m http.server 8000

その後、ブラウザで次のアドレスを開きます。

http://localhost:8000/

Pythonのhttp.serverは、手元での動作確認に使える簡易的な仕組みです。一方、Python公式ドキュメントでは、本番環境での利用は推奨されていません。

インターネットへ公開する用途では、そのまま使用しないでください。

参考:http.server|Python公式ドキュメント

問題データと画面を分けて管理した

ファイル構成は次のようにしました。

aws-study-site/
├─ index.html
├─ style.css
├─ script.js
└─ questions.json

index.htmlへ問題を直接書く方法でも動きます。しかし、398問をHTMLへ埋め込むとファイルが長くなり、修正箇所を探しにくくなるでしょう。

問題データをquestions.jsonへ分ければ、画面の修正と問題の修正を別々に進められます。

AWS資格学習サイトを作る前に決めたこと

AWS資格の学習サイトを自作するときは、最初に必要な機能とデータ形式を決めておくと作業が進みやすくなります。

思いついた機能をすべて入れようとすると、完成する前に作業量が膨らみます。まずは「最低限、何ができれば学習に使えるか」を決めました。

最初に必要な機能を絞った

最初の版で作ったのは、次の4機能です。

  • 問題を1問表示する
  • 選択肢を選ぶ
  • 正誤を判定する
  • 次の問題へ進む

この段階では、正答率も検索もありません。

3問程度の架空データで一連の流れが動くことを確認してから、以下を追加しました。

  • 分野別表示
  • 不正解のみ表示
  • 回答履歴
  • 分野別正答率
  • 問題番号検索
  • AWSサービス名検索

「まず小さく動かす」という進め方は地味ですが、かなり大切です。398問を先に登録してから不具合が見つかると、データと処理のどちらに原因があるのか分かりにくくなります。

問題データに必要な項目を決めた

問題データは、次のような形式にしました。

[
  {
    "id": 1,
    "domain": 1,
    "services": ["Amazon CloudWatch"],
    "type": "single",
    "requiredAnswers": 1,
    "question": "架空のEC2を監視するため、CPU使用率を確認したい。最も直接的なサービスはどれですか。",
    "choices": [
      "Amazon CloudWatch",
      "Amazon Route 53",
      "AWS Artifact",
      "AWS Organizations"
    ],
    "answer": [0],
    "explanation": "CPU使用率などのメトリクスはAmazon CloudWatchで確認できます。"
  }
]

answerを配列にしている理由は、単一選択と複数選択を同じ形式で扱うためです。

単一選択なら[0]、2つが正解なら[0, 2]のように保存します。配列の番号は0から始まるため、最初の選択肢は0です。

単一選択と複数選択を分けた

AWS認定試験では、1つを選ぶ形式と、複数を選ぶ形式があります。

データではtyperequiredAnswersを使い分けます。

{
  "type": "multiple",
  "requiredAnswers": 2
}

typesingleならラジオボタン、multipleならチェックボックスを表示します。

複数選択問題では、選択数が足りないときや多すぎるときに案内を出すようにしました。利用者が問題の指定を見落としても、誤った状態で採点されるのを防げます。

公開用と個人用を分けて考えた

AWS資格用の学習サイトでは、公開できる内容と、個人利用にとどめる内容を分ける必要があります。

今回の記事で公開するのは、次の内容です。

  • 画面構成
  • HTML・CSS・JavaScriptの例
  • 架空の問題データ
  • 回答履歴の保存方法
  • 集計方法

一方、第三者が作成した問題文や、実際の認定試験内容に由来する可能性があるデータは掲載しません。

学習サイトの仕組みを公開することと、問題集の内容を再配布することは別です。ここは曖昧にせず、最初からデータを分けて管理します。

HTMLで問題画面を作成する

最初に、問題を表示するためのHTMLを用意します。

以下は最小構成の例です。

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>AWS資格 学習サイト</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="container">
    <h1>AWS資格 学習サイト</h1>

    <section class="filters" aria-label="問題の絞り込み">
      <label for="domain-filter">出題分野</label>
      <select id="domain-filter">
        <option value="all">すべて</option>
        <option value="1">分野1:監視・ログ</option>
        <option value="2">分野2:信頼性</option>
        <option value="3">分野3:自動化</option>
        <option value="4">分野4:セキュリティ</option>
        <option value="5">分野5:ネットワーク</option>
      </select>

      <label>
        <input type="checkbox" id="incorrect-only">
        不正解のみ
      </label>
    </section>

    <section id="stats" aria-live="polite"></section>

    <article class="question-card">
      <p id="question-meta"></p>
      <h2 id="question-text"></h2>
      <form id="choices"></form>

      <div class="actions">
        <button type="button" id="answer-button">回答する</button>
        <button type="button" id="previous-button">前の問題</button>
        <button type="button" id="next-button">次の問題</button>
      </div>

      <p id="result" aria-live="polite"></p>
      <div id="explanation" hidden></div>
    </article>
  </main>

  <script src="script.js"></script>
</body>
</html>

問題文と選択肢の表示欄を作る

問題文はquestion-text、選択肢はchoicesへJavaScriptで追加します。

問題番号や出題分野はquestion-metaへ表示します。

表示先へ識別用のidを付けることで、JavaScriptから特定の部品を取得できます。

単一選択はラジオボタンを使う

単一選択では、同じnameを持つラジオボタンを使います。

<label>
  <input type="radio" name="answer" value="0">
  選択肢A
</label>

同じ名前のラジオボタンは、通常1つだけ選択できます。

複数選択はチェックボックスを使う

複数選択ではチェックボックスを使用します。

<label>
  <input type="checkbox" name="answer" value="0">
  選択肢A
</label>

チェックボックスは複数選べるため、必要な回答数をJavaScript側で確認します。

回答結果と解説の表示欄を用意する

結果はresultへ表示し、解説はexplanationへ入れます。

解説欄にはhidden属性を付け、回答するまでは非表示にしました。

正解を先に見えてしまうと問題演習にならないため、回答後にだけ表示します。

CSSで学習しやすい画面へ整える

次に、文字や余白を整えます。

以下は簡単な例です。

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  line-height: 1.7;
  background: #f5f5f5;
}

.container {
  max-width: 900px;
  margin: 0 auto;
  padding: 24px;
}

.filters,
.question-card,
#stats {
  margin-bottom: 20px;
  padding: 20px;
  background: #ffffff;
  border: 1px solid #dddddd;
  border-radius: 8px;
}

.choice {
  display: block;
  margin: 12px 0;
  padding: 14px;
  border: 1px solid #cccccc;
  border-radius: 6px;
  cursor: pointer;
}

.choice:focus-within {
  outline: 3px solid currentColor;
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 20px;
}

button,
select,
input {
  min-height: 44px;
  font: inherit;
}

button,
select {
  padding: 10px 16px;
}

.result-correct::before {
  content: "○ ";
  font-weight: bold;
}

.result-incorrect::before {
  content: "× ";
  font-weight: bold;
}

@media (max-width: 600px) {
  .container {
    padding: 12px;
  }

  .actions button {
    width: 100%;
  }
}

問題文を読みやすい幅にする

文章の横幅が広すぎると、長い問題文を目で追いにくくなります。

max-width: 900pxを設定し、画面中央へ配置しました。

900pxは絶対的な正解ではありません。実際の問題文の長さや文字サイズを見ながら調整してください。

選択肢を押しやすくする

入力欄だけでなく、label全体へ余白を付けます。

スマートフォンでは小さな丸や四角を正確に押すのが難しいため、選択肢の行全体を押せる方が操作しやすくなります。

W3Cが公開しているWCAG 2.2の達成基準2.5.8では、一部の例外を除き、操作対象を少なくとも24×24 CSSピクセルにするか、周囲に十分な間隔を設ける基準が示されています。

今回のCSSでは、ボタンや入力欄にmin-height: 44pxを指定しました。44pxがWCAGで一律に義務付けられているわけではありませんが、スマートフォンでも押しやすくするため、最低基準より余裕を持たせた設計です。

正解と不正解を色だけで表現しない

正解を緑、不正解を赤で表示するだけでは、色の違いが分かりにくい利用者へ伝わりません。

そこで、色に加えて「○ 正解」「× 不正解」と文字でも表示します。

WCAG 2.2の達成基準1.4.1でも、情報や操作を色だけで伝えないことが求められています。

見た目を整えることだけでなく、誰が使っても意味を理解できることが重要です。

参考:Web Content Accessibility Guidelines(WCAG)2.2

スマートフォン表示へ対応する

画面幅が600px以下の場合、操作ボタンを縦に並べます。

通勤中や休憩時間にスマートフォンで使うなら、指で押しやすい大きさが必要です。

パソコンで完成したと思っても、実際にスマートフォンで開くと、文字やボタンが小さい場合があります。公開前には両方で確認しましょう。

JavaScriptで問題を表示する

JavaScriptでは、問題データの読み込み、画面表示、採点、履歴保存を行います。

最初に必要な画面部品を取得します。

const questionMeta = document.querySelector("#question-meta");
const questionText = document.querySelector("#question-text");
const choicesForm = document.querySelector("#choices");
const resultElement = document.querySelector("#result");
const explanationElement = document.querySelector("#explanation");
const answerButton = document.querySelector("#answer-button");
const previousButton = document.querySelector("#previous-button");
const nextButton = document.querySelector("#next-button");
const domainFilter = document.querySelector("#domain-filter");
const incorrectOnly = document.querySelector("#incorrect-only");
const statsElement = document.querySelector("#stats");

let allQuestions = [];
let visibleQuestions = [];
let currentIndex = 0;
let history = loadHistory();

問題データを読み込む

questions.jsonfetch()で読み込みます。

async function initialize() {
  try {
    const response = await fetch("./questions.json");

    if (!response.ok) {
      throw new Error(`問題データを取得できませんでした: ${response.status}`);
    }

    const data = await response.json();

    if (!Array.isArray(data)) {
      throw new Error("問題データの形式が正しくありません。");
    }

    allQuestions = data;
    visibleQuestions = [...allQuestions];

    renderQuestion();
    renderStats();
  } catch (error) {
    questionText.textContent = "問題データの読み込みに失敗しました。";
    explanationElement.hidden = false;
    explanationElement.textContent = error.message;
    console.error(error);
  }
}

initialize();

fetch()は、サーバーから404などの応答が返っても、それだけでは処理が自動的に失敗扱いになりません。

そのため、response.okを確認し、問題データを正常に取得できたか判断します。

参考:Window.fetch()|MDN

問題を画面へ表示する

現在の問題を画面へ反映します。

function renderQuestion() {
  const question = visibleQuestions[currentIndex];

  resultElement.textContent = "";
  resultElement.className = "";
  explanationElement.hidden = true;
  explanationElement.textContent = "";
  choicesForm.replaceChildren();

  if (!question) {
    questionMeta.textContent = "";
    questionText.textContent = "条件に一致する問題がありません。";
    answerButton.disabled = true;
    previousButton.disabled = true;
    nextButton.disabled = true;
    return;
  }

  answerButton.disabled = false;
  previousButton.disabled = currentIndex === 0;
  nextButton.disabled = currentIndex >= visibleQuestions.length - 1;

  questionMeta.textContent =
    `問題 ${currentIndex + 1} / ${visibleQuestions.length}・` +
    `分野${question.domain}・${question.services.join("、")}`;

  questionText.textContent = question.question;

  const inputType = question.type === "multiple"
    ? "checkbox"
    : "radio";

  question.choices.forEach((choice, index) => {
    const label = document.createElement("label");
    label.className = "choice";

    const input = document.createElement("input");
    input.type = inputType;
    input.name = "answer";
    input.value = String(index);

    label.append(input, document.createTextNode(` ${choice}`));
    choicesForm.append(label);
  });
}

問題文と選択肢にはtextContentcreateTextNode()を使っています。

文字列をHTMLとして扱う必要がない場合、innerHTMLへ直接入れない方が意図しないタグの解釈を防ぎやすくなります。

回答を取得して正誤判定する

選択された値を取得し、正答配列と比較します。

function getSelectedAnswers() {
  return [...choicesForm.querySelectorAll(
    'input[name="answer"]:checked'
  )]
    .map(input => Number(input.value))
    .sort((a, b) => a - b);
}

function arraysEqual(left, right) {
  if (left.length !== right.length) {
    return false;
  }

  return left.every(
    (value, index) => value === right[index]
  );
}

回答ボタンの処理は次のようにします。

answerButton.addEventListener("click", () => {
  const question = visibleQuestions[currentIndex];

  if (!question) {
    return;
  }

  const selectedAnswers = getSelectedAnswers();

  if (selectedAnswers.length === 0) {
    resultElement.textContent = "回答を選択してください。";
    return;
  }

  if (selectedAnswers.length !== question.requiredAnswers) {
    resultElement.textContent =
      `${question.requiredAnswers}個の回答を選択してください。`;
    return;
  }

  const correctAnswers = [...question.answer]
    .sort((a, b) => a - b);

  const isCorrect = arraysEqual(
    selectedAnswers,
    correctAnswers
  );

  resultElement.textContent = isCorrect
    ? "正解です。"
    : "不正解です。";

  resultElement.className = isCorrect
    ? "result-correct"
    : "result-incorrect";

  explanationElement.hidden = false;
  explanationElement.textContent = question.explanation;

  history[String(question.id)] = {
    isCorrect,
    selectedAnswers,
    answeredAt: new Date().toISOString()
  };

  saveHistory();
  renderStats();
});

複数選択では、選んだ順番が違っても正解として扱う必要があります。

そのため、利用者の回答と正答を並べ替えてから比較しています。

前後の問題へ移動する

previousButton.addEventListener("click", () => {
  if (currentIndex > 0) {
    currentIndex -= 1;
    renderQuestion();
  }
});

nextButton.addEventListener("click", () => {
  if (currentIndex < visibleQuestions.length - 1) {
    currentIndex += 1;
    renderQuestion();
  }
});

先頭では「前の問題」、最後では「次の問題」を無効にします。

5つの出題分野で問題を絞り込む

SOA-C03では、問題を5つの出題分野へ分けて管理しました。

問題データのdomainへ1から5の番号を設定し、選択された番号と一致する問題だけを残します。

分野を選んだら対象問題だけを残す

絞り込み処理は次のように作れます。

function applyFilters() {
  const selectedDomain = domainFilter.value;

  visibleQuestions = allQuestions.filter(question => {
    const matchesDomain =
      selectedDomain === "all" ||
      question.domain === Number(selectedDomain);

    const savedResult = history[String(question.id)];

    const matchesIncorrect =
      !incorrectOnly.checked ||
      savedResult?.isCorrect === false;

    return matchesDomain && matchesIncorrect;
  });

  currentIndex = 0;
  renderQuestion();
}

domainFilter.addEventListener("change", applyFilters);
incorrectOnly.addEventListener("change", applyFilters);

すべての分野へ戻せるようにする

allを選んだ場合は、分野条件を付けません。

絞り込み機能を作るときは、元へ戻る方法も用意してください。「分野3を選んだ後、全問題へ戻れない」という状態は、意外と起こりやすい失敗です。

絞り込み後は問題位置をリセットする

絞り込み前に100番目の問題を表示していても、絞り込み後の問題数が20問しかない場合があります。

以前のcurrentIndexを残すと、存在しない位置を参照してしまいます。

そこで、条件を変更するたびにcurrentIndex = 0として先頭へ戻します。

回答履歴をブラウザへ保存する

回答履歴にはlocalStorageを使いました。

localStorageは、同じオリジンのWebページがキーと値の組み合わせを保存できる仕組みです。通常はブラウザを閉じてもデータが残ります。

ただし、ブラウザの設定によって保存が拒否される場合があります。また、file:形式で直接開いたページにおける動作は、ブラウザごとの差があり、仕様上も一定ではありません。

そのため、AWS資格学習サイトはlocalhostまたはHTTPSで開く構成をおすすめします。

参考:Window.localStorage|MDN

localStorageを使って回答を残す

保存と読み込みを関数にします。

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

function loadHistory() {
  try {
    const saved = localStorage.getItem(STORAGE_KEY);
    return saved ? JSON.parse(saved) : {};
  } catch (error) {
    console.error("履歴を読み込めませんでした。", error);
    return {};
  }
}

function saveHistory() {
  try {
    localStorage.setItem(
      STORAGE_KEY,
      JSON.stringify(history)
    );
  } catch (error) {
    console.error("履歴を保存できませんでした。", error);
    resultElement.textContent =
      "回答結果を保存できませんでした。";
  }
}

localStorageへ保存されるキーと値は文字列です。

そのため、履歴のオブジェクトをJSON.stringify()で文字列へ変換し、読み込むときはJSON.parse()で元へ戻します。

問題番号ごとに結果を保存する

履歴は問題IDをキーにします。

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

日時はtoISOString()で保存しているため、協定世界時を基準とした文字列になります。

画面へ日本時間で表示する場合は、Intl.DateTimeFormatなどを使って変換します。

localStorageの制限も理解する

localStorageは簡単ですが、万能ではありません。

  • 別のブラウザへ履歴を共有できない
  • パソコンとスマートフォンで同期されない
  • ブラウザの保存データを削除すると履歴も消える
  • 秘密情報の保存には向かない
  • ブラウザの設定で使用できない場合がある
  • 保存できる容量には上限がある

個人の回答履歴を保存する程度なら扱いやすいものの、複数端末で使うならサーバー側の保存先や利用者認証が必要です。

分野別の正答率を計算する

正答率は、回答済み問題のうち、何問正解したかで計算します。

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

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

回答数と正解数を集計する

function calculateStats(questions) {
  const answered = questions.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
  };
}

回答数が0件の場合は0で割れないため、先に分岐します。

分野別の結果を表示する

function renderStats() {
  const overall = calculateStats(allQuestions);

  const rows = [1, 2, 3, 4, 5].map(domain => {
    const domainQuestions = allQuestions.filter(
      question => question.domain === domain
    );

    const stats = calculateStats(domainQuestions);

    return `
      <tr>
        <th scope="row">分野${domain}</th>
        <td>${stats.answered}</td>
        <td>${stats.correct}</td>
        <td>${stats.rate.toFixed(1)}%</td>
      </tr>
    `;
  }).join("");

  statsElement.innerHTML = `
    <p>
      全体:${overall.answered}問回答・
      ${overall.correct}問正解・
      正答率${overall.rate.toFixed(1)}%
    </p>
    <table>
      <thead>
        <tr>
          <th>分野</th>
          <th>回答数</th>
          <th>正解数</th>
          <th>正答率</th>
        </tr>
      </thead>
      <tbody>${rows}</tbody>
    </table>
  `;
}

この例では、自分で計算した数値だけをHTMLとして組み立てています。

外部から入力された文字列をinnerHTMLへ入れる場合は、意図しないHTMLやスクリプトが混ざらないよう、別の方法で要素を作る必要があります。

正答率だけで判断しない

一度正解した問題と、何度も安定して正解できる問題は同じではありません。

今回の最小構成では直近結果だけを保存していますが、学習を続けるなら次の項目も役立ちます。

  • 回答回数
  • 正解回数
  • 連続正解数
  • 最後に回答した日
  • 次の復習予定日

ただし、記録項目を増やすほど処理も複雑になります。最初は直近結果だけでも十分です。

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

不正解問題の抽出は、回答履歴のisCorrectを確認します。

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

正解後に不正解一覧から外すか決める

不正解管理には、いくつかの考え方があります。

  • 直近の回答が正解なら外す
  • 一度でも間違えた問題は残す
  • 2回連続で正解したら外す
  • 手動で「理解済み」を付ける

今回の例では、分かりやすさを優先して直近結果を使っています。

「偶然当たった問題も残したい」という場合は、回答回数や連続正解数を保存する方式へ変更してください。

復習対象が0件の場合も考える

不正解がない場合、画面が空白になると故障したように見えます。

「条件に一致する問題がありません」と表示し、全問題へ戻る方法も示しましょう。

利用者が次に何をすればよいか分かる画面にすることが大切です。

問題番号とAWSサービスで検索できるようにする

398問あると、特定の問題を探すだけでも時間がかかります。

問題番号検索とサービス名検索があると、公式資料を読んだ後に関連問題だけを解き直せます。

問題番号検索を追加する

HTMLへ入力欄を追加します。

<label for="question-id">問題番号</label>
<input type="number" id="question-id" min="1">
<button type="button" id="go-to-question">
  移動
</button>

JavaScriptでは、IDが一致する位置を探します。

goToQuestionButton.addEventListener("click", () => {
  const targetId = Number(questionIdInput.value);

  const index = visibleQuestions.findIndex(
    question => question.id === targetId
  );

  if (index === -1) {
    resultElement.textContent =
      "現在の条件では、その問題が見つかりません。";
    return;
  }

  currentIndex = index;
  renderQuestion();
});

分野で絞り込んでいる場合、その条件に含まれない問題は見つかりません。

「すべて表示へ戻してから移動する」仕様にする方法もありますが、利用者が意図しない条件変更にならないよう、今回は案内だけを出します。

AWSサービス名で絞り込む

問題データのservicesを検索します。

function filterByService(keyword) {
  const normalizedKeyword = keyword
    .trim()
    .toLowerCase();

  return allQuestions.filter(question =>
    question.services.some(service =>
      service.toLowerCase().includes(normalizedKeyword)
    )
  );
}

「CloudWatch」「IAM」「CloudFormation」といったサービス名で関連問題を探せます。

表記の揺れを減らす

次のように、同じサービスを異なる名前で登録すると検索結果が分かれます。

  • CloudFormation
  • AWS CloudFormation
  • CFN

データへ保存する名称は、できる限り統一しましょう。

略称も検索対象にしたい場合は、別名を持たせる方法があります。

{
  "services": ["AWS CloudFormation"],
  "serviceAliases": ["CloudFormation", "CFN"]
}

AWS資格学習サイトを作る際につまずいた点

HTMLでAWS資格の学習サイトを自作すると、画面よりもデータ周りでつまずくことがあります。

実際に起こりやすい問題をまとめます。

JSONの記号ミスで読み込めなかった

JSONでは、記号の位置が1か所違うだけでも読み込みに失敗します。

よくある原因は次のとおりです。

  • 最後に余分なカンマがある
  • ダブルクォートが閉じていない
  • 角括弧と波括弧の数が合わない
  • キーをダブルクォートで囲んでいない
  • 文字列の中に未処理の改行がある

問題を大量に追加する前に、数問だけで読み込みを確認してください。

問題IDと配列の位置を混同した

問題IDを1から始めても、JavaScriptの配列位置は0から始まります。

問題IDが10だからといって、必ずquestions[10]が問題10になるとは限りません。

問題IDから探す場合は、find()findIndex()を使います。

const question = allQuestions.find(
  item => item.id === 10
);

複数選択の正誤判定に失敗した

正解が[1, 3]で、利用者が[3, 1]の順に選んだ場合も、回答内容は同じです。

配列をそのまま文字列化すると、順番の違いで不正解になる場合があります。

比較前に並べ替えることで、この問題を防げます。

localStorageの古いデータが残った

問題データの形式を変更した後も、以前の回答履歴がブラウザへ残ります。

古い履歴が新しい処理と合わず、エラーになることがありました。

保存キーへ版番号を入れると、形式変更に対応しやすくなります。

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

履歴を初期化するボタンを用意する方法もあります。

function clearHistory() {
  localStorage.removeItem(STORAGE_KEY);
  history = {};
  applyFilters();
  renderStats();
}

ファイルを直接開くとJSONを取得できなかった

index.htmlをダブルクリックし、file:///から開いた場合、fetch()によるJSON取得が失敗することがあります。

また、file:形式でのlocalStorageの動作も一定ではありません。

手元で確認するときは、簡易Webサーバーを使ってhttp://localhostから開く方が安定します。

参考:CORS request not HTTP|MDN

学習サイトへ後から追加したい機能

最低限の機能が動いた後で、必要に応じて機能を追加します。

学習を助ける機能なのか、単に作ってみたい機能なのかを分けて考えることが大切です。

ランダム出題

問題の配列を無作為に並べ替えれば、毎回違う順番で出題できます。

ただし、元の配列を直接並べ替えると、通常の問題順へ戻しにくくなります。複製した配列を使う方が扱いやすいでしょう。

公式配点に近い模擬試験

記事2で分類したデータを使い、SOA-C03の公式配点に近い50問を抽出できます。

学習用の目安は次のとおりです。

分野問題数
分野111問
分野211問
分野311問
分野48問
分野59問
合計50問

これは、実際の試験で各分野から必ずこの数が出るという意味ではありません。

公式配点22%・22%・22%・16%・18%を、採点対象50問へ単純に当てはめた学習用の構成です。

制限時間の表示

SOA-C03の試験時間に合わせ、130分のタイマーを付ける方法もあります。

ただし、次の条件を決めなければなりません。

  • ページを閉じても時間を続けるか
  • 一時停止を認めるか
  • 再読み込み後に再開するか
  • 時間切れ後に自動採点するか

単純な画面表示だけなら簡単ですが、途中再開まで考えると保存項目が増えます。

復習間隔の自動調整

間違えた問題は早めに表示し、連続して正解した問題は間隔を空ける方法です。

最初から複雑な計算を入れなくても、次のような段階で十分使えます。

  • 不正解:翌日に復習
  • 1回正解:3日後に復習
  • 2回連続正解:7日後に復習
  • 3回連続正解:14日後に復習

問題ごとにnextReviewAtを保存すれば、復習日が来た問題だけを表示できます。

複数端末での履歴共有

データベースと利用者認証を追加すれば、パソコンとスマートフォンで履歴を共有できます。

一方で、次の検討が必要です。

  • 利用者情報の管理
  • パスワードや認証情報の保護
  • 通信の暗号化
  • データのバックアップ
  • 運用費用
  • 不正アクセス対策

個人学習だけが目的なら、localStorageのまま使う方が簡単です。

GitHub Pagesで公開する方法

HTML・CSS・JavaScriptだけで作った静的なサイトは、GitHub Pagesでも公開できます。

GitHub Pagesは、GitHubリポジトリ内のHTML、CSS、JavaScriptなどを公開できる静的サイト向けの機能です。

参考:GitHub Pagesとは|GitHub Docs

公開までの基本的な流れ

大まかな流れは次のとおりです。

  1. GitHubへ新しいリポジトリを作成する
  2. HTML・CSS・JavaScript・JSONを登録する
  3. リポジトリのSettingsを開く
  4. Pagesの公開元となるブランチとフォルダを選ぶ
  5. 発行されたURLを開く

GitHub Pagesの設定画面や利用できる範囲は、GitHubのプランやリポジトリの公開設定によって異なります。

公開前に、最新の公式手順を確認してください。

JSONと正答データも公開される

GitHub Pagesは静的ファイルを配信する仕組みです。

questions.jsonへ正答を含めた場合、そのファイルも閲覧者のブラウザへ送信されます。開発者ツールやファイルのURLから、正答データを確認できる点に注意してください。

自分専用の学習サイトとして使うだけなら、大きな問題にならない場合もあります。

しかし、有料の試験サービスや、正答を隠す必要があるサービスには、この構成は適していません。

学習サイトを公開するときの注意点

AWS資格の学習サイトを自作して公開する場合、技術面だけでなく、著作権やAWS認定の規約も確認する必要があります。

問題文をそのまま公開しない

問題集がインターネット上で閲覧できても、転載や翻訳が自由とは限りません。

GitHub公式ドキュメントでは、リポジトリにライセンスがない場合、通常の著作権が適用され、作成者が権利を保持すると説明されています。

公開リポジトリを閲覧したり、GitHub上でフォークしたりできることと、問題文を別サイトへ転載できることは同じではありません。

参考:リポジトリのライセンス|GitHub Docs

第三者の問題を使う場合は、次の点を確認してください。

  • LICENSEファイルがあるか
  • 問題文の再配布が認められているか
  • 翻訳や改変が認められているか
  • 出典表示が必要か
  • 商用利用が認められているか

許諾内容を確認できない場合は、自分で作成した架空問題や練習問題を使う方が安全です。

AWS認定試験の秘密保持規定を確認する

AWS Certification Program Agreementでは、認定試験の内容を開示または広める行為が禁止されています。

実際の試験で見た問題や回答を再現し、学習サイトへ掲載してはいけません。

また、実際の試験問題が不正に流出した、いわゆる問題漏えい教材へ意図せず接触する可能性にも注意が必要です。

参考:AWS Certification Program Agreement

学習サイトへ登録する問題は、公式試験ガイドやAWS公式ドキュメントをもとに、自分で新しく作成する方法が適しています。

コードから正答を確認できる

ブラウザだけで動くサイトでは、問題データや正答が利用者の端末へ送られます。

JavaScriptを難読化しても、正答を完全に隠せるわけではありません。

個人用の学習補助として割り切るか、正答を保護する必要がある場合はサーバー側で採点する構成を検討してください。

個人情報や秘密情報をlocalStorageへ保存しない

localStorageへ保存した内容は、そのサイトで動くJavaScriptから読み取れます。

OWASPのHTML5 Security Cheat Sheetでも、認証や権限を前提とする機密情報をlocalStorageへ保存しないよう推奨しています。ページにクロスサイトスクリプティングの弱点があると、localStorage内の情報を読み取られる可能性があるためです。

氏名、メールアドレス、パスワード、セッション識別子、アクセストークン、AWSアクセスキーなどを保存する場所として使わないでください。

今回の構成では、問題ID、正誤結果、選択した回答、回答日時など、個人を特定しない学習履歴だけを保存します。

参考:HTML5 Security Cheat Sheet|OWASP

HTMLでAWS資格学習サイトを自作するメリット

AWS資格の学習サイトを自作する最大のメリットは、自分の学習方法へ合わせられることです。

自分の苦手分野に合わせられる

分野3が苦手なら、自動化の問題だけを表示できます。

IAMの権限問題を復習したい日は、関連サービスがIAMの問題へ絞り込めます。

問題集を最初から順番に解くより、目的を持って復習しやすくなります。

回答結果を同じ形式で整理できる

利用許諾を確認できた教材や、自分で作成した問題を、同じデータ形式で管理できます。

ただし、複数の教材を取り込む場合も、問題文の利用条件は個別に確認してください。

HTMLやJavaScriptの勉強にもなる

問題表示、絞り込み、保存、集計を作る過程で、Webページの基本を学べます。

実際に動くものを作るため、単に文法を読むより理解しやすいと感じました。

AWS資格の勉強とWeb制作を同時に進められる点も、自作ならではの利点です。

必要な機能を少しずつ追加できる

最初は1問を表示するだけでも構いません。

動くものを作ってから、回答履歴、正答率、検索などを追加する方が完成しやすくなります。

HTMLでAWS資格学習サイトを自作するデメリット

自作には便利な面がある一方、作成や保守に時間がかかります。

作成と修正に時間がかかる

サイト作りに夢中になると、問題を解く時間よりコードを直す時間の方が長くなる場合があります。

AWS資格へ合格することが目的なら、必要な機能まで作った段階で、一度開発を止める判断も必要です。

問題データの更新が必要になる

AWSサービスや認定試験ガイドは更新されます。

過去には正しかった問題でも、現在のサービス仕様と合わなくなる場合があります。

問題文、正答、解説、公式資料へのリンクを定期的に確認しなければなりません。

localStorageだけでは端末間で共有できない

パソコンで回答した履歴を、スマートフォンへ自動で引き継ぐことはできません。

複数端末で共有するには、データベースや同期の仕組みが必要です。

公開時には権利と規約への配慮が必要

自分だけで使う場合と、不特定多数へ公開する場合では、注意すべき内容が異なります。

問題データを公開する前に、著作権、ライセンス、AWS認定規約を確認してください。

AWS資格学習サイトの自作についてよくある質問

HTML初心者でも作れますか

小さな構成から始めれば作れます。

最初は、架空の問題を1問表示し、回答ボタンを押したら正誤を出すところまで作りましょう。

分野別表示や回答履歴は、基本動作を確認した後で追加する方が進めやすくなります。

JavaScriptはどこまで必要ですか

問題を表示するだけなら、基本的な変数、配列、条件分岐、繰り返しが分かれば始められます。

絞り込みではfilter()、問題検索ではfind()findIndex()、画面表示ではquerySelector()textContentを使います。

すべてを暗記する必要はありません。必要な処理を一つずつ調べながら作れば十分です。

サーバーやデータベースは必要ですか

個人利用で、回答履歴を同じブラウザへ保存するだけなら、データベースは不要です。

JSONを読み込むための動作確認では、簡易Webサーバーを使います。

利用者登録、複数端末での同期、問題データの非公開化などが必要になった場合は、サーバー側の仕組みを検討してください。

スマートフォンでも使えますか

画面幅に応じて配置を変えるCSSを設定すれば利用できます。

ただし、パソコンで表示できるだけでは十分ではありません。文字の大きさ、選択肢の押しやすさ、表の横幅などを実機で確認しましょう。

回答履歴はどこへ保存されますか

今回の構成では、利用中のブラウザのlocalStorageへ保存されます。

別のブラウザや端末には共有されません。ブラウザのデータを削除した場合、回答履歴も消える可能性があります。

GitHub Pagesで公開できますか

HTML・CSS・JavaScript・JSONによる静的な構成であれば、GitHub Pagesで公開できます。

ただし、問題データや正答も公開ファイルとして配信されます。また、第三者の問題文を公開する場合は、権利やライセンスの確認が必要です。

問題文を公開してもよいですか

自分で作成した問題であれば、AWSの商標や試験規約などにも配慮しながら公開を検討できます。

第三者が作成した問題や、実際の試験内容に由来する問題は、そのまま公開しないでください。

インターネット上で閲覧できることは、再配布の許可を意味しません。

複数のAWS資格へ対応できますか

問題データへ資格コードを追加すれば対応できます。

{
  "exam": "SOA-C03",
  "domain": 1
}

SAA-C03やDVA-C02などを追加する場合は、試験ごとに分野構成が異なります。

分野名や公式配点を一つに固定せず、資格ごとに設定できる形にすると管理しやすくなります。

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

AWS資格の学習サイトを自作すると、問題を分野別に整理し、不正解問題を効率よく復習できます。

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

  • HTMLで問題画面を作成する
  • CSSでパソコンとスマートフォンから使いやすくする
  • JavaScriptで問題表示と採点を行う
  • JSONで問題データと画面を分ける
  • localStorageで回答履歴を保存する
  • SOA-C03の5分野で問題を絞り込む
  • 分野別正答率を表示する
  • 不正解問題だけを復習できるようにする
  • 最初は少数の架空問題で動作確認する
  • Pythonの簡易サーバーは手元の確認だけに使う
  • 公開する場合は問題文の権利とAWS認定規約を確認する
  • 色だけに頼らず、操作しやすい画面を作る
  • localStorageへパスワードやAWSアクセスキーを保存しない
  • 静的サイトでは正答データを完全には隠せない
  • 学習サイト作りそのものが目的にならないよう注意する

自作したAWS資格学習サイトは、苦手分野の把握や不正解問題の復習を効率化するための補助教材です。

AWS公式では、AWS Skill Builderを通じて、公式練習問題集やExam Prepコースなどの無料教材を提供しています。サブスクリプションでは、公式模擬試験、実践ラボ、追加の練習問題なども利用できます。

自作サイトだけに頼らず、最新のAWS公式試験ガイド、公式練習問題集、操作練習を組み合わせて学ぶことが大切です。

参考:AWS認定試験に向けた準備

最初から398問へ対応する必要はありません。

まずは自分で作成した3問ほどを使い、問題表示、回答、採点まで動かしてみましょう。小さく完成させてから機能を追加する方が、途中で止まりにくくなります。

参考にした公式情報

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

コメント

コメントする

CAPTCHA


目次