최근, 제 아이 🧒🏻가 태블릿에서 무료 메모리 게임을 하는 모습을 보면서 그녀가 압도적인 광고와 성가신 팝업 배너로 어려움을 겪고 있는 것을 발견했습니다.
이것은 그녀를 위해 비슷한 게임을 만들고 싶다는 영감을 주었습니다. 현재 그녀가 애니메이션에 관심이 많기 때문에 귀여운 애니메이션 스타일의 이미지를 사용하여 게임을 만들기로 결정했습니다.
이 기사에서는 여러분이나 여러분의 아이들을 위해 게임을 만드는 과정을 안내하겠습니다 🎮.
먼저 게임 기능을 살펴본 다음, 기술 스택과 프로젝트 구조를 간단히 설명하겠습니다. 마지막으로 최적화와 모바일 기기에서 부드러운 게임 플레이를 보장하는 방법에 대해 논의하겠습니다 📱.
읽기를 건너뛰고 싶다면, 여기 💁에서 GitHub 저장소를 확인할 수 있습니다 🙌. 그리고 여기에서 라이브 데모를 볼 수 있습니다.
목차
프로젝트 설명
이 튜토리얼에서는 React를 사용하여 당신의 기억력을 테스트하는 도전적인 메모리 카드 게임을 만들어 보겠습니다. 당신의 목표는 동일한 이미지를 두 번 클릭하지 않고 고유한 애니메이션 이미지를 클릭하는 것입니다. 각 고유한 클릭은 점수를 획득하게 되지만, 이미지를 두 번 클릭하면 진행 상황이 재설정됩니다.
게임 특징:
-
🎯 기억력을 도전하는 동적 게임플레이
-
🔄 각 클릭 후 카드가 섞여 난이도가 증가합니다
-
🏆 최고 점수 유지를 위한 점수 추적
-
😺 네코시아 API에서 귀여운 애니메이지지
-
✨ 부드러운 로딩 전환 및 애니메이션
-
📱 모든 기기에 대응하는 반응형 디자인
-
🎨 깔끔하고 현대적인 UI
이 게임은 귀여운 애니메이지를 즐기면서 기억력을 테스트하는 데 도움이 됩니다. 완벽한 점수를 달성할 수 있을까요?
게임 방법
-
시작하려면 카드를 클릭하세요
-
클릭한 카드를 기억하세요
-
모든 카드를 정확히 한 번씩 클릭하려고 노력하세요
-
각 유니크한 선택마다 점수가 증가합니다
-
그런 다음 최고 점수를 넘기기 위해 계속 플레이하세요
기술 스택
주요 기술 목록은 다음과 같습니다:
-
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의 후크 패키지입니다 (그런데, 여기에서 React의 렌더링 방식에 대해 잘 설명된 기사를 찾아볼 수 있습니다).
다른 종속 항목은 유명한 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 버전에서 문자 집합 경고를 방지합니다
}
}
}
});
이 프로젝트를 Vite로 만들 때 대부분의 구성은 자동으로 생성됩니다. 여기에 어떤 일이 벌어지는지 알아보겠습니다:
-
SASS Configuration:
-
quietDeps: true
: 이는 SASS 모듈의 폐기된 종속성에 대한 경고를 억제합니다. 특히 제3자 SASS/SCSS 파일을 사용할 때 유용합니다. -
charset: false
: 스타일시트에서 특수 문자를 사용할 때 최신 버전의 SASS에서 나타나는 “@charset” 경고를 방지합니다.
-
-
Test Configuration:
-
globals: true
: 테스트 파일에서 테스트 함수를 전역적으로 사용할 수 있도록 합니다. -
environment: 'jsdom'
: 테스트를 위한 DOM 환경을 제공합니다. -
setupFiles
: 테스트 설정 파일을 가리킵니다.
-
이러한 구성은 불필요한 경고 메시지를 제거하고 적절한 테스트 환경 구성을 설정하며 SASS/SCSS 처리가 원활하게 작동하도록하여 더 깨끗한 개발 경험을 만들어줍니다.
이러한 구성을 하지 않으면 콘솔에 경고 메시지가 표시될 수 있습니다:
-
SASS/SCSS 기능을 사용하거나 SASS 파일을 가져올 때
-
DOM 조작을 필요로 하는 테스트를 실행할 때
-
스타일시트에 특수 문자를 사용할 때
2. 컴포넌트 빌드
카드 컴포넌트 생성
먼저 개별 이미지를 표시할 기본 카드 컴포넌트를 생성합시다:
// 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;
카드 컴포넌트는 게임의 기본 구성 요소입니다. 개별 이미지를 표시하고 플레이어 상호 작용을 처리하는 역할을 합니다. 구현 내용을 살펴보겠습니다:
속성 분해:
-
이미지
: (문자열)-
우리 API 서비스로부터 받은 이미지의 URL을 표시하는 이미지입니다.
-
img 태그의 src 속성에서 직접 사용됩니다.
-
-
id
: (문자열)-
클릭된 카드를 추적하는 데 중요한 각 카드의 고유 식별자입니다.
-
카드를 클릭했을 때
processTurn
콜백에 전달됩니다.
-
-
카테고리
: (문자열)-
이미지의 유형을 설명하는 것(예: “애니메이션”, “네코”)으로, 보다 나은 접근성을 위해 alt 속성에 사용됩니다.
-
SEO 및 스크린 리더 기능에 도움이 됩니다.
-
-
processTurn
: (함수)-
부모 구성 요소에서 전달된 콜백 함수로, 카드를 클릭할 때 게임 로직을 처리합니다.
-
또한 점수 업데이트 및 게임 상태 변경을 관리하고, 이전에 카드를 클릭했는지 여부를 결정합니다.
-
-
isLoading
: (부울)-
로딩 상태를 표시할지 여부를 제어합니다. true인 경우 이미지 대신 Loader 구성 요소를 표시합니다.
이미지 로딩 중 사용자 경험을 향상시킵니다.
-
컴포넌트 스타일링:
// 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
을 구현합니다 -
더 나은 사용자 경험을 위해 로딩 상태를 내부적으로 관리합니다
-
-
로딩 상태 관리:
-
내부
isLoading
상태가 이미지 로딩을 추적합니다 -
로딩 중 메시지와 함께 로더 컴포넌트를 표시합니다
-
CSS 클래스를 사용하여 이미지가 완전히 로드될 때까지 숨깁니다
-
-
이벤트 처리:
-
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);
// 동일한 이미지를 두 번 클릭하면 모든 것 재설정
if (clickedImages.includes(imageId)) {
// 필요한 경우 최고 점수 업데이트
updateBestScore(score);
setClickedImages([]);
setScore(0);
} else {
// 성공적인 카드 선택 처리
const newScore = score + 1;
setScore(newScore);
// 완벽한 점수 확인 (모든 카드를 한 번씩 클릭함)
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; /* 기본값: 모바일 우선으로 한 열 */
background-color: #2196f3;
padding: 0.7rem;
cursor: pointer;
}
@media (min-width: 481px) {
.container {
grid-template-columns: auto auto; /* 태블릿 및 이상용 두 열 */
}
}
@media (min-width: 769px) {
.container {
grid-template-columns: auto auto auto; /* 데스크톱 및 큰 화면용 세 열 */
}
}
주요 기능 상세 정보:
-
상태 관리:
-
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;
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 };
}
두 번째 예비 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);
// 두 번째 대체 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): 작가 정보 포함
-
두 번째 대체 (Nekos): 간단하고 신뢰할 수 있는 백업
-
-
일관된 데이터 형식:
- 모든 API는 응답을 우리가 예상하는 형식에 맞게 변환합니다:
{
images: [
{
id: string,
image: {
original: {
url: string
}
}
}
]
}
-
견고한 오류 처리:
-
API 응답 유효성 검사
-
올바른 이미지 URL 확인
-
자세한 오류 메시지 제공
-
호전적인 대체 매커니즘
-
-
안전 기능:
-
안전한 콘텐츠 필터링 (
rating=safe
) -
이미지 수 제한 (21개 이미지)
-
URL 유효성 검사
-
응답 형식 유효성 검사
-
-
성능 고려사항:
-
최적화된 이미지 크기
-
필터링된 콘텐츠 태그
-
효율적인 데이터 변환
-
최소한의 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 추출
-
빠른 액세스를 위한 구조화된 이미지 메타데이터
-
-
네트워크 최적화
-
CORS 문제를 효율적으로 처리하기 위해 적절한 곳에서
no-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/