클로드와 함께 방치형 게임 만들기: 1. 기술 스택 선정과 프로젝트 세팅
방치형 게임 '배달왕 키우기'를 Claude와 함께 만들기 시작한 첫 편. 1인 개발에서 빠른 프로토타이핑을 위해 Vite·React·TypeScript·Zustand 스택을 고른 이유와 프로젝트 세팅 과정을 정리한다.
들어가며
“배달왕 키우기”라는 방치형(Idle) 모바일 게임을 만들기로 결정했습니다. 유휴 수익화 모델로 광고 기반의 가벼운 게임이면 충분했습니다. 1인 개발에서 빠르게 프로토타입을 만들고 반복하기 위해 기술 스택부터 신중하게 선택해야 했습니다.
이 시리즈는 Claude AI와 협업하면서 게임을 처음부터 완성하는 과정을 기록합니다. 첫 번째 편은 “왜 이 기술들을 선택했는가”에 대한 고민입니다.
왜 웹 기술로 게임을 만드는가
게임 개발이라고 하면 대부분 Unity나 언리얼 엔진을 떠올립니다. 하지만 방치형 게임은 다릅니다.
후보 기술들
- Unity: 강력하지만 오버스펙입니다. 방치형은 복잡한 3D 렌더링도, 고급 물리 엔진도 필요 없습니다. 빌드 크기도 크고 학습곡선도 가파릅니다.
- React Native / Flutter: 네이티브 성능이 필요하면 좋지만, 앱스토어 심사, 플랫폼별 빌드 관리 같은 오버헤드가 있습니다. 광고 통합도 복잡해집니다.
- Godot / Phaser.js: 게임 엔진은 확실히 좋지만, 웹으로 배포할 때는 여전히 웹팩 같은 빌드 도구를 거쳐야 합니다.
웹 기술 선택의 이유
- 1인 개발 생산성: React와 TypeScript는 제 경험이 가장 깊은 스택입니다. 이미 알고 있는 도구로 프로토타입을 빠르게 만들 수 있습니다.
- PWA → 모바일 전환: 웹은 바로 모바일 웹으로 서빙되고, 필요하면 PWA로 “앱처럼” 동작하게 할 수 있습니다. React Native보다 마이그레이션이 훨씬 간단합니다.
- 방치형은 렌더링이 가볍다: 리소스 집약적인 애니메이션이나 물리 시뮬레이션이 없습니다. 주기적인 상태 업데이트와 UI 갱신만 하면 됩니다. 웹 성능으로 충분합니다.
- 개발 피드백 루프: Hot Module Replacement(HMR)로 코드 수정 후 1초 안에 결과를 봅니다. 빌드 시간이 거의 없습니다.
솔직한 단점
- 네이티브 성능: JS 엔진은 C/C++ 만큼 빠르지 않습니다. 하지만 방치형은 초당 수십 번의 복잡한 계산이 필요 없으므로 문제 없습니다.
- 앱스토어 심사: 웹 래퍼(Capacitor 같은)로 앱화하면 심사 이슈가 생길 수 있습니다. 다만 웹으로 먼저 런칭하고 필요하면 나중에 네이티브로 이식할 수 있습니다.
- 번들 크기: React는 gzip 후 ~40KB입니다. 네이티브 앱에 비하면 무겁지만, 웹에서는 표준입니다.
결론: 웹 기술은 방치형 게임과 1인 개발자의 생산성 사이의 최적의 교점입니다.
빌드 도구: Vite를 선택한 이유
프로젝트를 시작할 때 가장 먼저 결정해야 할 것이 빌드 도구입니다.
후보 비교
| 도구 | 장점 | 단점 |
| Webpack | 확장성, 커뮤니티 | 설정 복잡, 빌드 속도 느림 |
| Parcel | 설정 최소화 | 커뮤니티 상대적으로 작음 |
| Turbopack | 매우 빠름 | 아직 베타, 번들 분석 도구 부족 |
| esbuild | 빌드만 빠름 | 개발 서버 등은 직접 구성 |
| Rspack | 빠른 속도, Webpack 호환 | 한정된 플러그인 생태계 |
| Vite | ESM 기반 개발, 빠른 HMR, React + TS 템플릿 | - |
Vite를 선택한 이유
- ESM 기반 개발 서버: 번들링 없이 브라우저가 직접 ES 모듈을 로드합니다. 코드 수정 후 HMR이 정말 빠릅니다(보통 100ms 이내).
- React + TypeScript 템플릿: 이미 최적으로 설정된 템플릿이 있습니다.
npm create vite@latest -- --template react-ts로 끝입니다. - 프로덕션 빌드도 빠릅니다: Rollup을 기반으로 해서 코드 스플리팅, 트리 쉐이킹이 효과적입니다.
- 설정 최소화:
vite.config.ts는 수십 줄로 충분합니다. Webpack의 보일러플레이트는 필요 없습니다. - 생태계 성숙도: 2024년 기준 Vue, React, Svelte 등 대부분의 프론트엔드 프레임워크가 Vite를 기본으로 권장합니다.
다른 도구들도 충분하지만, Vite는 생산성과 성능의 최적 균형을 제공합니다.
상태 관리: Zustand가 필수였던 이유
방치형 게임의 가장 중요한 특성은 게임 로직이 UI와 독립적으로 동작해야 한다는 것입니다. 돈이 증가하는 로직, 탈것이 강화되는 로직, 시간이 흐르는 로직. 이 모든 것이 React 컴포넌트 외부에서 초당 60회, 혹은 백그라운드에서도 실행되어야 합니다.
상태 관리 도구 비교
| 도구 | 컴포넌트 외부 접근 | 렌더링 최적화 | 번들 크기 | 게임 루프 적합성 |
| Redux Toolkit | ✓ (getState) | ✓ | ~20KB | 보일러플레이트 많음 |
| React Context | ✗ | ✗ (전체 리렌더) | 0KB | 게임 루프에 부적합 |
| MobX | ✓ | ✓ | ~15KB | 복잡한 옵저버 패턴 |
| Valtio | ✓ | ✓ | ~4KB | 프록시 기반, 직관적 |
| Jotai | ✓ | ✓ | ~8KB | 원자적 상태 관리 |
| Zustand | ✓ (getState) | ✓ (selector) | ~3KB | 최적 |
Zustand 선택의 핵심 이유
// 게임 루프에서 컴포넌트 없이 상태 업데이트
const gameLoop = () => {
const { money, addMoney } = useGameStore.getState();
addMoney(money * 0.01); // 매 프레임 1% 증가
};
- 컴포넌트 외부 접근 가능:
getState()로 언제든 현재 상태를 읽고 액션을 호출할 수 있습니다. Redux도 가능하지만 Zustand가 훨씬 간단합니다. - 자동 렌더링 최적화: selector를 사용하면 필요한 부분만 구독합니다.
// money 변경시만 리렌더
const money = useGameStore((state) => state.money);
- 보일러플레이트 최소: Redux는 reducer, action, dispatch 등 5개 이상의 개념을 배워야 합니다. Zustand는 2개: 상태와 액션.
- 번들 크기: ~3KB로 매우 가볍습니다. 방치형 게임에서는 모든 KB가 중요합니다(특히 모바일).
- 게임 데이터 저장과 복원: JSON.stringify(useGameStore.getState())로 전체 상태를 저장하고 복원하기가 간단합니다.
Redux가 오버킬인 이유
Redux Toolkit은 훌륭하지만, 미들웨어, devtools, action creator 등의 개념이 게임 루프의 간단한 상태 업데이트에는 오버엔지니어링입니다.
React Context가 게임 루프에 부적합한 이유
Context는 값이 변경되면 모든 구독 컴포넌트가 리렌더링됩니다. 게임이 초당 60회 상태를 업데이트할 때 그 모든 순간마다 컴포넌트 리렌더링을 재귀적으로 트리거합니다. 최악의 성능 안티패턴입니다.
스타일링: Tailwind CSS의 실용성
방치형 게임의 UI는 특별합니다. 매우 단순하고, 같은 구조의 카드(탈것 목록)와 버튼이 반복됩니다.
스타일링 도구 비교
| 도구 | 개발 속도 | 번들 크기 | CSS 파일 관리 | 동적 스타일 |
| Vanilla CSS | 느림 | 작음 | 복잡함 | 번거로움 |
| CSS Modules | 중간 | 작음 | 각 컴포넌트마다 관리 | 자유로움 |
| styled-components | 빠름 | 크다(런타임) | JS 안에 CSS | 매우 자유로움 |
| Emotion | 빠름 | 중간(런타임) | JS 안에 CSS | 매우 자유로움 |
| UnoCSS | 빠름 | 매우 작음 | 빠른 런타임 | 제한적 |
| Tailwind CSS | 매우 빠름 | 작음(정적) | 없음 | 제한적 |
Tailwind 선택 이유
- 유틸리티 클래스로 빠른 프로토타이핑: UI 목업을 만들 때 CSS 파일을 왔다 갔다 할 필요가 없습니다.
// 따로 CSS 파일 안 만들어도 됨
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"> 업그레이드 </button>
- 번들 최적화: 빌드 시점에 사용하지 않는 클래스는 제거됩니다(purge). 결과 파일은 매우 작습니다.
- 일관된 디자인 시스템: 색상, 간격, 폰트 크기 등이 미리 정의되어 있습니다. 게임은 일관된 룩앤필이 중요한데, Tailwind는 이를 강제합니다.
- 반응형 설계: md:, lg: 같은 접두사로 모바일 우선 설계가 자연스럽습니다.
정직한 한계
- 복잡한 레이아웃: grid나 flex를 많이 조합하면 HTML이 복잡해집니다. 하지만 게임 UI는 단순하므로 문제 없습니다.
- 동적 스타일: 런타임에 색상을 동적으로 바꾸려면 CSS 변수나 인라인 스타일을 섞어야 합니다. 하지만 게임의 색상은 고정적입니다.
결론: 방치형 게임의 UI 특성(단순, 반복적, 고정적)에는 Tailwind가 완벽하게 맞습니다.
TypeScript: 게임 데이터의 안전성
방치형 게임은 데이터가 중심입니다. 탈것(자전거, 스쿠터, 오토바이)의 스펙, 강화 공식, 경제 밸런스, 모두 프로그래밍되어야 할 데이터입니다.
TypeScript의 핵심 가치
- 게임 데이터 구조의 타입 안전성
interface Bike {
id: string;
name: string;
baseSpeed: number; // km/h
costToPurchase: number; // cost in game money
level: number; // current level
enhancement: number; // 강화 단계
}
// 이 구조를 코드 전체에서 강제할 수 있다
- 리팩토링 용이성: 게임이 성장하면서 “탈것에 ‘연료 효율성’ 속성을 추가하자”는 결정을 할 때, 타입스크립트는 그 속성을 사용해야 할 곳을 모두 찾아냅니다.
- AI와의 협업 가치: Claude AI가 코드를 생성할 때, 타입 정보가 있으면 정확도가 훨씬 높습니다. “이 함수는
Bike배열을 받아서Bike를 반환한다”는 정보가 있으면 AI는 실수할 여지가 줄어듭니다. - 자기 문서화: 코드를 읽을 때
addBike(bike: Bike): void라는 서명만 봐도 무엇을 하는지 알 수 있습니다.
번들 크기의 관점
TypeScript는 프로덕션에 포함되지 않습니다. Vite가 트랜스파일할 때 타입은 제거되고 순수 JavaScript만 남습니다. 즉, 비용 없이 안전성을 얻습니다.
프로젝트 구조 설계
게임을 만들 때 가장 중요한 원칙은 게임 로직과 UI 분리입니다.
src/
├── game/ # 게임 로직 (React 무관)
│ ├── bikes/ # 탈것 데이터, 규칙
│ ├── gameLoop/ # 시간 흐름, 자동 업데이트
│ ├── economy/ # 돈, 비용 공식
│ ├── prestige/ # 초기화 보상 시스템
│ └── constants.ts # 모든 게임 상수 (UPPER_SNAKE_CASE)
│
├── store/ # Zustand 상태 관리
│ ├── gameStore.ts # 게임 상태 + 액션
│ └── uiStore.ts # UI 상태 (탭 선택 등)
│
├── components/ # React UI 컴포넌트
│ ├── BikeCard.tsx # 탈것 카드
│ ├── GameScreen.tsx # 메인 화면
│ └── ...
│
├── hooks/ # 커스텀 훅
│ ├── useGameLoop.ts # 게임 루프 시작/정지
│ └── useLocalStorage.ts # 저장/복원
│
├── ads/ # 광고 통합
│ └── adManager.ts # Google AdMob 등
│
└── main.tsx # 엔트리포인트
각 계층의 책임
- game/: 순수 로직. React 임포트 없음. 단순히 데이터와 함수.
- store/: 게임 상태의 단일 진실 공급원. Zustand로 관리.
- components/: store를 구독하고 렌더링만 합니다.
- hooks/: store와 components 사이의 다리. useGameLoop처럼 게임 루프를 관리합니다.
이렇게 분리하면, 나중에 게임 로직을 다른 플랫폼(React Native, CLI)으로 이식할 때도 game/ 폴더는 그대로 쓸 수 있습니다.
Claude AI와의 협업
이 프로젝트는 Claude AI(구체적으로는 oh-my-claudecode)와 함께 진행하기로 결정했습니다.
세팅 과정에서 얻은 것
- 타입 정의의 정확성: AI에게 “Bike 인터페이스를 정의해줘, 속성은…“이라고 말하면, 즉시 올바른 TypeScript 코드를 생성합니다.
- 구조적 조언: “게임 로직과 UI를 분리하려면 어떻게 해야 해?”라는 질문에 명확한 폴더 구조와 의존성 관계를 제시받습니다.
- 빠른 피드백: 코드를 작성한 후 “이 부분이 성능 문제를 일으킬까?”라고 물으면 즉시 답변을 얻습니다.
- 보일러플레이트 자동화: Zustand store, TypeScript 타입, Tailwind 컴포넌트의 기본 구조를 순식간에 생성받습니다.
이 시리즈의 각 편은 “내가 AI와 함께 어떻게 구현했는가”를 기록하는 형태가 될 것입니다.
다음 편: Zustand Store와 게임 루프
프로젝트 세팅이 끝났으니, 이제 게임의 핵심인 상태 관리와 시간 루프를 구현합니다.
다음 편에서 다룰 내용
- gameStore 구조: money, bikes, prestige 상태와 업데이트 액션
- 게임 루프 시작: requestAnimationFrame으로 초당 60회 tick
- 자동 수익: 보유한 탈것이 자동으로 돈을 버는 로직
- 저장/복원: localStorage에 게임 상태를 주기적으로 저장
맺으며
“배달왕 키우기”는 겉보기에 단순한 방치형 게임이지만, 기술 선택부터는 신중했습니다.
- 웹 기술로 빠른 개발과 모바일 배포를 동시에.
- Vite로 개발 환경의 피드백 루프를 최소화.
- Zustand로 게임 로직과 UI의 명확한 분리.
- Tailwind로 프로토타이핑 속도를 극대화.
- TypeScript로 복잡한 게임 데이터의 안전성 확보.
이 조합은 1인 개발자가 빠르게 움직이면서도, AI와의 협업으로 품질을 유지하는 데 최적화되어 있습니다. 프로젝트 세팅은 이미 완료되었고, 이제 게임 로직을 구현할 차례입니다. Claude와의 다음 대화에서는 Zustand store와 게임 루프를 함께 만듭니다.