プログラミング

生成AIで爆速成長!タイピングゲームWebアプリ開発記 #1


皆さん、こんにちは。最近生成AIの進化が凄まじいですね。

雑なプロンプトでも、こちらの意図を汲み取って動くソースコードを生成してくれます。

その結果、自分の知識が浅い分野は生成AIの出力に対して「よくわからないけどコピペしたらできた⋯」と思うことが増えてきました。


そこで、ちゃんと生成AIの出力結果を評価できるようなエンジニアになるため、Webアプリを作ってプログラミングのスキルアップを目指します!

とはいえ、片っ端から書籍を読んでいると時間がかかるため、生成AIを活用して勉強します。



「生成AI使ってコピペしてたら、スキルアップできないのでは?」
「業務に関係のないアプリ作って意味あるの?」

と 思っていたのですが、実際に作ってみると学びが多いことに気づきました。

どんなモノを作るより、とりあえず手を動かすことが大事と痛感しました。

今回私が作ったものや学んだことについて共有したいと思います。


作ったWebアプリ「タイピングゲーム」

Webアプリの動作

今回作ったのはタイピングゲームで、画面に表示された文字を打ち込むゲームになります。

早口言葉を10個事前に用意し、それをランダムに表示しています。

ユーザーは画面に表示された早口言葉を打ち込み、「送信」ボタンを押します。

タイピング結果の画面でユーザーの入力と、タイピングにかかった時間が表示されます。


ディレクトリ構成

各画面のHTMLを用意し、マルチページの構成で作ってみました。

  • index.html:Top画面
  • typing.html:早口言葉を表示されてユーザーが入力する画面
  • result.html:ユーザーが入力した文字とかかった秒数を表示する画面

知識がなかったので「1画面=1HTML」にしましたが、必ずしもマルチページの構成にする必要はありません。

むしろマルチページだとページ遷移ごとに状態がリセットされて、遷移先にデータを渡すのが大変でした…

(こういうのに気づけるのも実際に手を動かしたからと思ってます…!)

C:.
│  index.html
│  result.html
│  typing.html
│  package-lock.json
│  package.json
│  
├─node_modules
│
└─src
        main.js


ソースコード

各画面を構成するHTMLが3つと、JavaScriptの計4つになります。

main.js

export function getQueryParams() {
  return Object.fromEntries(new URLSearchParams(location.search));
}

if (location.pathname.endsWith('typing.html')) {
  const wordList = [
    '生麦生米生卵',
    '青巻紙、赤巻紙、黄巻紙',
    '隣の客はよく柿食う客だ',
    'すももも、桃も、桃のうち',
    '庭には2羽ニワトリがいる',
    '新人歌手、新春シャンソンショー',
    '東京特許許可局',
    '坊主が屏風に上手に坊主の絵を描いた',
    'この釘は、引き抜きにくい釘だ',
    '貨客船の旅客と旅客機の旅客',
  ];

  const typingWordEl = document.getElementById('typing-word');
  const userInputEl = document.getElementById('user-input');
  const elapsedTimeEl = document.getElementById('elapsed-time');
  const submitBtn = document.getElementById('submit-button');

  /* ランダム単語を決定&表示 */
  const typingWord = wordList[Math.floor(Math.random() * wordList.length)];
  typingWordEl.textContent = typingWord;

  /* タイマー開始 */
  const startTime = Date.now();
  let timerId = setInterval(() => {
    const sec = Math.floor((Date.now() - startTime) / 1000);
    elapsedTimeEl.textContent = `経過時間:${sec}秒`;
  }, 500);

  /* 終了ボタン押下 */
  submitBtn.addEventListener('click', () => {
    // タイマー停止
    clearInterval(timerId);
    const elapsedSec = Math.floor((Date.now() - startTime) / 1000);

    const params = new URLSearchParams({
      correct: typingWord, // 正解
      typed: userInputEl.value.trim(), // ユーザー入力
      time: elapsedSec, // 経過時間
    });

    location.href = `result.html?${params.toString()}`;
  });

  /* Enter キーで送信したい場合 */
  userInputEl.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') submitBtn.click();
  });
}

