最近、娘がタブレットで無料の記憶ゲームをプレイしているのを見て、広告や迷惑なポップアップバナーの多さに苦労しているのを気づきました。
これが私に刺激を与え、彼女のために同様のゲームを作ることにしました。彼女は現在アニメに夢中なので、かわいいアニメスタイルの画像を使用してゲームを作成することにしました。
この記事では、自分自身やお子さんのためにゲームを作るプロセスを紹介します🎮。
まずはゲームの特徴を探求し、次に使用する技術スタックとプロジェクト構造について説明します。最後に、最適化とモバイルデバイス📱でのスムーズなゲームプレイを確認します。
お読みいただくのをスキップしたい場合は、💁こちらがGitHubリポジトリ🙌です。そして💁こちらがライブデモです。
目次
プロジェクトの説明
このチュートリアルでは、Reactを使用して記憶力をテストする難しいメモリカードゲームを作成します。あなたの目標は、同じ画像を2回クリックせずにユニークなアニメ画像をクリックすることです。ユニークなクリックごとにポイントがもらえますが、同じ画像を2回クリックすると進行がリセットされます。
ゲームの特徴:
-
🎯 記憶力に挑戦するダイナミックなゲームプレイ
-
🔄 カードがクリックごとにシャッフルされて難易度が上がる
-
🏆 ベストスコアの永続的なスコアトラッキング
-
😺 The Nekosia API からのかわいいアニメ画像
-
✨ スムーズな読み込みトランジションとアニメーション
-
📱 すべてのデバイスに対応したレスポンシブデザイン
-
🎨 クリーンでモダンなUI
このゲームは、かわいいアニメ画像を楽しみながら記憶力をテストする手助けをします。完璧なスコアを達成できるかな?
遊び方
-
開始するには、任意のカードをクリックします
-
クリックしたカードを覚えておきます
-
すべてのカードを正確に1回クリックしよう
-
ユニークな選択ごとにスコアが増えていきます
-
その後、ベストスコアを更新しようとプレイし続けます
使用するテクノロジースタック
使用する主なテクノロジーのリストです:
-
NPM – JavaScript のパッケージマネージャーで、プロジェクトの依存関係とスクリプトを管理するのに役立ちます。
-
Vite – 特にモダンなウェブプロジェクトに最適化された、迅速な開発環境を提供するビルドツールです。
-
React – ユーザーインターフェースを構築するための人気のJavaScriptライブラリで、効率的なレンダリングと状態管理を可能にします。
-
CSS Modules – CSSを個々のコンポーネントにスコープするスタイリングソリューションで、スタイルの競合を防ぎ、保守性を確保します。
ゲームを作ろう
ここから先は、このゲームを作成する際に私がたどったプロセスを案内します。
プロジェクトの構造とアーキテクチャ
このメモリーカードゲームを構築する際、私はコードベースを慎重に整理し、保守性、スケーラビリティ、関心の分離を明確にすることを重視しました。それぞれの決定の背後にある構造と理由を探りましょう:
コンポーネントベースのアーキテクチャ
私はいくつかの理由からコンポーネントベースのアーキテクチャを選びました:
-
モジュラリティ: 各コンポーネントは独自のロジックとスタイルを持つ自己完結型です
-
再利用性:
Card
やLoader
のようなコンポーネントはアプリケーション全体で再利用できます -
保守性: 個々のコンポーネントのデバッグと修正が容易になります
-
テスト: コンポーネントは独立してテストできます
コンポーネントの組織
- カードコンポーネント
-
コアなゲーム要素であるため、独自のディレクトリに分割されています
-
カプセル化のためのJSXとSCSSモジュールが含まれています
-
個々のカードのレンダリング、ローディング状態、およびクリックイベントを処理します
- カードグリッドコンポーネント
-
ゲームボードのレイアウトを管理します
-
カードのシャッフルと配布を処理します
-
異なる画面サイズに対応するレスポンシブグリッドレイアウトを制御します
- ローダーコンポーネント
-
再利用可能なローディングインジケーター
-
画像の読み込み中にユーザーエクスペリエンスを向上させます
-
ローディング状態が必要な任意のコンポーネントで使用できます
- ヘッダー/フッター/サブタイトルコンポーネント
-
アプリのレイアウトのための構造コンポーネント
-
ヘッダーにはゲームのタイトルとスコアが表示されます
-
フッターには著作権とバージョン情報が表示されます
-
サブタイトルはゲームの説明を提供します
CSSモジュールアプローチ
CSSモジュール(.module.scss
ファイル)を使用して、いくつかの利点を活用しました:
-
スコープ付きスタイリング: コンポーネント間のスタイルリークを防止します
-
名前の衝突: ユニークなクラス名が自動生成されます
-
保守性: スタイルはそれに関連するコンポーネントと共に配置されます
-
SCSSの機能: スタイルをモジュラーに保ちつつ、SCSSの機能を活用します
カスタムフック
hooks
ディレクトリには、useFetchなどのカスタムフックが含まれています:
-
関心の分離: データ取得ロジックを分離します
-
再利用性: 画像データを必要とする任意のコンポーネントで使用できます
-
状態管理: ロード、エラー、およびデータの状態を処理します
-
パフォーマンス: 画像サイズ制御などの最適化を実装します
ルートレベルファイル
App.jsx:
-
アプリケーションのエントリーポイントとして機能します
-
グローバルな状態とルーティングを管理します(必要な場合)
-
コンポーネントの組み合わせを調整します
-
トップレベルのレイアウトを処理します
パフォーマンスに関する考慮事項
この構造はパフォーマンスの最適化をサポートします:
-
コード分割: 必要に応じてコンポーネントを遅延読み込みできます
-
メモ化: コンポーネントを効果的にメモ化できます
-
スタイルの読み込み: CSSモジュールを使用して効率的なスタイル読み込みを実現します
-
アセット管理: 画像やリソースが適切に整理されます
拡張性
この構造により簡単にスケーリングできます:
-
新機能は新しいコンポーネントとして追加できます
-
新機能のための追加フックを作成できます
-
アプリケーションが成長してもスタイルを維持しやすくなります
-
テストは任意のレベルで実装できます
開発経験
構造は開発者の経験を向上させます:
-
明確なファイルの整理
-
直感的なコンポーネントの配置
-
特定の機能を見つけて変更しやすい
-
効率的なコラボレーションをサポート
このアーキテクチャは、タブレットでのゲーム最適化時に特に価値があり、次のことができました:
-
パフォーマンスのボトルネックを簡単に特定して最適化
-
他のデバイスに影響を与えずにタブレット固有のスタイルを追加
-
モバイル体験向上のためのローディング状態を実装
-
ゲームロジックとUIコンポーネントのクリーンな分離を維持
さて、コーディングを始めましょう。
段階的なビルドガイド
1. プロジェクトのセットアップ
開発環境のセットアップ
クリーンなReactプロジェクトを始めるには、ターミナルアプリを開いて次のコマンドを実行します(プロジェクトフォルダの名前を自由につけることができます – 私の場合は名前が「memory-card」です):
npm create vite@latest memory-card -- --template react
cd memory-card
npm install
必要な依存関係をインストールする
このプロジェクトで使用する唯一の依存関係は、UI.devのhookパッケージです(ところで、こちらでReactでのレンダリングの仕組みについて分かりやすく説明された記事が見つかります)。
もう1つの依存関係は有名なCSSプリプロセッサー、SASSです。通常のCSSの代わりにSASSでCSSモジュールを記述するために必要です。
npm install @uidotdev/usehooks sass
Viteとプロジェクト設定を構成する
プロジェクトを設定する際には、SASSの警告を処理し開発体験を向上させるために特定の構成調整を行う必要があります。以下にVitestの設定方法を示します:
// vitest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/setupTests.js'],
css: {
modules: {
classNameStrategy: 'non-scoped'
}
},
preprocessors: {
'**/*.scss': 'sass'
},
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/setupTests.js',
'src/main.jsx',
'src/vite-env.d.ts',
],
},
},
css: {
preprocessorOptions: {
scss: {
quietDeps: true, // SASS依存関係の警告を抑制します
charset: false // 最近のSASSバージョンでのcharset警告を防止します
}
}
}
});
これらの構成のほとんどは、Viteでプロジェクトを作成すると自動生成されます。何が起こっているかについては以下の通りです:
-
SASSの設定:
-
quietDeps: true
: これにより、SASSモジュールで非推奨の依存関係に関する警告が抑制されます。特にサードパーティのSASS/SCSSファイルを使用する場合に便利です。 -
charset: false
: スタイルシートで特殊文字を使用すると、新しいバージョンのSASSで表示される「@charset」警告を防ぎます。
-
-
テストの設定:
-
globals: true
: テスト関数をテストファイルでグローバルに利用可能にします -
environment: 'jsdom'
: テスト用のDOM環境を提供します -
setupFiles
: テストセットアップファイルを指定します
-
これらの設定は、コンソールに不要な警告メッセージが表示されないようにし、適切なテスト環境の設定を行い、SASS/SCSSの処理がスムーズに動作するようにします。
これらの設定がない場合、次のような場面でコンソールに警告が表示されるかもしれません:
-
SASS/SCSSの機能を使用したり、SASSファイルをインポートしたりする場合
-
DOM操作を必要とするテストを実行する場合
-
スタイルシートで特殊文字を使用する場合
2. コンポーネントの構築
Cardコンポーネントを作成する
まず、個々の画像を表示する基本的なカードコンポーネントを作成しましょう。
// src/components/Card/Card.jsx
import React, { useState, useCallback } from "react";
import Loader from "../Loader";
import styles from "./Card.module.scss";
const Card = React.memo(function Card({ imgUrl, imageId, categoryName, processTurn }) {
const [isLoading, setIsLoading] = useState(true);
const handleImageLoad = useCallback(() => {
setIsLoading(false);
}, []);
const handleClick = useCallback(() => {
processTurn(imageId);
}, [processTurn, imageId]);
return (
<div className={styles.container} onClick={handleClick}>
{isLoading && (
<div className={styles.loaderContainer}>
<Loader message="Loading..." />
</div>
)}
<img
src={imgUrl}
alt={categoryName}
onLoad={handleImageLoad}
className={`${styles.image} ${isLoading ? styles.hidden : ''}`}
/>
</div>
);
});
export default Card;
Cardコンポーネントは、ゲームの基本的な構成要素です。個々の画像を表示し、プレーヤーとのやり取りを処理します。実装を細かく見ていきましょう。
プロップ分解:
-
image
: (string)-
APIサービスから受け取った表示される画像のURL。
-
imgタグのsrc属性で直接使用されます。
-
-
id
: (string)-
クリックされたカードを追跡するために重要な各カードのユニーク識別子。
-
カードがクリックされたときに
processTurn
コールバックに渡されます。
-
-
category
: (string)-
画像の種類を説明します(たとえば、「アニメ」、「ネコ」など)、アクセシビリティ向上のためにalt属性で使用されます。
-
SEOとスクリーンリーダーで役立ちます。
-
-
processTurn
: (function)-
親コンポーネントから渡されるコールバック関数で、カードがクリックされたときのゲームロジックを処理します。
-
スコアの更新やゲーム状態の変更を管理し、カードが以前にクリックされたかどうかを判断します。
-
-
isLoading
: (boolean)-
ローディング状態を表示するかどうかを制御します。trueの場合、画像の代わりにローダーコンポーネントが表示されます。
-
画像の読み込み中にユーザーエクスペリエンスを向上させます。
-
コンポーネントのスタイリング:
// src/components/Card/Card.module.scss
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.8);
padding: 20px;
font-size: 30px;
text-align: center;
min-height: 200px;
position: relative;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
.image {
width: 10rem;
height: auto;
opacity: 1;
transition: opacity 0.3s ease;
&.hidden {
opacity: 0;
}
}
.loaderContainer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
コンポーネント内での使用:
<Card
key={getKey()}
imgUrl={item?.image?.original?.url || ""}
imageId={item?.id}
categoryName={item?.category}
processTurn={(imageId) => processTurn(imageId)}
/>
主な特徴:
-
パフォーマンスの最適化:
-
不必要な再レンダリングを防ぐために
React.memo
を使用 -
イベントハンドラーに
useCallback
を実装 -
より良いUXのために内部で読み込み状態を管理
-
-
ローディング状態の管理:
- 内部の
isLoading
状態が画像の読み込みを追跡
- 内部の
- 読み込み中にメッセージ付きのローダーコンポーネントを表示
-
イベント処理:
-
handleImageLoad
: 読み込み状態の遷移を管理します -
handleClick
:processTurn
コールバックを介してプレイヤーの動きを処理します
-
CardsGridコンポーネントを構築します
これはゲームの状態、スコアロジック、およびカードの相互作用を管理するメインゲームコンポーネントです。その実装を詳しく見ていきましょう:
// src/components/CardsGrid/CardsGrid.jsx
import React, { useState, useEffect } from "react";
import { useLocalStorage } from "@uidotdev/usehooks";
import Card from "../Card";
import Loader from "../Loader";
import styles from "./CardsGrid.module.scss";
import useFetch from "../../hooks/useFetch";
function CardsGrid(data) {
// ステート管理
const [images, setImages] = useState(data?.data?.images || []);
const [clickedImages, setClickedImages] = useLocalStorage("clickedImages", []);
const [score, setScore] = useLocalStorage("score", 0);
const [bestScore, setBestScore] = useLocalStorage("bestScore", 0);
const [isLoading, setIsLoading] = useState(!data?.data?.images?.length);
// 画像を取得するためのカスタムフック
const { data: fetchedData, fetchData, error } = useFetch();
// 新しいデータが取得されたときに画像を更新する
useEffect(() => {
if (fetchedData?.images) {
setImages(fetchedData.images);
setIsLoading(false);
// 新しいバッチがロードされたときにクリックされた画像をリセットする
setClickedImages([]);
}
}, [fetchedData]);
// ベストスコアを更新するためのヘルパー関数
function updateBestScore(currentScore) {
if (currentScore > bestScore) {
setBestScore(currentScore);
}
}
// コアゲームロジック
function processTurn(imageId) {
const newClickedImages = [...clickedImages, imageId];
setClickedImages(newClickedImages);
// 同じ画像を2回クリックした場合、すべてリセットする
if (clickedImages.includes(imageId)) {
// 必要に応じてベストスコアを更新する
updateBestScore(score);
setClickedImages([]);
setScore(0);
} else {
// 成功したカードの選択を処理する
const newScore = score + 1;
setScore(newScore);
// パーフェクトスコアを確認する(すべてのカードを1度クリック)
if (newClickedImages.length === images.length) {
updateBestScore(newScore);
fetchData();
setClickedImages([]);
} else {
// 画像をシャッフルする
const shuffled = [...images].sort(() => Math.random() - 0.5);
setImages(shuffled);
}
}
}
if (error) {
return <p>Failed to fetch data</p>;
}
if (isLoading) {
return <Loader message="Loading new images..." />;
}
return (
<div className={styles.container}>
{images.map((item) => (
<Card
key={getKey()}
imgUrl={item?.image?.original?.url || ""}
imageId={item?.id}
categoryName={item?.category}
processTurn={(imageId) => processTurn(imageId)}
/>
))}
</div>
);
}
export default React.memo(CardsGrid);
コンポーネントのスタイリング:
.container {
display: grid;
gap: 1rem 1rem;
grid-template-columns: auto; /* デフォルト:モバイルファーストで1列 */
background-color: #2196f3;
padding: 0.7rem;
cursor: pointer;
}
@media (min-width: 481px) {
.container {
grid-template-columns: auto auto; /* タブレット以上の場合は2列 */
}
}
@media (min-width: 769px) {
.container {
grid-template-columns: auto auto auto; /* デスクトップおよびそれ以上の場合は3列 */
}
}
主な機能の分解:
-
状態管理:
-
useState
をコンポーネントレベルの状態管理に使用 -
useLocalStorage
を使用して永続的なゲームデータを実装:-
clickedImages
: クリックされたカードを追跡 -
score
: 現在のゲームスコア -
bestScore
: 達成した最高スコア
-
-
画像の取得のためのローディング状態を管理
-
カードをシャッフル
-
-
ゲームロジック:
-
processTurn
: プレイヤーの動きを処理する-
重複クリックを追跡する
-
スコアを更新する
-
完璧なスコアのシナリオを管理する
-
-
updateBestScore
: 必要に応じてハイスコアを更新する -
ラウンドが完了した際に新しい画像を自動的に取得する
-
-
データ取得:
-
画像データのためにカスタム
useFetch
フックを使用する -
読み込みとエラーの状態を処理する
-
新しいデータが取得された際に画像を更新する
-
-
パフォーマンスの最適化:
-
React.memo
でラップされたコンポーネント -
効率的な状態更新
-
レスポンシブなグリッドレイアウト
-
-
永続性:
-
ページのリロードを超えてゲーム状態が維持される
-
最高スコアの追跡
-
現在のゲームの進行状況の保存
-
使用例:
...
...
function App() {
const { data, loading, error } = useFetch();
if (loading) return <Loader />;
if (error) return <p>Error: {error}</p>;
return (
<div className={styles.container}>
<Header />
<Subtitle />
<CardsGrid data={data} />
<Footer />
</div>
);
}
export default App;
Memory Cardゲームの中心となるCardsGridコンポーネントは以下を管理します:
-
ゲームの状態とロジック
-
スコアの追跡
-
カードの相互作用
-
画像の読み込みと表示
-
レスポンシブレイアウト
-
データ永続性
この実装は、コードの可読性と保守性を維持しながら、適切な状態管理と関心事の明確な分離を通じてスムーズなゲーム体験を提供します。
3. APIレイヤーの実装
当社のゲームは信頼性の高い画像配信を確保するために複数のフォールバックオプションを備えた堅牢なAPIレイヤーを使用しています。各サービスとフォールバックメカニズムを実装しましょう。
プライマリAPIサービスの設定:
// src/services/api/nekosiaApi.js
const NEKOSIA_API_URL = "https://api.nekosia.cat/api/v1/images/catgirl";
export async function fetchNekosiaImages() {
const response = await fetch(
`${NEKOSIA_API_URL}?count=21&additionalTags=white-hair,uniform&blacklistedTags=short-hair,sad,maid&width=300`
);
if (!response.ok) {
throw new Error(`Nekosia API error: ${response.status}`);
}
const result = await response.json();
if (!result.images || !Array.isArray(result.images)) {
throw new Error('Invalid response format from Nekosia API');
}
const validImages = result.images.filter(item => item?.image?.original?.url);
if (validImages.length === 0) {
throw new Error('No valid images received from Nekosia API');
}
return { ...result, images: validImages };
}
最初のフォールバックAPIサービスの作成:
// src/services/api/nekosBestApi.js
const NEKOS_BEST_API_URL = "https://nekos.best/api/v2/neko?amount=21";
export async function fetchNekosBestImages() {
const response = await fetch(NEKOS_BEST_API_URL, {
method: "GET",
mode: "no-cors"
});
if (!response.ok) {
throw new Error(`Nekos Best API error: ${response.status}`);
}
const result = await response.json();
// 応答を期待される形式に変換
const transformedImages = result.results.map(item => ({
id: item.url.split('/').pop().split('.')[0], // URLからUUIDを抽出
image: {
original: {
url: item.url
}
},
artist: {
name: item.artist_name,
href: item.artist_href
},
source: item.source_url
}));
return { images: transformedImages };
}
2番目のフォールバックAPIサービスの作成:
// src/services/api/nekosApi.js
const NEKOS_API_URL = "https://api.nekosapi.com/v3/images/random?limit=21&rating=safe";
export async function fetchNekosImages() {
const response = await fetch(NEKOS_API_URL, {
method: "GET",
});
if (!response.ok) {
throw new Error(`Nekos API error: ${response.status}`);
}
const result = await response.json();
// 応答を期待される形式に変換
const transformedImages = result.items.map(item => ({
id: item.id,
image: {
original: {
url: item.image_url
}
}
}));
return { images: transformedImages };
}
APIフォールバックメカニズムを構築する:
// src/services/api/imageService.js
import { fetchNekosiaImages } from "./nekosiaApi";
import { fetchNekosImages } from "./nekosApi";
import { fetchNekosBestImages } from "./nekosBestApi";
export async function fetchImages() {
try {
// 最初にプライマリAPIを試す
return await fetchNekosiaImages();
} catch (error) {
console.warn("Primary API failed, trying fallback:", error);
// 最初のフォールバックAPIを試す
try {
return await fetchNekosBestImages();
} catch (fallbackError) {
console.warn("First fallback API failed, trying second fallback:", fallbackError);
// 2番目のフォールバックAPIを試す
try {
return await fetchNekosImages();
} catch (secondFallbackError) {
console.error("All image APIs failed:", secondFallbackError);
throw new Error("All image APIs failed");
}
}
}
}
画像サービスを使用する:
// src/hooks/useFetch.js
import { useState, useEffect } from "react";
import { fetchImages } from "../services/api/imageService";
export default function useFetch() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const result = await fetchImages();
setData(result);
} catch (err) {
setError(err.message || 'An error occurred');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
return {
data,
loading,
error,
fetchData,
};
}
当社のAPI実装の主な特徴:
-
複数のAPIソース:
-
プライマリAPI(Nekosia):高品質なアニメ画像を提供
-
最初のフォールバック(Nekos Best):アーティスト情報を含む
-
2番目のフォールバック(Nekos):シンプルで信頼性のあるバックアップ
-
-
一貫したデータ形式:
- すべてのAPIは、期待される形式に応じてレスポンスを変換します:
{
images: [
{
id: string,
image: {
original: {
url: string
}
}
}
]
}
-
堅牢なエラー処理:
-
APIレスポンスの検証
-
有効な画像URLをチェック
-
詳細なエラーメッセージを提供
-
優雅なフォールバックメカニズム
-
-
安全機能:
-
安全なコンテンツフィルタリング(
rating=safe
) -
画像数制限(21枚の画像)
-
URLの検証
-
レスポンス形式の検証
-
-
パフォーマンスに関する考慮事項:
-
最適化された画像サイズ
-
フィルタリングされたコンテンツタグ
-
効率的なデータ変換
-
最小限のAPIコール
-
この実装により、ゲームが信頼性のある画像ソースを持ち、潜在的なAPIの障害を上手く処理できます。すべてのAPIで一貫したデータ形式を使用することで、ゲームの機能に影響を与えることなくAPI間をスイッチすることが容易になります。
アプリのテスト
テストはアプリケーション開発の重要な部分であり、私たちのメモリーカードゲームでは、現代のツールと実践を使用した包括的なテスト戦略を実装しました。テストの構造と使用した主なテストパターンについて見ていきましょう。
テストスタック
-
Vitest: 速度とViteとのシームレスな統合のために選択した当社の主要なテストフレームワーク
-
React Testing Library: ユーザー中心のアプローチでReactコンポーネントをテストするためのライブラリ
-
@testing-library/user-event: ユーザーの操作をシミュレートするためのライブラリ
-
jsdom: テストにおけるDOM環境の作成のために
主要なテストパターン
テストは、このメモリーカードゲームの信頼性と保守性を確保するための重要な部分でした。React Testing LibraryとVitestを使用して包括的なテスト戦略を実装し、いくつかの重要な領域に焦点を当てました:
1. コンポーネントテスト
私のReactコンポーネントが正しくレンダリングされ、期待どおりに動作することを確認するために、広範なテストを作成しました。たとえば、ゲームの中心であるCardsGrid
コンポーネントは、以下を含む徹底的なテストカバレッジがあります:
-
初期レンダリング状態
-
ローディング状態
-
エラーハンドリング
-
スコア追跡
-
カードインタラクションの動作
2. テストモッキング
信頼性が高く迅速なテストを確保するために、いくつかのモッキング戦略を実装しました:
-
useLocalStorageフックを使用したローカルストレージ操作
-
useFetch
フックを使用したAPI呼び出し -
イベントハンドラと状態の更新
3. テストのベストプラクティス
テストの実装中、以下のベストプラクティスに従いました:
-
beforeEach
とafterEach
フックを使用してテスト間で状態をリセットする -
React Testing Libraryから
fireEvent
を使用してユーザーのインタラクションをテストする -
ユーザーがアプリとやり取りする方法に似たテストを記述する
-
成功およびエラーシナリオの両方をテストする
-
適切なモッキングを使用してテストを分離する
4. テストツール
プロジェクトはモダンなテストツールとライブラリを活用しています:
-
Vitest: テストランナーとして
-
React Testing Library: Reactコンポーネントのテストに使用
- @testing-library/jest-dom: 拡張されたDOMテストアサーションに使用
-
@testing-library/user-event: ユーザー操作のシミュレーション用
この包括的なテストアプローチは、バグを早期に発見し、コード品質を確保し、リファクタリングをより安全かつ管理しやすくしました。
最適化
特にモバイルデバイスでスムーズなパフォーマンスを確保するために、いくつかの最適化テクニックを実装しました:
-
レスポンス変換
-
すべてのAPIで標準化されたデータ形式
-
URLから効率的なIDの抽出
-
迅速なアクセスのための構造化された画像メタデータ
-
-
ネットワーク最適化
-
適切なときに
no-cors
モードを使用してCORSの問題を効率的に処理 -
より良いデバッグのための特定のステータスコードでのエラーハンドリング
-
すべてのAPI実装で一貫したレスポンス構造
-
-
モバイルファーストの考慮事項
-
最適化された画像読み込み戦略
-
不要なリトライを防ぐための効率的なエラーハンドリング
-
処理オーバーヘッドを削減するための効率的なデータ変換
-
将来の改善点
このプロジェクトをさらに改善するいくつかの方法があります:
-
APIレスポンスのキャッシュ
-
よく使用される画像のためのローカルストレージキャッシュの実装
-
新しいコンテンツ用のキャッシュ無効化戦略の追加
-
プログレッシブ画像読み込みの実装
-
-
パフォーマンスの最適化
-
初回読み込み時間を短縮するための画像の遅延読み込みを追加する
-
帯域幅管理を向上させるためのリクエストキューイングを実装する
-
より高速なデータ転送のためのレスポンス圧縮を追加する
-
-
信頼性の向上
-
試行前のAPIヘルスチェックを追加する
-
指数バックオフを伴う再試行メカニズムを実装する
-
失敗するAPIに対するサーキットブレーカーパターンを追加する
-
-
分析と監視
-
APIの成功率を追跡する
-
応答時間を監視する
-
パフォーマンス指標に基づいて自動API切り替えを実装する
-
この堅牢な実装により、私たちのゲームは悪化したネットワーク条件やAPIの利用不可の状況下でも機能的かつパフォーマンスを保ちながら、将来の改善や最適化の余地を残しています。
結論
このメモリーカードゲームの構築は、子供たちのための楽しい広告のない代替手段を作る以上のものでした。現代のウェブ開発のベストプラクティスを実装し、実際の問題を解決するための練習でもありました。
このプロジェクトは、思慮深いアーキテクチャ、堅牢なテスト、および信頼できるフォールバック機構を組み合わせることで、エンターテインメント性と教育性を兼ね備えた本番対応のアプリケーションを実現できることを示しています。
🗝️ 重要なポイント
-
ユーザー中心の開発
-
明確な問題から始めた(広告で満たされたゲームがユーザー体験に影響を与える)
-
ゲームプレイを中断せずに向上させる機能を実装
-
デバイス間でのパフォーマンスと信頼性に焦点を当て続けた
-
-
技術的卓越性
-
クリーンで保守可能なコードのために最新のReactパターンとフックを活用
-
信頼性を確保するための包括的なテスト戦略を実装
-
中断のないゲームプレイのための堅牢なAPIフォールバックシステムを構築
-
-
パフォーマンス優先
-
レスポンシブデザインを用いたモバイルファーストアプローチを採用しました
-
画像の読み込みと処理を最適化しました
-
効率的な状態管理とキャッシング戦略を実装しました
-
📚 学習成果
このプロジェクトは、一見シンプルなゲームが複雑な技術ソリューションを実装する優れた手段となることを示しています。コンポーネントアーキテクチャからAPIフォールバックまで、各機能はスケーラビリティとメンテナンス性を考慮して構築されており、趣味のプロジェクトでもプロフェッショナルなコード品質を維持できることを証明しています。
🔮 今後の展望
ゲームは広告のない楽しい体験を提供するという主要な目標を達成していますが、文書化された将来の改善点は進化のための明確なロードマップを提供します。追加の最適化を実施したり新機能を追加したりすることにかかわらず、基盤は堅固で拡張に備えています。
メモリーカードゲームは、個人プロジェクトがどのように実際の問題を解決し、現代のウェブ開発におけるベストプラクティスを実装するためのプラットフォームとなり得るかを証明しています。コードを自由に探索したり、貢献したり、自分のプロジェクトのインスピレーションとして利用してください!
Source:
https://www.freecodecamp.org/news/how-to-build-a-memory-card-game-using-react/