최종 프로젝트: 게임 출시 정보·랭킹 대시보드
24강. 최종 프로젝트: 게임 출시 정보·랭킹 대시보드
1. 이번 강의에서 해결할 문제
지금까지 만든 컴포넌트와 기능을 실제 포트폴리오로 정리해야 합니다. 데이터 요청부터 반응형 페이지, 오류 처리, 라우팅까지 하나의 일관된 제품 흐름으로 통합합니다.
2. 학습 목표
- 기능별 폴더와 데이터 흐름을 최종 정리한다.
- 출시 정보, 랭킹, 상세, 404 화면을 완성한다.
- 접근성·반응형·빌드·Git 품질 기준으로 결과물을 점검한다.
3. 핵심 개념
최종 앱의 데이터 흐름은 fetchGames → useGames → Page → GameList → GameCard입니다. API 계층은 HTTP와 검증, Hook은 요청 상태, 페이지는 필터와 화면 상태, 컴포넌트는 표현과 사용자 이벤트를 맡습니다.
완성 기준은 “화면이 보임”이 아닙니다. 느린 요청, 빈 배열, 잘못된 URL, 키보드 탐색, 작은 화면, 새로고침, 프로덕션 빌드까지 확인해야 합니다.
가장 작은 수직 조각부터 닫기
처음부터 네 페이지를 동시에 만들지 않습니다. games.json의 한 항목 → fetchGames 검증 → useGames 상태 → HomePage 한 카드 → 카드 링크가 브라우저에서 끝까지 보이는 조각을 먼저 완성합니다. 이 조각이 정상·로딩·오류를 처리한 뒤 같은 검증된 배열을 랭킹과 상세 페이지에 확장합니다.
| 계층 | 입력 | 출력·책임 |
|---|---|---|
fetchGames | HTTP 응답 | 검증된 Game[] 또는 오류 |
useGames | API 함수 | loading/success/error, games, retry |
| 페이지 | 요청 상태와 배열 | 빈 결과·오류·페이지별 계산 |
| 표현 컴포넌트 | 표시할 Game과 이벤트 | 접근 가능한 카드·버튼·링크 |
상태를 한 단계씩 통과시키면 오류가 HTTP, 상태 관리, 필터, 렌더 중 어디서 생겼는지 찾을 수 있습니다.
4. 단계별 코드 예시
시작 전 상태와 구현 순서
이전 강의의 React·TypeScript 프로젝트가 개발 서버에서 열리고, npm.cmd run lint가 현재 변경의 기준 결과를 내야 합니다. public/data/games.json에는 id가 겹치지 않는 최소 한 항목이 있고 이미지·상세 링크의 실제 경로를 확인합니다.
구현 순서는 다음과 같습니다.
- 한 게임의 요청·검증·카드 렌더를 닫습니다.
- 로딩, 빈 배열, 잘못된 JSON, 재시도를 재현합니다.
- 검색·필터를 추가하고 원본 배열 유지 여부를 확인합니다.
- 랭킹과 상세 Route를 연결합니다.
- 잘못된 game id와 알 수 없는 경로를 각각 처리합니다.
- 모바일·키보드·빌드를 통과시킵니다.
먼저 최종 구조를 정리합니다.
game-release-dashboard/
├─ public/data/games.json
├─ src/
│ ├─ api/games.ts
│ ├─ components/
│ │ ├─ FilterBar.tsx
│ │ ├─ GameCard.tsx
│ │ ├─ GameCard.module.css
│ │ └─ GameList.tsx
│ ├─ hooks/useGames.ts
│ ├─ pages/
│ │ ├─ HomePage.tsx
│ │ ├─ RankingsPage.tsx
│ │ ├─ GameDetailPage.tsx
│ │ └─ NotFoundPage.tsx
│ ├─ types/game.ts
│ ├─ App.tsx
│ ├─ App.module.css
│ └─ main.tsx
└─ package.json
요청은 App에서 한 번 수행해 여러 페이지에 전달합니다.
파일 경로: game-release-dashboard/src/App.tsx
import { NavLink, Route, Routes } from 'react-router';
import { useGames } from './hooks/useGames';
import { GameDetailPage } from './pages/GameDetailPage';
import { HomePage } from './pages/HomePage';
import { NotFoundPage } from './pages/NotFoundPage';
import { RankingsPage } from './pages/RankingsPage';
export default function App() {
const { games, status, retry } = useGames();
return (
<>
<header>
<strong>Release Radar</strong>
<nav aria-label="주요 메뉴">
<NavLink to="/">출시 정보</NavLink>
<NavLink to="/rankings">랭킹</NavLink>
</nav>
</header>
<Routes>
<Route
path="/"
element={<HomePage games={games} status={status} retry={retry} />}
/>
<Route path="/rankings" element={<RankingsPage games={games} />} />
<Route path="/games/:gameId" element={<GameDetailPage games={games} />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</>
);
}
랭킹은 원본을 변경하지 않고 계산합니다.
파일 경로: game-release-dashboard/src/pages/RankingsPage.tsx
import type { Game } from '../types/game';
import { GameList } from '../components/GameList';
export function RankingsPage({ games }: { games: Game[] }) {
const rankedGames = games.toSorted((a, b) => b.score - a.score);
return (
<main>
<h1>게임 랭킹</h1>
<GameList games={rankedGames} />
</main>
);
}
마지막으로 품질 명령을 실행합니다.
실행 위치: game-release-dashboard/
npm.cmd run lint
npm.cmd run build
npm.cmd run preview
git status
git add .
git commit -m "feat: complete game release dashboard"
브라우저에서 확인할 구체 결과
| 시나리오 | 조작 | 기대 결과 |
|---|---|---|
| 정상 응답 | / 새로고침 | 로딩 뒤 JSON 항목 수만큼 카드 표시 |
| 검색 없음 | 존재하지 않는 제목 입력 | 빈 결과 안내와 필터 초기화 동작 |
| 랭킹 | /rankings 이동 | score 내림차순, 동점 정책 일관 |
| 없는 게임 | /games/not-found 직접 입력 | 게임 상세 없음 안내와 목록 링크 |
| 없는 경로 | /unknown 입력 | 사이트 404 페이지 |
| 요청 실패 | JSON 이름을 임시로 잘못 지정 | 오류 문구와 재시도 버튼, Console의 원인 |
| 모바일 | 320px에서 Tab 이동 | 가로 넘침 없이 메뉴·카드·버튼에 초점 표시 |
preview 주소를 연 뒤 Network에서 games.json의 상태 200과 응답 크기를 확인하고, 실패 실험 후 경로를 반드시 원래대로 돌립니다.
5. 코드가 동작하는 이유
useGames를 App에서 한 번 호출하므로 페이지 이동 때마다 같은 요청 로직을 중복하지 않습니다. Route는 URL에 맞는 페이지를 선택하고 모든 페이지는 검증된 Game 배열을 받습니다. rankings의 toSorted는 새 배열을 반환해 props를 변경하지 않습니다.
한 번 받은 배열은 App에서 페이지로 내려가고, 랭킹 페이지는 복사된 배열만 정렬합니다. 따라서 랭킹 방문 뒤 홈의 원래 순서가 바뀌지 않습니다. URL은 페이지 선택 상태가 되므로 상세 주소를 새로고침해도 같은 gameId로 항목을 다시 찾습니다.
6. 자주 하는 실수와 해결법
- 성공 경로만 확인: 로딩, 빈 데이터, 네트워크 오류, 잘못된 id와 경로를 일부러 재현합니다.
- 배포 base 경로 누락: 데이터 URL과 Vite base 설정을 배포 주소에 맞춥니다.
- 모바일에서만 메뉴를 숨기고 대체 탐색 없음: 모든 화면 폭에서 키보드로 이동 가능한 메뉴를 둡니다.
- lint 오류를 무시하고 커밋: lint와 build를 모두 통과한 상태를 기준으로 커밋합니다.
- README 없이 결과물만 제출: 목적, 기능, 실행 명령, 기술 선택, 개선점을 기록합니다.
7. 직접 실습
실습 목표
한 게임의 요청부터 카드·상세까지 닫은 뒤 랭킹과 실패 상태를 확장해 배포 빌드에서 검증합니다.
시작 전 상태
프로젝트 루트, Node 버전, 현재 lint·build 결과와 games.json 한 항목을 기록합니다. 기존 기능을 한꺼번에 고치지 말고 첫 수직 조각의 파일만 엽니다.
1단계: 따라 하기 — 한 게임 수직 조각
한 항목을 fetch하고 검증된 Game[]를 Hook과 HomePage를 거쳐 카드 한 개로 표시합니다. 카드 링크로 상세 페이지에 이동하고 새로고침 뒤 같은 제목을 확인합니다.
2단계: 값과 실패 바꿔 보기
점수를 바꿔 랭킹 순서 변화를 확인합니다. JSON 경로 또는 데이터 타입을 임시로 잘못 만들어 오류·재시도를 확인하고 원상 복구합니다. 빈 배열에서는 빈 결과 안내가 보여야 합니다.
3단계: 직접 확장
검색·플랫폼 필터, 랭킹, 상세, 두 종류의 404를 연결합니다. 요청 중 페이지를 떠났을 때 취소된 요청이 상태를 덮어쓰지 않는지도 확인합니다.
4단계: 스스로 확인
- 랭킹 방문 뒤 홈 배열 순서가 유지되나요?
- 320px와 키보드 탐색에서 모든 기능을 사용할 수 있나요?
- Network 404와 Console 오류가 최종 상태에 남지 않나요?
- lint와 build가 모두 성공하나요?
- README에 실행 방법·구조·기술 판단 두 가지가 있나요?
막혔을 때
| 증상 | 먼저 확인할 위치 | 해결 방향 |
|---|---|---|
| 카드가 계속 로딩 | Network와 useGames 상태 전이 | 응답 완료·예외·cleanup 경로를 분리합니다. |
| 랭킹 뒤 홈 순서 변경 | 정렬 호출 | 원본 sort 대신 새 배열을 반환하는 정렬을 사용합니다. |
| 상세 새로고침에서 없음 | Route param과 데이터 준비 시점 | 로딩 완료 뒤 id를 찾고 없음 상태를 구분합니다. |
| 배포에서 JSON 404 | Vite base와 데이터 URL | preview와 실제 base 기준 URL을 확인합니다. |
8. 이해 점검 질문 3개
- 잘못된 JSON 타입이 카드까지 전달됐습니다. API 함수·Hook·페이지 중 어느 경계에서 차단해야 하며 각 계층은 어떤 상태를 전달해야 하나요?
- 랭킹 페이지를 방문한 뒤 홈 카드 순서까지 바뀌었습니다. 어떤 배열 연산과 props 규칙을 확인해야 하나요?
- 개발 서버는 되지만 배포 preview에서 games.json이 404입니다. 어떤 URL·base·Network 증거를 순서대로 확인하겠나요?
9. 핵심 요약
React, TypeScript, fetch, CSS, Router를 하나의 데이터 흐름으로 통합했습니다. 정상 화면뿐 아니라 실패·모바일·접근성·빌드까지 검증하면 혼자서 API 기반 반응형 웹사이트를 완성할 수 있습니다.
최종 프로젝트: 게임 출시 정보·랭킹 대시보드 미니 퀴즈
선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.
학습을 마쳤나요?
직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.