if (location.pathname.endsWith('result.html')) {
  const { correct = '', typed = '', time = 0 } = getQueryParams();

  // 画面に反映(テキストとして扱う)
  document.getElementById('correct-word').textContent = String(correct);
  document.getElementById('typed-word').textContent = typed
    ? String(typed)
    : '(入力なし)';
  document.getElementById('elapsed-sec').textContent = String(time);
}


index.html

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>タイピングゲーム</title>
</head>

<body>
  <h1>タイピングゲーム</h1>
  <p>画面に映った文字を入力してね!</p>
  <button onclick="location.href='./typing.html'">START</button>
</body>

</html>


typing.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Typing Game</title>
    <script type="module" src="./src/main.js"></script>
</head>

<body>
    <h1>タイピングゲーム</h1>
    <!-- タイピングする文字 -->
    <p id="typing-word"></p>

    <!-- ユーザの入力欄 -->
    <!--  autocompleteをOFFにするとブラウザで過去の入力が表示されなくなる -->
    <input id="user-input" type="text" autocomplete="off" />
    <button id="submit-button">送信</button>
    <p id="elapsed-time">経過時間:0秒</p>

</body>

</html>


result.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>result</title>
    <script type="module" src="./src/main.js"></script>
</head>

<body>
    <h1>タイピング結果</h1>
    <ul>
        <li>正解文字列:<span id="correct-word"></span></li>
        <li>あなたの入力:<span id="typed-word"></span></li>
        <li>経過時間:<span id="elapsed-sec"></span> 秒</li>
    </ul>
    <button onclick="location.href='./index.html'">もう1回!</button>
</body>

</html>


Webアプリの開発プロセス

どのようにWebアプリを作っていったのか、ざっくりとした流れを書いていきます。

特にこれが正解というわけではありません。

何か参考になれば幸いです。

設計:画面設計と動作イメージを考える


作成するWebアプリの設計をします。

簡単なアプリ、かつ、あまり設計に時間をかけないことを意識しました。

時間をかけると機能をたくさん入れて、複雑なアプリを作ろうとしてしまいます。

ある程度、頭の中で実装イメージが湧いていないと、生成AIからの回答を全てコピペして、理解してないけど動くアプリが出来上がってしまうと思います。

最初に実装するのは最低限の機能にして、後で追加していく形で進めていきます。


タイピングゲームは「文字が画面に表示されて、それをユーザーが入力する」が最低限の機能です。

そこから画面の動作をイメージしながら必要な処理を洗い出します。

  • どんな画面が必要? → 文字を表示して、ユーザーが入力する画面
  • 文字はどのタイミングで表示する? → Startボタンを押したら画面に表示
  • どんな文字を表示する? → 10種類ぐらいの文字列をランダムに表示
  • etc


何を表示するのか、ボタンは何個必要か?といった画面を構成する要素を最初にイメージしておくといいと思います。

そうすると、生成AIにも「Startボタンを押したら文字を表示するソースコードを書いてください」と具体的な質問がしやすくなります。


準備:Viteで開発環境をセットアップ

Vite」という便利なフロントエンドの開発サーバー兼ビルドツールがあります。

これを使うと、フロントエンドの開発環境を簡単にセットアップできます

npm create vite@latest でプロジェクトを作成し、npm install で依存関係をインストール、npm run dev で開発サーバーを起動すれば準備は完了です。

日本語の公式ドキュメントにもセットアップ方法が解説されています。

Vite公式ドキュメント「最初のViteプロジェクトを作成する」


Viteでは、ReactやVue.jsなど複数のフレームワークに対応しています。

タイピングゲームでは特にフレームワークを使わないため「Vanilla」を選択し、言語は「JavaScript」でセットアップしています。


実装:手を動かしてから生成AIに聞く

そのまま設計した内容を生成AIに投げると、おそらくアプリは完成するでしょう。

ただ、今回の目的はプログラミングの理解なので、まずは手を動かしました

とはいえ、何から手をつけたらいいかわからないので、最初にタスクを細分化しました。

  • HTMLで画面を作る(タイトルや本文、ボタン、入力欄など)
  • ボタンを押して画面を遷移させる
  • 10種類の文字を画面にランダムに表示する
  • 経過時間をカウントする
  • 経過時間を止めて画面に表示する
  • etc.

小さな機能単位で実装することを意識し、自分でコードを書いてみて、わからなければ生成AIに聞くという形で進めました。

