はじめに
自分用のTOEIC Part5(短文穴埋め)学習アプリを作成したので、その開発記録を残します。
TOEIC Part5の対策として、アプリを眺めていましたが、
既存ではなかなかしっくりくるアプリがなかったため自作するとこにしました。
条件は以下
- ワンクリックで問題が開始される
- 強制的に問題を解くしかないシンプルな画面
- とにかくサクサク動く
- ログイン不要でも間違えた問題は記憶できる
- 間違えた問題の反復をしたい
👉 実際に作成したアプリはこちらから触れます
PART5 FOCUS
使用した技術とデプロイ環境
- バックエンド: Python 3 / FastAPI (デプロイ先: Render)
- フロントエンド: JavaScript / Vue 3 / Vite (デプロイ先: Vercel)
構築にあたり考えたこと
速さを実現するための結論は、サーバーへのリクエストと、裏側の処理を最小限にすることです。
重い仕組みは一切入れず、以下の構成で割り切って構築しました。
-
DBは不要(JSONファイルとメモリで済ませる)
学習アプリなため問題が必要。DBではなくJSONファイルに記載することにしました。 -
フロントエンドは「画面表示」のみ
フロントエンド(Vue.js)側では、ただ画面を表示し、ユーザーのクリック操作を受け取るだけのシンプルな作りにしました。 -
データ保存はローカルストレージ
「どの問題を間違えたか」などの学習履歴をいちいちサーバーに送って保存するとリクエストが増えるため、すべてブラウザのlocalStorageに保存するようにしました。 -
アップデート前提
自分用にアップデートできればいいかと思い、スモールスタートで開発する。
ログイン機能を追加して、学習記録を紐づけるなど考えています。
実際のソースコード
上記の「処理を最小限にする」工夫として、バックエンドとフロントエンドのコアな部分のコードを残しておきます。
バックエンド(FastAPI)
毎回ファイル(JSON)を読み込むと処理が遅くなるため、Pythonの @lru_cache を使い、初回以外はメモリから一瞬でデータを返すようにしています。
from fastapi import FastAPI
from functools import lru_cache
import json
import random
app = FastAPI()
# リクエストを最小限にし、速さを出すための工夫
# @lru_cache(maxsize=1) をつけることで、2回目以降はファイルの読み込み処理をスキップし、メモリから爆速で返す
@lru_cache(maxsize=1)
def load_questions():
with open("questions.json", "r", encoding="utf-8") as f:
return json.load(f)
@app.get("/api/next_question")
def get_next_question(missed_ids: str = ""):
questions = load_questions()
# フロントエンドから送られてきた苦手な問題(missed_ids)のリスト化
missed_list = [int(id) for id in missed_ids.split(",")] if missed_ids else []
# 簡単な出題ロジックの例(実際はここに絞り込み処理などが入る)
next_q = random.choice(questions)
return {"question": next_q}
フロントエンド(Vue 3)
学習履歴(間違えた問題ID)はすべてブラウザの localStorage に保存・取得し、それをバックエンドに渡すだけのシンプルな設計です。
<script setup>
import { ref, onMounted } from 'vue';
// 画面に表示する現在の問題
const currentQuestion = ref(null);
// 1. ローカルストレージから「間違えた問題のIDリスト」を取得する処理
const getMissedIds = () => {
const storedIds = localStorage.getItem('missed_question_ids');
return storedIds ? JSON.parse(storedIds) : [];
};
// 2. 新しく間違えた問題のIDをローカルストレージに保存する処理
const saveMissedId = (id) => {
const missedIds = getMissedIds();
// まだ保存されていなければ追加する
if (!missedIds.includes(id)) {
missedIds.push(id);
localStorage.setItem('missed_question_ids', JSON.stringify(missedIds));
}
};
// 3. バックエンドから次の問題を取得する処理
const fetchNextQuestion = async () => {
const missedIds = getMissedIds();
// URLパラメータにカンマ区切りでIDをくっつけてサーバーに送る
const query = missedIds.length > 0 ? `?missed_ids=${missedIds.join(',')}` : '';
const response = await fetch(`/api/next_question${query}`);
const data = await response.json();
currentQuestion.value = data.question;
};
// 初回読み込み時に最初の問題を取得する
onMounted(() => {
fetchNextQuestion();
});
</script>
<template>
<div>
<p v-if="currentQuestion">{{ currentQuestion.text }}</p>
</div>
</template>
まとめ
アプリ構築の際に、間違えた問題をどうやって記録しようかな? と考えていたところ「ローカルストレージ」の存在を思い出しました。考えてみると基本的にはログイン機能があって、情報はDBに保存して~みたいなセオリーがあると思うのですが、個人開発ならそのセオリーは無視できますね。
ここまでご覧いただきありがとうございました!また機会がありましたらご参照ください!
👉 実際に作成したアプリはこちらから触れます
PART5 FOCUS
👉 X(旧Twitter)アカウントはこちら
https://x.com/it_sabamiso