また、ファクトチェックも兼ねて生成AIの出力結果で出てきた単語やコードはGoogleで検索して、Qiitaや他のサイトで理解を深めました。

(生成AIはインターネット上のデータを学習しているのでファクトチェックになってないと思いますが、書籍で調べるのは大変なので…あくまで一般的な実装かを確認するイメージでやってました。)

そもそもどうやって実装するのかイメージが湧かない機能はGoogleで検索してみて、その後生成AIに確認しました。

生成AIに聞きすぎるのも良くないですが、自力で実装するのにこだわりすぎるのも時間がかかってしまいます…

「10分は自分で考える」というように時間で区切るといいと思います。

特にエラーの時は自分で頑張らずに生成AIに聞いて解決しましょう!

「実装するのが楽しくない!つらい!」というのが続くと挫折してしまうので、自分で考えることを意識しながら生成AIをうまく活用するのがポイントになると思います。

今回学んだこと

マルチページのデータの受け渡し方法

フロントエンドの知識が浅かったこともあり、1画面 1HTMLで作成しました。

そこで困ったのがデータの受け渡しです。

マルチページの構成だと、ページ遷移すると状態がリセットされます。

(実際に作ってから、もしかして遷移したら情報が消える?と気づきました…)

遷移した結果画面で「画面に表示された早口言葉」と「ユーザーが入力した情報」を表示できるように、どこかに保持しておく必要があります。

今回はクエリパラメータというURLに情報を含み、次の画面でURLから情報を取得する方法を採用しました。

「?」の後に変数名と、その値が格納されていることがわかります。

  • correct:画面に表示された早口言葉(正解文字列)
  • typed:ユーザーが入力した文字列(あなたの入力)
  • time:経過時間


クエリパラメータはURLに情報が見えても問題ない場合に使えます。

情報を見せないようにするためには「Session Storage」、「Local Storage」、「Cookie」などのストレージに情報を保持しておく必要があります。

実運用ではストレージに保存する方が多いので、次の機会に試してみようと思います。

XSS攻撃の対策方法

生成AIにソースコードを生成してもらっていると「XSS攻撃の対策もしています」と書いてありました。

XSS(クロスサイトスクリプティング)攻撃は、WebアプリまたはWebサイトの脆弱性を突き、悪意のあるスクリプトをユーザーのブラウザ上で実行させるサイバー攻撃です。

入力フォームやコメント欄などを通じて不正なスクリプトを埋め込み、それが無害化されないままWebページに表示されると、そのページを閲覧した他のユーザーのブラウザ上でスクリプトが実行されてしまいます。

タイピングゲームはユーザーに文字を入力してもらい、その内容を画面に表示します。

この「画面に表示する」処理で innerHTMLなどを使うとHTMLと解釈されて、ブラウザで実行されてしまう危険があります。

XSS攻撃を防ぐには、ユーザー入力をHTMLとして解釈させず、単なる文字列として扱うことが重要です。

今回はtextContentを使い、ブラウザが入力内容をHTMLやJavaScriptとして解釈しないようにしました。

  // 画面に反映(テキストとして扱う)
  document.getElementById('correct-word').textContent = String(correct);
  document.getElementById('typed-word').textContent = typed
    ? String(typed)
    : '(入力なし)';
  document.getElementById('elapsed-sec').textContent = String(time);


生成AI(使ったのはChatGPT)でソースコードを自動生成すると、セキュリティ対策やエラーハンドリングも対応してくれることがあります。

実運用で必要な知識も一緒に学べるのは、生成AIを活用するメリットになりますね。


まとめ

生成AIを使ってタイピングゲームを作ってみました。

業務と関係ないものを作りましたが、セキュリティ対策やデータの受け渡しなど実際にWebアプリを作成するのに必要な知識が身につきました。

やっぱりアウトプットが大事というのは こういうことですね。

どんなものを作っても学べることはたくさんあります!

今回はクエリパラメータを使ったので、ユーザーが入力した内容がURLに表示されています。

Session Storageなどデータが見えないように改善してみようと思います。


参考情報

  • この記事を書いた人

ねぎねず

IT企業で働くエンジニア。プログラミングや生成AIを勉強中。勉強や生活するなかで役に立った情報を発信していきます。

-プログラミング
-