구글이 4월 6일 iOS 앱스토어에 조용히 앱 하나를 올렸다. 블로그 글도, 보도자료도, 트위터 공지도 없었다. 이름은 Google AI Edge Eloquent. 발견은 미국 테크 미디어들이 먼저 했고, 사흘 뒤 Neowin·TechCrunch·9to5Google이 일제히 기사를 냈다.
핵심은 “오프라인에서 동작하는 받아쓰기”
Eloquent는 구글의 온디바이스 모델 라인업인 Gemma를 탑재한 음성 인식 앱이다. 경쟁 구도를 보면 이렇게 정리된다.
iOS 기본 받아쓰기: 빠르지만 정확도·후처리 약함
OpenAI Whisper 기반 앱들: 정확도 높음, 대부분 클라우드 전송
Microsoft MAI-Transcribe-1: API 기반, 서버 의존
Google AI Edge Eloquent: 완전 오프라인 가능, 음성이 기기를 떠나지 않음
오프라인 모드에서는 Gemma ASR 모델을 다운받아 기기 안에서 처리한다. 클라우드 모드를 켜면 음성 인식은 여전히 기기에서 시작되고, 텍스트 정리만 Gemini에 위임한다. “내 음성 데이터가 어디로 가는가”에 예민한 사용자에겐 설계 자체가 메시지다.
실제로 써보면 눈에 띄는 것
기사들이 공통으로 언급하는 포인트는 세 가지다.
첫째, 필러 제거. “음…”, “어…”, 말하다 멈춘 자기 교정이 자동으로 빠진다. 회의 받아쓰기용으로 특히 강한 기능이다.
둘째, 한 번의 녹음으로 여러 형식 전환. 받아쓴 초안 아래 Key points, Formal, Short, Long 버튼이 뜬다. 길게 말한 걸 요점 세 줄로, 혹은 반말을 존댓말 공문으로 바꾸는 게 한 번의 탭이다.
셋째, 무료. 구글 계정만 있으면 된다. Whisper API를 쓰던 유료 앱들의 가격 포지션이 어색해진다.
Android 먼저가 아닌 이유
구글이 Android 앱을 먼저 내지 않은 건 이례적이다. 앱스토어 설명에는 Android 버전 언급이 있어서 결국 나오긴 할 테지만, 순서가 뒤집힌 건 사실상 iOS 사용자 사이 “AI 받아쓰기” 포지셔닝을 선점하려는 움직임으로 읽힌다. iOS 기본 받아쓰기의 점유율을 Gemma 모델이 갉아먹는 그림을 노린 것.
작지만 중요한 디테일: 제품 이름이 “Google AI Edge”로 시작한다는 점. “Edge”는 구글이 밀고 있는 온디바이스 AI 플랫폼 브랜드다. 이 앱은 단독 제품이 아니라 구글의 온디바이스 전략 쇼케이스 성격이 강하다.
So What?! — 한국 사용자·직장인 기준
한국어 인식 수준은 직접 써봐야 할 영역이지만, 기존 Gemma 모델이 한국어 대응을 포함하고 있어 기본기는 나쁘지 않다. 실전에서 체감될 포인트는 두 가지다.
보안 민감 업무 — 법무·의료·금융 현장 메모는 클라우드 전송이 부담스럽다. 오프라인 모드가 이 공백을 정확히 메운다. “기기 비행기 모드에서 받아쓰기”가 실사용 시나리오가 된다.
회의록 워크플로우 — Granola·Otter 같은 회의록 앱을 쓰는 사람에게 Eloquent는 대체재가 아닌 프런트엔드로 쓰일 수 있다. 현장에서 Eloquent로 정제된 초안을 뽑고, 이후 긴 요약은 Claude/ChatGPT에 맡기는 조합.
지금 바로 할 수 있는 것
앱 설치: iOS 앱스토어에서 “Google AI Edge Eloquent” 검색. 한국 계정에서도 다운로드 가능. 오프라인 모델은 처음 실행 시 내려받는다.
한국어 테스트: 3분 정도 일상 대화를 녹음해 한국어 필러(“어”, “그”) 제거가 얼마나 정확한지 직접 확인해본다. 팀에 공유할 판단 근거가 된다.
민감 데이터 워크플로우 점검: 지금 쓰는 받아쓰기/회의록 앱이 클라우드 기반이라면, Eloquent 오프라인 모드로 전환 가능한 케이스(상담 녹취, 법률 메모 등)가 있는지 따져본다.
Anthropic이 4월 8일(현지시간) Claude Managed Agents 공개 베타를 공개했다. 한 줄로 요약하면 “에이전트의 뇌는 당신이 짜고, 손발(infra)은 Anthropic이 맡는다”는 제품이다. 샌드박스, 장기 세션, 권한, 툴 실행, 트레이싱까지 한 통으로 묶어서 판다. 직접 harness 만들어 본 팀이면 “아, 결국 이렇게 가는구나” 싶은 발표다.
뭘 대신 해주는가
기존에는 에이전트 하나 돌리려면 Docker로 sandbox 격리하고, Redis로 상태 저장하고, credential vault 붙이고, 끊겼을 때 재시작까지 직접 구현해야 했다. Managed Agents는 이걸 그대로 대체한다.
보안 샌드박스에서 코드 실행·파일 읽기·웹 브라우징
몇 시간 이어지는 long-running 세션, 연결이 끊겨도 진행 상태 복원
자격증명·시크릿 저장, 툴마다 권한 스코프 지정
end-to-end tracing으로 “어느 단계에서 엉켰는지” 확인 가능
에이전트 정의는 자연어 프롬프트 혹은 YAML 파일 두 가지 방식이다. 콘솔, Claude Code, 신규 CLI에서 모두 띄울 수 있다.
가격 — 싸 보이지만 계산이 필요하다
공식 가격은 세션 러닝 시간당 $0.08. 여기에 Claude 모델 토큰 비용이 별도로 붙는다. 중요한 건 과금 기준이 “wall-clock”이 아니라 실제 실행 시간이라는 점이다. 사용자 응답 기다리거나 tool confirmation 대기 중인 시간은 빠진다.
24시간 내내 돌리는 에이전트면 런타임만 월 약 $58. 문제는 토큰이다. Opus 4.6을 장시간 세션에 쓰면 토큰 비용이 런타임의 10~20배를 가볍게 넘긴다. “$0.08이라니 싸네”에 속으면 안 되고, 실제로는 Sonnet을 언제 쓰고 Opus를 언제 꺼낼지가 비용을 좌우한다.
초기 고객, 그리고 Anthropic이 노리는 시장
Notion, 라쿠텐, Asana가 이미 제품에 통합했다고 밝혔다. 셋 다 “우리 앱 안에서 백그라운드로 일하는 AI”를 필요로 하는 SaaS다. Anthropic의 그림은 분명하다 — 에이전트 런타임을 AWS Lambda 같은 공용 인프라로 만들어, OpenAI Codex가 IDE 안쪽을 잡는 사이 뒤쪽 서버 레이어를 선점하는 것.
발표 직후인 4월 10~11일, SaaS 종목이 흔들렸다. 24/7 Wall St.는 “Anthropic이 소프트웨어 산업 전체를 다시 투자 불가로 만들 수 있다”고 썼다. 과장이지만, 투자자들이 왜 긴장했는지는 이해된다. 기존 SaaS의 UI-구독 모델 대신 “내 워크플로우 대신 돌아가는 에이전트 + 러닝 타임 과금”이 표준이 되면 그림이 완전히 달라진다.
So What?! — 한국 개발자·기술리더에게
세 가지로 정리된다.
직접 harness 만들던 팀 — 프로덕션 가기 전 “세션 유지·권한·샌드박스” 부분은 이제 만들지 말고 사보는 게 맞다. 만들어도 대부분 Anthropic 스펙을 재현하게 된다.
SaaS 기획·운영 쪽 — 구독 모델에 “AI 에이전트가 대신 실행하는 시간”을 과금 단위로 붙이는 실험이 곧 퍼진다. Notion이 이미 안 한다고 못 박지도 않았다.
비용 판단 — 베타 가격 $0.08/hour는 미끼다. 실비용은 토큰이 결정하고, 토큰 선택은 모델 라우팅 설계 문제다. “언제 Sonnet, 언제 Opus”에 대한 회사 기준이 없다면 지금 만들 타이밍.
이번 달 들어 내 노트북에서 일어난 일이 좀 이상하다. Cursor 3 창을 띄우고, 그 안에서 Claude Code 터미널을 열고, Claude Code 안에서 OpenAI Codex 플러그인이 코드 리뷰를 돌린다. 한 화면에 세 회사의 에이전트가 동시에 일하고 있다.
2026년 4월, 누가 의도한 적도 없는 새 AI 코딩 스택이 그냥 모양을 잡아 가고 있다. The New Stack이 4월 초 기사로 정리한 바로 그 흐름이다. 일주일 정도 본격적으로 써 보고, 무엇이 새롭고 무엇이 과대평가인지 정리한다.
한 줄 그림: 오케스트레이션 / 실행 / 리뷰
각 툴이 같은 일을 두고 싸우던 시대가 끝났다. 지금은 역할이 갈린다.
레이어
대표 도구
역할
오케스트레이션
Cursor 3 (agent-first UI)
여러 에이전트·여러 작업의 병렬 진행 관리
실행 (의사결정·구현)
Claude Code
주력 코드 작성·리팩토링·테스트
리뷰·대조
OpenAI Codex (Claude Code 플러그인으로)
독립 코드 리뷰, 적대적 검증(Adversarial), 백그라운드 태스크 위임
이 그림에서 핵심 사건은 OpenAI가 자기 Codex CLI를 Anthropic Claude Code의 플러그인 형태로 출시한 것이다. 시장 1위가 Claude Code라는 걸 OpenAI가 인정한 셈이다. 사용자가 Codex로 옮겨오길 기다리는 대신, Codex를 사용자가 있는 곳으로 보냈다.
실제로 써 보면 — 워크플로우 한 사이클
내 평일 작업 패턴이 이렇게 바뀌었다.
Cursor 3에서 작업 분할. “API 리팩토링”, “테스트 보강”, “마이그레이션 스크립트” 세 가지를 병렬 에이전트로 띄운다. 옛날엔 한 번에 하나만 돌렸는데, 이제 세 개가 동시에 돈다.
각 에이전트는 Claude Code로 실행. 코드 변경의 80%는 여기서 일어난다. Claude의 컨텍스트 처리가 가장 안정적이다.
변경이 끝나면 Codex 플러그인을 호출해서 리뷰. “이 PR을 적대적으로 검토해라”라고 시키면, 자기가 작성하지 않은 코드라 더 인색하게 본다. 사람 시니어 한 명을 옆에 앉힌 효과가 난다.
리뷰 결과를 다시 Claude Code가 반영. 한 사이클 끝.
가장 효과가 컸던 건 “같은 모델이 작성도 하고 리뷰도 하면 똑같은 사각지대를 본다”라는 오래된 문제를 해소한 점이다. Codex가 Claude의 코드를 보면, 둘이 학습 데이터·정렬 방식이 달라서 잡아내는 결함이 다르다. 일주일 사용 기준, Codex 리뷰가 Claude 자기 리뷰가 놓친 결함을 의미 있는 빈도로 잡아냈다.
과대평가된 부분 — 솔직히
마케팅 글에서 빠지는 얘기 셋.
① 비용이 합산된다. Cursor 구독 + Anthropic Max + OpenAI API. 한 명 개인 개발자 기준 월 $80~$200 사이에서 시작한다. 회사 카드면 모르겠지만, 사이드 프로젝트만 하는 사람에겐 빡빡하다.
② “병렬 에이전트”는 자유의 대가가 있다. 세 개를 동시에 돌리면 충돌이 난다. 같은 파일을 두 에이전트가 건드리는 일이 1주일에 두세 번 있었다. Cursor 3가 잠금·머지 UX를 더 다듬어야 한다.
③ 플러그인 호출은 아직 거칠다. Codex 플러그인이 가끔 응답을 길게 잡아먹는다. 빠른 리뷰를 원하면 별도 터미널에서 Codex CLI 직접 호출이 더 빠를 때가 있다.
그래서 — 한국 개발자에게 달라지는 것
“어느 툴 하나로 정착할까”라는 질문은 이제 틀렸다. 1년 전엔 그게 맞는 질문이었지만 2026년 4월의 답은 “세 툴을 역할로 나눠 쓰는 워크플로우를 본인이 정의해야 한다”이다. 회사·팀·개인 단위로 그 조합이 다르다.
지금 바로 할 수 있는 것
Claude Code 사용자라면 OpenAI Codex 플러그인을 일주일 깔아 본다. “이 PR을 adversarial하게 리뷰해 줘” 한 줄만 시켜 봐도 효용이 보인다.
Cursor 1.x 쓰던 사람이라면 Cursor 3의 병렬 에이전트 모드를 작은 작업 2개로 시범 운영해 본다. 처음부터 3개는 충돌만 늘어난다.
팀이라면 “어느 레이어를 표준화할지”부터 합의한다. 모두 자유롭게 골라 쓰면 PR 리뷰 품질이 들쑥날쑥해진다.
Perplexity가 4월 9일 던진 한 줄. “8주 안에 유니콘으로 갈 수 있는 회사를 만들어 봐라. 우리가 최대 200만 달러를 줄게.”
이름은 Billion Dollar Build. 자기네 에이전트 제품인 Perplexity Computer를 써서 회사를 만드는 8주짜리 컴페티션이다. 한국 1인 빌더 입장에서 정확히 어떤 의미인지, 그리고 못 본 척 넘어가면 안 되는 이유를 정리한다.
대회 한 줄 정리
항목
내용
기간
8주
상금
최대 $1M 시드(최대 3팀에 분배) + $1M Computer 크레딧
자격
미국 거주자 18세+ / 개인 또는 2인 팀
전제 조건
2026년 4월 13일 PT 자정까지 Perplexity Max 또는 Pro 구독
등록 시작
4월 14일
제출 마감
6월 2일 (데모 영상 + 트랙션 지표 포함)
피칭 → 발표
6월 9일 라이브 피칭 → 6월 10일 우승팀 발표
표면만 보면 깔끔한 액셀러레이터다. 그런데 약관을 읽으면 두 줄이 눈에 박힌다.
읽어야 하는 두 가지 캐치
캐치 1. 투자는 “약속”이 아니다. Perplexity Fund는 어떤 참가자에게도 투자할 의무가 없다고 약관에 명시돼 있다. 즉 우승해도 시드 $1M이 자동으로 들어오는 게 아니다. 별도 듀딜리전스 통과가 조건이다.
캐치 2. 라이선스 범위가 매우 넓다. 참가만 해도 Perplexity는 회사명·제품·코드·컨셉·워크플로우·창업자 본인 모습·데모 영상 일체를 마케팅·IR·기타 상업적 목적으로 무료·전세계 영구 사용할 권리를 가져간다. “참가비 0원”의 진짜 가격이다.
왜 200만 달러를 거는가
Perplexity는 지금 검색 트래픽으론 구글을 못 잡는다. 그 대신 “AI 에이전트가 대신 일해 주는 다음 시대의 OS는 우리”라는 내러티브를 사고 싶다. 그래서 Computer라는 제품을 사람들이 실제로 쓰면서 회사를 만드는 그림이 필요하다.
유니콘 후보 3개를 8주 안에 양산해 낸다면, 그건 Perplexity의 다음 시리즈 라운드 피치덱 첫 페이지다. 200만 달러는 마케팅 예산 치고 싸다.
한국에서 못 들어가는데, 왜 봐야 하나
한국 거주 빌더는 직접 참가 못 한다. 그래도 챙겨야 할 이유 셋.
① Perplexity Computer의 진짜 한계가 8주 안에 공개된다. 200만 달러를 걸고 외부인이 두들기는 만큼, 6월 라이브 피칭 회차는 이 제품의 실전 능력을 가장 정직하게 볼 수 있는 무대다. 한국에서 같은 영역(Manus·Genspark·OpenClaw 등)을 검토 중이라면 6월 10일 발표 직후가 비교의 적기다.
② “8주 유니콘”이라는 작업 단위가 새 표준이 된다. 6개월 MVP가 아니다. 8주에 데모 영상 + 트랙션 지표까지 만들어 와야 한다. AI 시대의 빌더 사이클이 분기 단위에서 8주 단위로 짧아지고 있다는 신호다.
③ “광범위 라이선스 약관”이 곧 한국 대회·해커톤에도 온다. 한국에서도 카카오·네이버·통신사 주최 AI 해커톤이 줄줄이 예정돼 있다. 약관 읽는 습관, 지금부터 들이는 게 좋다.
지금 바로 할 수 있는 것
본인 사이드 프로젝트를 “8주 유니콘” 프레임으로 다시 적어 본다. 무슨 시장, 무슨 트랙션 지표, 8주 안에 어디까지. 못 적어 내려가면 그건 좋은 신호다 — 대상이 너무 넓다는 뜻이다.
Perplexity Pro/Max를 안 써 봤다면 이번 주 한 달 무료 또는 학생 플랜으로 Computer 기능을 직접 만져 본다. 6월 라이브 피칭 영상을 그냥 보는 사람과, 만져 본 채로 보는 사람은 흡수율이 다르다.
참가하는 미국 친구가 있다면, 한국 시장 진출 자문 역할로 묻어 들어가는 것도 옵션이다. 2인 팀 허용이라 가능성 있다. 나도 좀… 🙂 !!!
This is the final Part 3 of the series. In Part 1, I built a game in five hours with AI and got rejected by the App Store. In Part 2, I passed review and reached the first penny. Part 3 is the story after launch — marketing, a new tool I started building, and two more experiments already underway.
Day 3 After Launch, I Opened Facebook
I had no illusion that people would just show up after launch, but the first three days were still a bit lonely. Almost no downloads. Instinctively, I posted on Facebook and LinkedIn. Then I opened a Claude chat and asked a simple question — how do I get more downloads for a small game like this? Several answers came back. One of them was that some indie game community contact pages accept app review requests.
Now I send one or two emails a day. At first, it was honestly embarrassing and awkward. I learned that sending a cold email like this was the smallest form of putting down the VP title. But after a few days, a different thought hit me — why didn’t I think of this sooner?
Another Scene from a Decade Ago
When LINE Bubble was hitting tens of millions of downloads in Japan, I personally visited Japanese gaming communities every single day. I watched what was being posted, what people were saying, which games might blow up next. I thought that was a PD’s job. Over a decade later, I realized I was doing the exact same thing for my own side project — right there on an indie community contact page. Tools change, eras change, but the fact that the maker has to walk to where the users are — that stays the same.
YouTube Shorts, and Then I Started Building a Tool
Another method Claude suggested was YouTube Shorts. Unlike other channels, you see real-time reactions the moment you upload. For someone like me who can’t wait, it was a strangely good fit.
Then something else started. I began building a tool to make Shorts videos more easily and efficiently. I’m currently working on about 10 apps and services simultaneously, and I’m designing this tool as a shared module across all of them. The functionality is simple — record a short video of an app running on a real device, edit it, add sound, create subtitles, all driven by LLM prompts. To be clear, this is not a “content auto-generation” tool. It captures and processes real screens from real apps I built. The concept isn’t generating video — it’s prompting an LLM to capture the most compelling scenes of the game on video. This is why I’m building all game scenes in a hybrid architecture.
The fact that I was now building a tool that uses AI, while using AI — that was the most interesting change from a senior’s perspective.
HOL4B.com — In the Meantime, I’ll Focus on This
I should be honest about one thing. I’m currently looking for a job. Ideally, I want to continue in a C-level role at an enterprise company. I don’t know how long that will take. So in the meantime, I decided to give this a real shot. HOL4B.com is the result of that decision. It’s still a mostly empty single-page site, but the plan is to add one line at a time for each product I build going forward. A small workshop for my personal brand.
Version 1.3 and Two More Experiments
Trains Out is gearing up for 1.3.0. The biggest change is on the business side — registering as a business entity and adding a paid ad-removal item. The first in-app purchase button will sit next to the one-penny ad revenue. Two smaller additions: a leaderboard and daily challenges for motivation. I’ll also try to improve the core game feel and rules as much as possible.
And there are two more experiments that have already started.
Gyeol (결 Seoul) — A camera app. I’ve always liked photography, and this time I’m bundling my 10 favorite filters into a camera app. But the core of this app is the 11th filter — an experimental filter that extracts the dominant color of a photo and layers it back over the image. I don’t know if the results will be worth using, but that’s the whole point of a side project. The app is already built, and I’ve been walking around Seoul taking photos to test it in the real world.
Spell Stack — A word-merging game. My kid wanted an app for learning words, so I took that request and layered it onto a 2048-style number-merging mechanic. Instead of numbers, letters combine to form words. Just like Trains Out where my family was the first user, the first user for this one is already decided. It includes grade-level vocabulary curated from public word lists.
So — What Did AI Become for a Senior Developer?
The question I asked at the start of this series was simple. Can AI really code? Can it go all the way to commercialization? After about ten days, one game, and one penny, here’s my answer.
Yes, it’s possible. But AI can’t finish the job alone. Policy reviews (ATT, AdMob child ad ratings), game feel, self-critique, ad appropriateness, marketing that requires walking to where users are — in every one of these areas, a human had to step in repeatedly. But when that human is a 25-year veteran, AI works at a terrifying speed. I could even delegate tool selection, and it still produced results. Working on 10 apps simultaneously, I genuinely feel the power of AI.
There’s a phrase I keep hearing lately. “One-click builders” or “I didn’t touch a single line of code.” That era will come eventually. But having spent ten days living it firsthand, my conclusion is different. Right now, AI is the event of a really good partner appearing for senior developers. Not a tool that replaces someone — a partner that multiplies the judgment seniors have built over a lifetime. I’ve shipped LINE Bubble to #1 in eight countries, and worked across commerce, advertising, mobility, and manufacturing. All those memories are coming alive again through this five hours, this first penny, and these next two experiments. I’ve come to believe again — starting with myself — that seasoned experience isn’t something to be leveled away, but the thing that holds down the far end of the AI era.
This three-part series closes here. Trains Out is still just the beginning. Gyeol and Spell Stack are coming soon. Download links and short gameplay videos are collected on the English launch page.
What You Can Do Right Now
In the AI era, walk to where your users are. Indie communities, small Facebook groups, company Slack channels — AI won’t take that one step for you.
Run one side project seriously. You’ll see what AI multiplies for you and what still needs your own hands.
This is Part 2 of the series. Part 1 covered building a game in five hours and getting rejected by the App Store. Part 2 picks up from that rejection — introducing QA for the first time, passing the second review, and earning the first penny. And the strange question that penny raised.
One Rejection Email Made Me Stop
Version 1.0 was rejected for two reasons. First, an ATT handling bug. The second was more embarrassing — level 11 out of the 50 I’d built was unsolvable. The reviewer had tried it themselves on an iPad and couldn’t clear it. They included a polite explanation. They also asked me to record a video of ATT working on a real device for the re-review. As rejections go, it was a warm one.
Working on 1.1, I thought about “QA” for the first time. With 1.0, I’d honestly thought — what’s the point of QA at this scale? I remember exactly where that arrogance cracked. Once I started carefully checking the two rejection reasons, other issues started appearing one after another. About three more hours went into it.
QA Is Where I Started Seeing the Game
The first thing I noticed was humbling. The game was too easy. “Why would anyone play this?” — I asked myself that out loud. I seriously considered scrapping the whole thing.
Then I calmed down and looked again. With a clear head, small things became visible. The movement of a single finger, the hook of the first 30 seconds, that vague thing people call game feel. It felt like a senior game PD’s instincts waking up after a decade. If you ask me today whether this game is actually fun, I’d need more than a sentence to answer honestly. But in that short time, a lot changed — including one major pivot in direction.
I resubmitted 1.1. The review took two days. The code took five hours, but human review time was 9.6 times that. I thought that ratio perfectly captured the asymmetry of the AI era.
The approval email arrived.
After Launch: “I Made This Too Carelessly”
The joy of approval was short-lived. The day 1.1 went live on the App Store, I opened the game on my own phone and my immediate thought was — even for an experiment, this is too sloppy.
I spent two days polishing. I’d added a system where watching an ad gives you two extra lives, but some of those ads were surprisingly inappropriate. When I opened the AdMob console, a completely different interface from a decade ago was waiting for me. Lots of settings, complex policies — more time gone.
After all the fixes, the ads became kid-friendly enough for ages 4 to 9. That one sentence is the summary of the 1.2 update.
The First $0.01
At some point after 1.2 went live, the number on the AdMob dashboard changed from 0 to 0.01.
The person who once built LINE Bubble to #1 on the App Store in eight countries — their first revenue over a decade later was one penny.
Here’s what that penny really was. The first users were my wife and kid. Right after launch, the two of them opened the game on their phones, and their ad views became that one penny. As of this writing, about four days after launch, the total is just over $10. There are barely any users. That’s the honest number.
A Déjà Vu from a Decade Ago
When I was working as a VP, CTO, CISO, CPO — I was making big decisions and setting direction for hundreds of people. Now it’s different. I work alone, thinking only about myself. The weight is lighter. For the first time, I can think on my own terms, and that means I can do what I actually want to do.
I only realized that was the real motivation behind this side project when I saw that first penny.
And strangely, a scene from the LINE Bubble days came back to me. Back then, there was this unofficial challenge among us — “who can clone a hit American game the fastest.” When Don’t Touch the Spikes was trending in the US, I cloned it in 2–3 hours without any AI help.
Over a decade later, I did the same kind of thing in five hours with AI beside me. Same kind of work, but not the same. Back then, the 2–3 hours were because my hands were fast. This time, the five hours were because the tool was fast — and pulling that tool all the way to the finish line still required the instincts from when my hands were the fast part.
So What Changed
Part 2’s conclusion is short. AI can code in this era, but commercialization is a different story. Policies, game feel, ad appropriateness, self-critique — none of these could be handled by AI alone. But when the human in the loop is a 25-year veteran, AI works at a terrifying speed.
Part 3 continues with what came after launch — marketing, HOL4B.com, and the next experiments that have already begun.
What You Can Do Right Now
Always run at least one cycle of human QA on your AI-built first version. Even if it took five hours to build, spend one hour manually tapping through every screen. Seniors are especially prone to skipping this step. And don’t bolt on AdMob at the end — review the ad policies from the start. They’ve changed completely from a decade ago. If your target audience includes children, you need to manually restrict ad ratings.
This is Part 1 of the series. A developer with 25 years of experience took a brief career break and built an iOS game as a side project — testing one question firsthand: “Can AI really code? Can it go all the way to commercialization?” Part 1 covers the story from starting to build the game to receiving the first App Store rejection.
https://www.hol4b.com
Instead of Meetings, I Opened Claude
After finishing my tenure as CTO / Managing Director at an EdTech company, I found myself with a short gap. While figuring out what to do next, what I reached for wasn’t a résumé — it was Claude.
It had been 25 years. No executive meetings, no org charts, no quarterly KPIs. I barely remembered what it felt like to write code in that kind of space. So I just opened it.
I Barely Used Any Other Tools
The first few days were warm-up. Getting a feel for how far AI coding had come — how to write prompts, how to manage context, what a harness even is. I installed and tried everything: GSD, gstack, PaperClip, OpenClaw. Kept only what fit my hands.
Right before this, I had been designing and running AX-related agents at work, but using AI as an individual developer is a completely different scope and perspective.
What survived was two things. gstack for strategy and planning, Claude (both Chat and Code) for everything else. React Native, Expo, AdMob — I didn’t pick these by comparing options. AI suggested them and I said OK.
The most awkward moment for a 25-year developer was right here — the realization that even tool selection could be delegated.
I’ve since set up local CI/CD on my MacBook, but early on, if Claude said it needed something, I just gave it. That’s how I ended up paying $19 for the EAS starter kit.
One aside. While using gstack, a YCombinator recruiting message pops up at some point. Offering a great tool for free while eyeing its users as hiring candidates — I found that impressive. In the AI era, talent discovery runs on usage traces, not résumés. I jotted down a short note. I sent a reply thanking them for the offer, though I doubt it’ll lead anywhere.
Five Hours — and Trains Out
The initial idea was mine alone. “What should I build?”
I was scouting casual games on the US App Store when I spotted a small game called Arrows Out. Small might be an understatement — it had over ten million downloads, and there were tons of clones with slightly different names. A natural chain reaction started in my head. Arrows to Snakes? Trains? Then I remembered the three years I lived in Japan.
“Let’s go with Trains for now, and later swap in pretty Japanese-style train graphics.”
That’s how the name Trains Out was decided.
From there to the first build — five hours. More precisely, strategy, planning, and development threaded into a single breath over five hours. The number sounds impressive, but AI didn’t do it on autopilot. AI kept asking questions, and I had to keep making decisions. I had to keep feeding it opinions drawn from 25 years of experience and knowledge. AI made a lot of mistakes too. Wrong libraries, wrong APIs, code going in circles. The five hours were filled with the number of times a senior had to say “that’s wrong right there.”
Still, the build ran. Looking at the first screen installed on the simulator, I had a strange thought.
This is really possible in five hours. Even a 25-year veteran needed time to process that.
The Blueprints Behind Those Five Hours — Full Documents
There are documents that made the five hours possible. The project directive given to AI (CLAUDE.md), the original design spec (SPEC.md), and a record of 53 decisions made collaboratively with AI (DECISIONS.md). Published here unedited.
CLAUDE.md — The project directive given to AI (click to expand)
# TrainsOut — Train Escape
## Status
**v1.2.0** — Live on Korean Apple App Store, in operation.
Production stability is the top priority. **Any change that may affect the release build must be confirmed with the user.**
| Item | Detail |
|---|---|
| App Name | Train Escape - Puzzle Game |
| Package | `com.hol4b.trainescapepuzzle` |
| Target | iOS only (Korean App Store). `android/` folder is stale, ignore |
| Category | Puzzle (4+) |
| Monetization | AdMob only (banner + interstitial + rewarded) |
| Build | **Local EAS build only** (`eas build --local`). No cloud build billing |
## Stack
- React Native + **Expo SDK 55** + TypeScript
- **Zustand** (`gameStore`, `devStore`)
- **Reanimated v4** (worklet-based animation)
- **expo-audio** (BGM + SFX)
- **Expo Router** (file-based routing)
- **react-native-google-mobile-ads 16.3.1** (service abstraction with mock fallback)
- AsyncStorage (level progress saving)
## Directory
```
app/ Expo Router (index, levels, game/[level], settings)
components/ Grid, Train, Track, HUD, DevPanel, TutorialOverlay
store/ gameStore.ts, devStore.ts
engine/ collisionDetect.ts
hooks/ useSound, useInterstitialAd, useRewardedAd
services/ ad/ + sound/ (real + mock dual implementation)
data/ generated.ts (200 levels, gridSize 10x10 ~ 24x24)
assets/sounds/ BGM, move, collision, win, fail, bomb_explode
utils/i18n.ts Korean strings
docs/SPEC.md 2026-03-27 original spec snapshot (differs from current code, reference only)
demo/ Shorts recording scene code (excluded from production build) — not yet present
shorts/ Shorts recording tooling (Node scripts) — not yet present
```
## Data Model
```typescript
type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';
type TrainState = 'IDLE' | 'MOVING' | 'ESCAPED';
type TrainColor = 'RED' | 'BLUE' | 'GREEN' | 'YELLOW' | 'PURPLE' | 'ORANGE' | 'CYAN';
interface Train {
id: string;
row: number;
col: number;
direction: Direction;
color: TrainColor;
state: TrainState;
length: number; // number of train cars
offsets?: [number,number][]; // L/S/U shape offsets (undefined for straight trains)
}
interface Level {
id: number;
gridSize: number; // actual 10~24
trains: Train[];
par?: number; // 3-star move threshold
timeLimit?: number; // time limit on some levels
}
```
## Game Mechanics (v1.2.0 actual)
- **Tap to move train**: moves cell by cell in its direction, `ESCAPED` on boundary exit, `WIN` when all trains escape
- **Collision detection**: if another train exists on the path, collision -> heart -1, return to original position
- **Hearts**: 3 per game. `hearts === 0` -> `FAIL`
- **Bombs**: appear from Level 11+. Countdown timer, explosion (heart loss) if not cleared in time
- **Timer**: time limit on some levels
- **Stars**: `moves <= par -> 3 stars`, `<= par+2 -> 2 stars`, else `1 star`
- **Tutorial overlay**: Level 1 (basic controls), Level 11 (bomb explanation)
- **Drag+Zoom**: pinch zoom + pan for large grids, fit-to-screen for small grids (`Grid.tsx`)
- **DevPanel**: 5-tap on home logo -> `unlockAll`, time manipulation, dev toggles
## Monetization (actual, differs from SPEC)
- **Banner**: game screen bottom (`SafeBannerAd` + `ErrorBoundary` for safe fallback when native module missing)
- **Interstitial**: **every 5 levels cleared** (`useInterstitialAd`). Differs from SPEC's "hearts=0 auto-ad"
- **Rewarded**: fail overlay **"watch ad to recover 2 hearts"** button (`useRewardedAd`). Not forced, user choice
- **SIMULATOR=1** env: AdMob plugin not loaded at all (`app.config.js`). Simulator dev/recording mode
- **Service abstraction** (`services/ad/`): auto fallback to mock when native module unavailable
## Animation Timing
- Movement: 200ms (`Easing.out(Easing.quad)`)
- Escape: 85ms/cell linear
- Bomb urgent pulse: 250ms repeat (countdown <= 3)
- **No `Date.now()` or non-deterministic time calls** -> recording reproducibility OK
---
## Shorts Pipeline (2026-04-12, WIP)
Marketing YouTube Shorts auto-production pipeline. Topic sentence -> Claude Code writes scene code -> iOS simulator recording (`xcrun simctl io booted recordVideo`) -> ffmpeg post-processing -> `out/<slug>.mp4`.
### Isolation Principle (Zero production impact)
- All Shorts-related code exists **only in `/demo` and `/shorts`**
- Production code (`app/`, `components/`, `store/`, `engine/`, `hooks/`, `services/`) **never imports `/demo`**
- `.easignore` excludes `/demo`, `/shorts` from build artifacts (when introduced)
- Only modification to production files: **`testID` prop additions**, which have zero runtime impact (metadata only)
SPEC.md — Original design spec (2026-03-27 snapshot, click to expand)
# Train Escape — Claude Code Project Spec
## Project Overview
A sequence puzzle game where you escape all trains from the tracks without collisions.
React Native + Expo, iOS development.
**Target: Korean Apple App Store**
---
## App Basic Info
| Item | Detail |
|------|--------|
| App Name | Train Escape |
| Package | com.hol4b.trainescapepuzzle |
| Category | Puzzle |
| Age Rating | All ages |
| Store | Apple App Store (Korea) |
| Language | Korean |
---
## Tech Stack
| Layer | Choice | Reason |
|-------|--------|--------|
| Framework | React Native + Expo (SDK 51+) | Fast builds, single codebase |
| Language | TypeScript | Type safety |
| State | Zustand | Lightweight game state management |
| Animation | React Native Reanimated v3 | 60fps train movement |
| Ads | react-native-google-mobile-ads | AdMob banner + interstitial |
| Storage | AsyncStorage | Level progress saving |
| Navigation | Expo Router | File-based routing |
---
## Directory Structure
```
/app
index.tsx # Home screen
game/[level].tsx # Gameplay screen
levels.tsx # Level selection
settings.tsx # Settings (sound)
/components
Grid.tsx # Track grid
Train.tsx # Train component (SVG + animation)
Track.tsx # Track background cell
HUD.tsx # Hearts, level, move count
BannerAd.tsx # Bottom banner ad
CrashEffect.tsx # Collision effect
InterstitialGate.tsx # Hearts 0 -> interstitial ad gate
/store
gameStore.ts # Zustand game state
/engine
levelValidator.ts # Level solution validator (BFS)
collisionDetect.ts # Collision detection
levelGenerator.ts # Level data parser
/data
levels/
easy.ts # Levels 1-50 (3x3~4x4)
medium.ts # Levels 51-150 (4x4~5x5)
hard.ts # Levels 151-300 (5x5~6x6)
/assets
sounds/
train_move.mp3
train_horn.mp3
crash.mp3
/hooks
useGameLoop.ts
useSound.ts
useAd.ts # Ad load/display hook
```
---
## Core Game Engine Spec
### Theme Mapping
| Arrow Out Original | Train Escape |
|-------------------|-------------|
| Arrow | Train (distinguished by color) |
| Movement direction | Train heading direction |
| Grid cell | Track intersection |
| Collision | Train crash |
| Off-screen exit | Station arrival / Escape success |
| Heart | Life (3 per game) |
### Game Loop
```
Game Start
-> hearts = 3
train[id] tap
-> state = MOVING
-> cell-by-cell movement, collision check at each cell entry
Collision detected:
hearts -= 1
CrashEffect plays (flash + haptics + crash sound)
state = IDLE (return to original position)
hearts === 0 -> status = FAIL
Normal:
continue movement
-> boundary exit -> state = ESCAPED
-> all ESCAPED -> status = WIN
status === FAIL
-> InterstitialGate fires
-> Interstitial ad shown (AdMob)
-> After ad closes -> hearts = 3, restart current level
```
### Collision Detection Rules
- Cell-based movement (not pixel-based)
- Pre-check entire movement path for other trains
- Sequential taps only (no simultaneous movement)
---
## Monetization: AdMob Only
```
Ad Structure:
+-- Banner Ad (BannerAd)
| Position: always visible at bottom of game screen
| Size: BANNER (320x50)
|
+-- Interstitial
Trigger: all 3 lives lost (hearts === 0)
Flow: FAIL -> ad shown -> ad closes -> hearts restored to 3 -> level restart
Note: if ad fails to load, still restore hearts and restart (no forced ads)
In-app purchases: none
Boosters: none
```
---
## UI/UX Spec
### Color Palette (Dark Theme)
```
Background: #0f1923
Grid/Track: #1a2a3a
Track Line: #2d4a6e
Train RED: #e63946
Train BLUE: #457b9d
Train GREEN: #2a9d8f
Train YELLOW:#f4a261
Train PURPLE:#9b72cf
Heart full: #e63946
Heart empty: #2d3748
Text: #f0f4f8
```
### Screens
1. Home: Logo, Start, Level Select, Settings
2. Level Select: Grid, stars, locked levels
3. Game: Track grid center, top HUD (hearts/level/moves), bottom banner ad
4. Clear: "Escape Success!" + stars + next/retry
5. Fail (hearts=0): InterstitialGate -> ad -> auto restart
---
## Known Risks & Mitigations
| Risk | Mitigation |
|------|-----------|
| Interstitial UX friction | Only shown at hearts=0 -> low frequency, acceptable |
| Ad load failure | Still restore hearts and restart on failure (no forced ads) |
| Reanimated performance | Worklet-based animations |
| Level creation time | 50 manual -> BFS auto-generator |
| Review rejection | Privacy policy URL prepared in advance (required for AdMob) |
---
## Claude Code First Prompt
```
Read CLAUDE.md and start from Phase 1.
1. Initialize Expo + TypeScript project
2. Install dependencies (zustand, reanimated@3, expo-router, expo-haptics, expo-av)
3. Render 3 trains on a 3x3 grid
4. Trains built with React Native SVG directly (no image assets)
5. Verify basic behavior: tap -> move in direction -> ESCAPED on boundary exit
```
DECISIONS.md — 53 decisions made collaboratively with AI (click to expand)
# Decisions Register
<!-- Append-only. Never edit or remove existing rows.
To reverse a decision, add a new row that supersedes it.
Read this file at the start of any planning or research phase. -->
| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |
|---|------|-------|----------|--------|-----------|------------|---------|
| D001 | M001 | integration | AdMob milestone placement | Keep AdMob integration out of M001 and schedule it for a later milestone that assumes a development/native build path | Current library guidance confirms AdMob flows are not an Expo Go-first path, so mixing monetization into the first milestone would dilute proof of the core puzzle loop and add native build complexity too early. | No | collaborative |
| D002 | M001 | architecture | M001 sequencing focus | Plan M001 around proving puzzle feel and readability before broader game-shell or monetization work | The user emphasized that the first playable must not feel sluggish or frustrating, so early slices should be player-visible vertical increments that prove responsiveness, clarity, and fun rather than invisible technical layers. | Yes — if later testing shows shell or UX framing must move earlier | collaborative |
| D003 | M001/S01 | requirement | R001 | validated | S01 delivered a real home→level-1→tap-to-move→escape loop with deterministic store transitions, SVG board rendering, and passing movement/store tests plus successful Expo Metro boot proof. | Yes | agent |
| D004 | M001/S01 | requirement | R003 | validated | S01 established immediate tap responsiveness for the first playable loop by wiring direct train press handling to deterministic movement/store updates, exposing visible status/move labels, and proving the runtime boots cleanly in Expo with green automated tests. | Yes | agent |
| D005 | M001/S02/T02 | observability | How movement results are retained in the game store | Persist the full engine MovementResolution as `lastResolution` alongside the compact `lastInteraction` feedback snapshot. | This keeps the pure engine as the single source of truth while giving downstream UI and debugging surfaces direct access to path and collision metadata without recomputation or duplicated derivation logic. | Yes | agent |
| D006 | M001/S02 | requirement | R002 | validated | S02 delivered deterministic cell-based collision/movement contracts in the pure engine, persisted structured movement resolutions and last-interaction feedback in Zustand, exposed blocked/escaped/no-op outcomes in the game UI, and re-verified the behavior with 15 passing movement/store tests plus Expo Metro startup evidence. | Yes | agent |
| D007 | M001/S03 | requirement | R004 | validated | S03 added pure board-presentation derivation, visible exit markers, directional train visuals, highlighted attempted/blocked paths, collision-cell overlays, board legend and feedback cards, then proved the contract with passing boardPresentation + movement Jest suites and clean non-interactive Expo Metro startup on pinned port 8081. | Yes | agent |
| D008 | M001/S04 | requirement | R005 | validated | S04 expanded the easy pack into multiple handcrafted solvable levels, wired the pack into the home and game flows with deterministic replay/next-level behavior and malformed-param fallback, and verified it with passing Jest suites plus successful Expo Metro startup proof. | Yes | agent |
| D009 | M001/S05 | requirement | R005 | validated | S05 locked the multi-level easy-pack flow with integrated regression coverage for home entry, malformed fallback, replay reset, next-level progression, completed-pack behavior, and verified Expo Metro startup on the pinned 8081 path. | Yes | agent |
| D010 | M002 | pattern | 레벨 해금 방식 | 순차 해금 — 이전 레벨 클리어해야 다음 레벨 열림 | 선형 진행이 퍼즐 난이도 곡선 관리에 유리하고, 구현이 단순함 | Yes | collaborative |
| D011 | M002/S01 | requirement | R006 | validated | S01 added hearts to the shared game snapshot, surfaced explicit PLAYING/FAIL copy plus a fail CTA on the game screen, locked board input while FAIL, and verified the behavior with passing heart-system, integrated-flow, movement, and screen-level Jest suites plus clean TypeScript checks. | Yes | agent |
| D012 | M002/S02 | requirement | R007 | validated | S02 added deterministic WIN-only par-based star summaries, rendered the result card with replay/next-level or pack-complete branching, and proved the behavior with passing starRating, integratedPlayFlow, and gameScreenFailLoopScreen Jest suites. | Yes | agent |
| D013 | M002/S03 | requirement | R008 | active | S03 delivered the AsyncStorage-backed progress contract, home/levels/game route integration, and passing progression/levels-screen coverage, but the slice-level verification contract is not fully complete because gameScreenFailLoopScreen.test.tsx remains failing and the planned chained typecheck did not run. Requirement evidence advanced substantially, but full validation proof is not yet closed. | Yes | agent |
| D014 | M002/S04 | requirement | R005 | validated | S04 expanded the easy content pack from 6 to 50 ordered levels, preserved deterministic anchor fixtures, updated progression/UI regressions for first/middle/last and late-pack behavior, and re-verified the 50-level contract with passing levelPack, integratedPlayFlow, levelsScreen, progressPersistence, and TypeScript checks. | Yes | agent |
| D015 | M002/S05 | requirement | R005 | validated | S05 closed the remaining M002 proof gap by passing the mounted game-screen regression harness, the focused integrated progression/persistence/result/fail regression set, and TypeScript checks against the current 50-level easy pack. This now proves the real home→levels→play→FAIL→restart→WIN→next-level→relaunch persistence loop with the expanded pack. | Yes | agent |
| D016 | | requirement | R005 | validated | S04/S05 now prove the expanded easy-pack gameplay loop with 50 playable levels, mounted/integrated regression coverage for progression/fail-restart/win/next-level/relaunch persistence, and passing `levelPack.test.ts`, `integratedPlayFlow.test.ts`, `levelsScreen.test.tsx`, `progressPersistence.test.ts`, `starRating.test.ts`, `heartSystem.test.ts`, `movement.test.ts`, plus `npx tsc --noEmit`. | Yes | agent |
| D017 | M003 S01 T01 | architecture | AdMob boundary exposure for game UI | Expose banner rendering through a typed runtime descriptor in hooks/useAd.ts and keep production ad unit IDs in Expo config extra instead of hardcoding SDK calls into screen components. | This keeps the screen and later banner wrapper mock-friendly, prevents silent production fallback to test IDs, and leaves a deterministic debug label that tests can assert without depending on native ad behavior. | Yes | agent |
| D018 | M003/S01 | requirement | R010 | active | M003/S01 integrated the bottom banner ad region into the game screen through a typed runtime descriptor, config-backed ad unit IDs, deterministic fallback messaging, and passing game-screen/levels-screen plus TypeScript verification. This advances the monetization integration requirement, but full validation still depends on the hearts=0 interstitial gate in later slices. | Yes | agent |
| D019 | M003 S02 planning | architecture | FAIL restart ownership during interstitial gating | Keep store/gameStore.ts as the source of FAIL and restart semantics, but let a dedicated InterstitialGate component own when restartLevel() fires by consuming a typed interstitial descriptor/controller from hooks/useAd.ts. | The store already provides correct hearts=0 and restart behavior. Putting ad lifecycle branching in hooks/useAd.ts and restart timing in a gate component preserves the S01 ad-boundary pattern, keeps app/game/[level].tsx mock-friendly, and prevents early restart bypassing the interstitial or duplicating fallback logic in the screen. | Yes | agent |
| D020 | M003/S02 | requirement | R011 | validated | S02 proved that when the interstitial ad cannot be shown or errors during the FAIL gate, the game screen releases through the fallback path and restores PLAYING with hearts=3, moves=0, cleared lastInteraction/lastResolution, and unchanged persisted progress. Evidence: passing gameScreenFailLoopScreen.test.tsx, heartSystem.test.ts, and npx tsc --noEmit. | Yes | agent |
| D021 | M003/S03 | requirement | R010 | validated | S03 closed the monetization proof boundary by passing the mounted GameScreen regression plus persistence/result suites and TypeScript. Evidence now proves banner presence on the game screen, hearts=0 interstitial gate lock-and-release behavior, restart back to PLAYING with hearts reset, no durable progress writes during FAIL/restart, and surviving WIN/progression behavior via `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx progressPersistence.test.ts integratedPlayFlow.test.ts` and `npx tsc --noEmit`. | Yes | agent |
| D022 | M004 discussion | scope | M004 scope boundary for repeat-play and release prep | Keep M004 repeat-play work on-device and privacy-minimal: no account system, no personal-information product features, no backend storage, and no analytics-driven expansion as part of this milestone. | The user explicitly wants repeatable on-device content and does not want the app to receive personal information. Locking this boundary now prevents M004 from expanding into live-ops, backend state, or disclosure-heavy product systems that would dilute the release-finish milestone. | Yes | collaborative |
| D023 | M004 discussion | pattern | M004 feedback feel bar | Target clear, restrained feedback rather than loud spectacle: subtle movement cues, stronger collision/clear moments, and verification focused on clarity at the existing game-screen boundary. | The user emphasized 'clear' rather than flashy. This gives S01 a concrete product bar and keeps audio/haptic work aligned with the puzzle-first tone established by the current board and result shell. | Yes | collaborative |
| D024 | | requirement | R013 | validated | M004/S01 shipped a typed feedback derivation helper, Expo audio+haptics runtime seam, mounted GameScreen feedback diagnostics, and passing slice verification via `npm test -- --runInBand -- gameFeedback.test.tsx gameScreenFailLoopScreen.test.tsx heartSystem.test.ts movement.test.ts boardPresentation.test.ts && npx tsc --noEmit`, proving move/collision/clear/fail-recovery feedback integration without regressing gameplay or monetization flows. | Yes | agent |
| D025 | M004/S02 | requirement | R014 | validated | M004/S02 delivered a local challenge catalog, isolated challenge persistence, source-aware home/challenge entry, and mounted GameScreen reuse for challenge play with replay-after-win and fail-gate recovery. Evidence: `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx levelsScreen.test.tsx challengeMode.test.ts integratedPlayFlow.test.ts` passed 28/28 and `npx tsc --noEmit` passed, proving repeatable on-device content can be opened and replayed multiple times per day without backend or account dependence. | Yes | agent |
| D026 | | requirement | R012 | validated | M004/S03 established a single release-metadata/privacy contract across `app/release/_lib/storeMetadata.ts`, `docs/release/app-store-korea.md`, `app.json`, and the mounted `/release` screen reachable from home, then re-verified the Korean launchability surface with passing `npm test -- --runInBand -- releaseMetadata.test.ts levelsScreen.test.tsx gameScreenFailLoopScreen.test.tsx challengeMode.test.ts integratedPlayFlow.test.ts` and `npx tsc --noEmit`. | Yes | agent |
| D027 | M005 discussion | pattern | M005 result readability bar | Define result-surface success by one-glance understanding: the player can tell win/loss, quality, and next action without explanation, and the screen must not contradict run memory. | The user defined M005 closeout around one-glance comprehension rather than around raw presence of stars, moves, or CTAs. The result surface must therefore be validated against this product bar. | Yes | collaborative |
| D028 | M005/S01 | requirement | R006 | validated | M005/S01 re-proved the mounted GameScreen state-truth contract across PLAYING, BLOCKED, ESCAPED, FAIL, and WIN through visible copy plus deterministic diagnostics, while the closure suite (`npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx heartSystem.test.ts integratedPlayFlow.test.ts starRating.test.ts boardPresentation.test.ts`) passed 38/38 and workspace TypeScript diagnostics reported no issues. | Yes | agent |
| D029 | | requirement | R007 | validated | M005/S02 mounted fail-recovery verification now proves the continuity loop end to end: after hearts reach 0 and the interstitial gate releases, GameScreen returns to a fresh PLAYING read with hearts 3/3, moves 0, FAIL UI removed, grid re-enabled, and baseline board/debug copy restored. Evidence: `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx heartSystem.test.ts boardPresentation.test.ts gameFeedback.test.tsx` and `npx tsc --noEmit`. | Yes | agent |
| D030 | M005/S03/T02 | requirement | R007 | validated | S03 closed the mounted result-surface contract without further runtime edits: the mounted GameScreen WIN branches, pure helper suites, and integrated progression tests now agree on quality wording, next-action semantics, and diagnostic state lines. Evidence: passing `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx starRating.test.ts integratedPlayFlow.test.ts` and `npx tsc --noEmit`. | Yes | agent |
| D031 | M005/S03 | requirement | R007 | validated | S03 closed the mounted result-surface contract without runtime edits: GameScreen WIN branches, deriveStarRatingSummary(), and deriveScreenLevelContext() now agree on one-glance quality wording, next-action semantics, and deterministic feedback/debug lines. Proof: passing `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx starRating.test.ts integratedPlayFlow.test.ts` and `npx tsc --noEmit`. | Yes | agent |
| D032 | M006 | scope | M006 scope boundary | Treat M006 as a browser-local verification milestone, not a web product expansion milestone. | The user wants to run the game locally in this PC's browser and directly verify the current product, not add web monetization, web deployment, or parity with the native target. | Yes | collaborative |
| D033 | M006 | architecture | Browser ad/runtime strategy | On web, disable or replace native ad behavior behind a web-safe import boundary instead of trying to run `react-native-google-mobile-ads`. | The actual investigation showed the current Expo web bundle fails at import time on the native ads package, so runtime fallback is not enough. The browser path must avoid bundling unsupported native ad modules while preserving the existing native-focused contract. | Yes | collaborative |
| D034 | M007 | architecture | 가변 길이 열차 좌표 기준 | Train 좌표를 열차의 head(진행 방향 쪽 맨 앞칸) 기준으로 정의하고, 몸통 점유 셀은 방향 반대편으로 계산한다. | 앞부분이 둥근 실루엣, 방향 판독, 탈출 애니메이션, 경계 이탈 타이밍 모두 head 기준이 가장 자연스럽다. 기존 단일 셀 row/col 의미를 앞칸으로 해석하면 길이 확장 시 엔진/렌더링 계산이 단순해진다. | Yes | collaborative |
| D035 | M006/S01 | architecture | M006/S01 web runtime import boundary | Keep ads and gameplay feedback behind shared descriptor hooks plus `.native`/`.web` platform files, and preserve mounted `banner=` / `phase=` / `fallback=` / feedback debug lines as the regression seam. | This slice proved that Expo web export succeeds only when unsupported native packages are removed from the web module graph at import time. Preserving the existing mounted diagnostics lets browser-local verification reuse the same GameScreen and InterstitialGate contracts instead of inventing web-only assertions. | Yes | agent |
| D036 | M006/S01 | requirement | R010 | active | M006/S01 preserved the monetization UI contract across web-safe runtime boundaries: banner diagnostics still render, interstitial phase/fallback diagnostics still drive FAIL recovery, `gameScreenFailLoopScreen.test.tsx` and `npx tsc --noEmit` pass, and `CI=1 npx expo export --platform web` now succeeds without crashing on `react-native-google-mobile-ads`. This advances browser-local compatibility for the monetization seam but does not newly validate native AdMob behavior itself. | Yes | agent |
| D037 | M006/S01 | requirement | R013 | active | M006/S01 preserved gameplay feedback behavior while moving `expo-audio` and `expo-haptics` behind platform files. Focused feedback tests, TypeScript, and web export now prove the browser path no longer crashes on feedback imports, but this slice did not change the already validated native feedback capability contract. | Yes | agent |
| D038 | M006/S02 | architecture | M006/S02 browser proof route handling | Use a dedicated route-only proof flag and stable train selectors for the browser WIN seam; keep normal unlock rules and existing GameScreen diagnostics unchanged. | Live web verification showed the dedicated home CTA fell back to level 1 on cold start because hydration correctly enforced normal easy-pack unlocks. Adding an explicit proof=browser-win route flag let the browser proof open deterministic level 3 without weakening progression rules, while stable train selectors and the existing screen-boundary debug lines kept browser automation and mounted regression coverage aligned. | Yes | agent |
| D039 | M007/S01/T01 | architecture | M007/S01 movement engine seam for variable-length trains | Treat `Train.row`/`col` as head coordinates, derive occupied cells backward from direction, and derive escape/collision checks from the head-forward path only. | This keeps single-cell behavior unchanged while making length-based occupancy, blocking, and escape timing deterministic for downstream store and rendering slices. Collision checks now use the same explicit helper contract that tests can exercise directly. | Yes | agent |
| D040 | M007/S02 | architecture | M007/S02 variable-length rendering seam | Use a shared pure render-geometry helper backed by engine occupancy for Grid placement, Train silhouette sizing, highlight bounds, and tap-target expansion. | S02 proved that rendering and interaction stay deterministic when both Grid and Train consume getTrainRenderGeometry(), which itself reuses getTrainOccupiedCells(). This keeps mounted FAIL/WIN/restart behavior aligned with the head-based engine contract and gives S03 one geometry seam for long-train animation work. | Yes | agent |
| D041 | M007/S03 planning | architecture | M007/S03 long-train exit animation state boundary | Keep long-train exit animation as Grid-owned presentation state derived from render geometry and ESCAPED transitions, and clear it immediately on reset/restart/level changes instead of persisting animation state in Zustand. | S01/S02 already proved that engine/store truth flips to ESCAPED immediately and resetLevel()/restartLevel() restore the original level snapshot synchronously. A presentation-only overlay preserves those rule-truth contracts while allowing the UI to animate the tail off-screen and reset cleanly without changing movement, WIN timing, persistence, or mounted debug seams. | Yes | agent |
| D042 | | architecture | M007/S03 long-train exit overlay lifetime | Keep exit overlays as Grid-local transient presentation state keyed off ESCAPED transitions, with reset-on-replay/restart/level-change and no store persistence. | The slice proved the pure animation seam and the runtime overlay/reset behavior on disk without changing engine semantics. Store-level ESCAPED/WIN truth remains immediate, while replay/fail recovery can clear overlay residue synchronously. The remaining instability is in the mounted Jest fake-timer harness, not in the gameplay contract itself. | Yes | agent |
| D043 | | requirement | R009 | validated | S04 rebalanced easy and challenge boards around calibrated mixed-length layouts, preserved stable proof anchors and public ids, and re-verified meaningful par-driven star feedback through passing `levelPack.test.ts`, `starRating.test.ts`, `challengeMode.test.ts`, `heartSystem.test.ts`, `integratedPlayFlow.test.ts`, `gameScreenFailLoopScreen.test.tsx`, and `npx tsc --noEmit`. This now proves players receive durable move-quality feedback against the shipped harder boards rather than trivial single-car layouts. | Yes | agent |
| D044 | M008/S01 planning | architecture | M008/S01 native proof surface | Use SDK-55 dependency normalization plus a checked-in `scripts/verify-ios-native-run.sh` and `docs/ios-native-run.md` as the canonical iOS simulator proof/diagnostic seam for the milestone. | Research showed the first blocker is native build health, not route logic. Closing S01 requires both fixing the package/config contract enough for `npx expo run:ios` to reach simulator install/boot and leaving a repeatable phase-classified operator path that later slices can reuse without rediscovering CLI, port, prebuild, pods, and xcodebuild behavior. | Yes | agent |
gstack Milestones — 10 Phases, 258 Documents
This project has milestone documents automatically generated and managed by gstack. 10 milestones, 258 markdown files total. Each milestone includes context, roadmap, slices (small work units), validation results, and a summary. When AI and a human work together, this much documentation accumulates automatically.
Milestone
Title
Status
M001
Playable core puzzle loop
Complete
M002
Hearts, stars, progress saving
Complete
M003
AdMob ad integration
Complete
M004
Polish & launch prep (sound, challenges, Korean metadata)
M001 Summary — Playable core puzzle loop (click to expand)
---
id: M001
title: "플레이 가능한 코어 퍼즐 루프"
status: complete
completed_at: 2026-03-27T13:44:08.014Z
key_decisions:
- Stay on Expo SDK 55 and align package versions until `expo-doctor` is clean before treating the baseline as stable — avoids carrying SDK drift into later milestones.
- Use Expo Router from the start so later slices build on real route files instead of migrating from a temporary single-screen app.
- Keep movement resolution in a pure engine module (`engine/movement.ts`) that returns deterministic outcomes reusable by UI, store, tests, and future animation work.
- Expose `status`, `moves`, and per-train `state` directly in the Zustand store so runtime state is inspectable from the UI without recomputation.
- Represent movement results with shared typed reason codes and collision metadata (`MovementResolution`) instead of ad-hoc booleans.
- Persist both a concise `lastInteraction` and the full engine `lastResolution` in the store so downstream code can inspect exact blocker coordinates without recomputing gameplay rules.
- Keep board readability as a pure derived presentation layer (`boardPresentation.ts`) over existing store snapshots — no persisted UI state added.
- Keep route-adjacent pure helpers under `app/game/_lib/` so Expo Router does not misclassify them as routes during web runtime.
- Pin `expo.port` to 8081 in `app.json` so CI-style Metro startup proof is deterministic across verification runs.
- Preserve earlier fixture level ids (1, 3, 4) when expanding content packs so engine/UI regression suites keep trustworthy deterministic anchors.
key_files:
- engine/movement.ts
- store/gameStore.ts
- types/game.ts
- app/game/[level].tsx
- app/game/_lib/boardPresentation.ts
- app/game/_lib/levelProgression.ts
- app/game/_lib/levelParam.ts
- components/Grid.tsx
- components/Train.tsx
- data/levels/easy.ts
- app/index.tsx
- __tests__/movement.test.ts
- __tests__/boardPresentation.test.ts
- __tests__/levelPack.test.ts
- __tests__/integratedPlayFlow.test.ts
- app.json
lessons_learned:
- Expo scaffold gotcha: when a repo already contains `.gsd/` metadata, `create-expo-app` must be generated in a temp directory and copied in — it will not initialize cleanly over existing non-Expo files.
- Expo Router/Jest gotcha: keep route-param coercion and other boundary helpers in pure modules under `_lib/` — this avoids pulling Expo Router runtime into Jest and keeps edge-case coverage independent of the router.
- Expo CLI gotcha: `--non-interactive` is no longer supported; use `CI=1 npx expo start --offline --clear` for non-interactive startup proof.
- Expo port prompt gotcha: pin `expo.port` to `8081` in `app.json` to prevent alternate-port prompts in CI-style verification when Metro was previously shifted to another port.
- First-playable game-store pattern: keep movement resolution in a pure engine module and expose `status`, `moves`, and per-train `state` directly from Zustand so UI and later animation slices inspect gameplay state without recomputing it in components.
- Collision UX pattern: keep `lastInteraction` for concise player-facing copy and `lastResolution` for full engine truth; have screens read both directly from the store instead of re-deriving blockers, path length, or mutation state in UI code.
- Board readability pattern: derive all presentation signals (highlighted train, path, blocked, collision, exits) from existing store snapshots in a pure helper — adding no persisted UI state keeps the seam testable and the store authoritative.
- Content pack pattern: keep next/previous/completed-pack adjacency in a pure data helper (`levelProgression.ts`) so screens consume one contract; preserve fixture ids when expanding so earlier regression suites remain valid.
---
# M001: 플레이 가능한 코어 퍼즐 루프
**Delivered a fully playable tap-to-escape puzzle loop with deterministic collision rules, readable board presentation, a six-level handcrafted pack, and integrated regression coverage — proving the core game feel before any polish layer is added.**
## What Happened
M001 built the foundation of 열차 탈출 from an empty repository to a short but genuinely playable puzzle session across five sequenced slices.
**S01 — 첫 플레이어블 루프** established the architectural skeleton: an Expo Router app shell, a pure `engine/movement.ts` resolving tap-to-escape in deterministic cell steps, a Zustand store exposing `status`/`moves`/per-train `state` directly, and reusable SVG Grid and Train components that render the board without image assets. The home screen routes into `app/game/[level].tsx`, level params are coerced safely in a pure helper, and the WIN state fires when the last train escapes. A 11-case Jest suite locked the escape/win/reset/boundary contracts from the start.
**S02 — 충돌 규칙과 상태 전이** made blocked moves feel explainable rather than arbitrary. Movement outcomes gained shared typed reason codes, traversed path data, and blocker cell metadata. The Zustand store persists both a concise `lastInteraction` for UX copy and a full `lastResolution` snapshot so UI and future diagnostics never recompute engine truth. An on-screen feedback card and compact debug line show outcome/reason/path length/blocker coordinates in the live game screen. A level-4 collision fixture was exposed from the home screen for fast runtime repro. The suite grew to 15 cases covering blocked paths, no-ops, repeated inputs, and WIN/move-count protections.
**S03 — 가독성 있는 보드/열차 표현** added a pure presentation layer on top of the S01/S02 store contracts. `boardPresentation.ts` derives highlighted train, attempted path, blocked path, collision cell, escaped count, exit marker visibility, and hint tone without adding any persisted UI state. Grid gained exit markers, path overlays, blocked-cell emphasis, and collision-cell highlighting. Train gained directional nose geometry and per-state visual styling (selected, blocked, escaped). The game screen now pairs the board with a legend, hint card, and result card. A `boardPresentation.test.ts` suite locked the derived presentation contract with seven regression cases. The Expo port was pinned to 8081 in `app.json` to make CI-style Metro startup deterministic.
**S04 — 짧지만 진짜 풀 수 있는 레벨 묶음** replaced the minimal starter set with six hand-crafted 3×3 and 4×4 easy levels that form a short connected playable arc. A pure `levelProgression.ts` helper handles next/previous/completed-pack lookups so screens consume one authoritative contract. The home screen became a level-entry card list; the game WIN screen shows replay/next buttons and completed-pack messaging. A `levelPack.test.ts` suite with 15 cases locked ordering, adjacency, coercion, last-level, and empty-pack edge cases. Earlier regression suites (movement + board presentation) continued passing unmodified.
**S05 — M001 통합 정리와 플레이 검증** closed the milestone by composing all prior seams into an integrated regression suite (`integratedPlayFlow.test.ts`, 14 cases), aligning game-screen UI badges and copy to the helper-driven progression contract, and verifying that `CI=1 npx expo start --offline --clear --port 8081` reaches `Waiting on http://localhost:8081` deterministically. TypeScript diagnostics were confirmed clean via LSP (a stale relative import in `boardPresentation.ts` was fixed). The authoritative verification contract for the milestone is now: `npm test -- --runInBand -- movement.test.ts boardPresentation.test.ts levelPack.test.ts integratedPlayFlow.test.ts` passing plus pinned-port Metro boot.
## Success Criteria Results
## Success Criteria Results
- **탭 기반 열차 이동** ✅ — S01 ships `engine/movement.ts` + Zustand store + SVG board. Tapping a train resolves its direction, traverses cells, and escapes the board. `npm test movement.test.ts` passes 15 cases including escape, no-op, reset, and WIN transition.
- **셀 기반 충돌 규칙** ✅ — S02 adds structured `MovementResolution` with reason codes, traversed path, and blocker metadata. Level-4 collision fixture is reachable from home. 15 movement contract tests prove blocked/escaped/no-op/WIN-protection determinism.
- **읽기 쉬운 보드 표현** ✅ — S03 ships `boardPresentation.ts` pure helper, Grid/Train overlays, and in-screen legend/hint/result cards. `boardPresentation.test.ts` passes 7 regression cases covering neutral baseline, blocked movement, escaped state, malformed input, and fallbacks.
- **여러 개의 초기 레벨** ✅ — S04 delivers six handcrafted 3×3 and 4×4 easy levels with home entry cards, WIN-screen replay/next controls, and completed-pack messaging. `levelPack.test.ts` passes 15 cases.
- **코어 퍼즐 루프 통합 재검증** ✅ — S05 ships `integratedPlayFlow.test.ts` (14 cases) composing home-entry, fallback, collision, progression, and completed-pack contracts. Metro boots deterministically on port 8081.
- **"짧게라도 재밌다" 증명** ✅ — The six-level pack, readable board, feedback cards, and explicit UAT scripts across all five slices provide sufficient mixed-evidence proof for a first-playable milestone bar. Subjective UX evidence is intentionally hybrid (automation + manual UAT scripts) rather than a recorded device session.
## Definition of Done Results
## Definition of Done Results
- **All slices marked complete** ✅ — S01, S02, S03, S04, S05 all show `[x]` in the roadmap and have corresponding SUMMARY.md + UAT.md artifacts on disk.
- **All slice summaries exist** ✅ — Verified: S01-SUMMARY.md, S02-SUMMARY.md, S03-SUMMARY.md, S04-SUMMARY.md, S05-SUMMARY.md — all present.
- **Non-.gsd/ code changes present** ✅ — `git diff --stat HEAD~5 HEAD -- ':!.gsd/'` shows 14 changed source/test files, 1,211 insertions and 168 deletions across engine, store, components, app screens, data, and test suites.
- **Jest regression suites passing** ✅ — `movement.test.ts` (15 cases), `boardPresentation.test.ts` (7 cases), `levelPack.test.ts` (15 cases), `integratedPlayFlow.test.ts` (14 cases) — all passing per S02/S03/S04/S05 verification evidence.
- **Metro runtime boots deterministically** ✅ — `CI=1 npx expo start --offline --clear --port 8081` reaches `Waiting on http://localhost:8081` with no blocking errors after S05 pinned the port in `app.json`.
- **TypeScript diagnostics clean** ✅ — S05 confirmed LSP-verified clean diagnostics after fixing a stale relative import in `boardPresentation.ts`.
- **Cross-slice integration consistent** ✅ — S02/S03/S04/S05 all consumed prior slice contracts (pure movement seam, store snapshots, board helper, progression helper) without replacing them; fixture ids 1, 3, and 4 were preserved across all content expansions.
- **Validation verdict** ✅ — M001-VALIDATION.md records `verdict: pass` with detailed per-slice delivery audit, cross-slice integration assessment, and requirement coverage analysis.
## Requirement Outcomes
## Requirement Outcomes
- **R001** (첫 플레이어블 루프) — **Active → Validated.** Evidence: `app/index.tsx` routes into `app/game/[level].tsx`; tapping trains updates store state through `engine/movement.ts` until all escape and `status = WIN`; `movement.test.ts` proves the full escape/win flow.
- **R002** (충돌 규칙과 상태 전이) — **Active → Validated.** Evidence: `engine/movement.ts` returns `MovementResolution` with typed reason codes, path data, and blocker metadata; `store/gameStore.ts` persists `lastResolution`/`lastInteraction`; `movement.test.ts` (15 cases) proves blocked/escaped/no-op/WIN determinism.
- **R003** (즉각적인 탭 반응과 상태 가시성) — **Active → Validated.** Evidence: `components/Grid.tsx`, `components/Train.tsx`, and feedback/debug cards on the game screen make every state change immediately inspectable; `npm test movement.test.ts` confirms store updates are synchronous and deterministic.
- **R004** (가독성 있는 보드/열차 표현) — **Active → Validated.** Evidence: `boardPresentation.ts` pure helper, Grid overlays (exit markers, path, blocked, collision), Train direction visuals, in-screen legend/hint/result cards; `boardPresentation.test.ts` (7 cases) proves the presentation contract.
- **R005** (여러 개의 플레이 가능한 레벨) — **Active → Validated.** Evidence: six handcrafted levels in `data/levels/easy.ts`, pure `levelProgression.ts` helper, home multi-level entry cards, WIN-screen replay/next/completed-pack flow; `levelPack.test.ts` (15 cases) and `integratedPlayFlow.test.ts` (14 cases) prove progression contracts end-to-end.
## Deviations
Scaffolding had to be generated in a temp directory and merged into the repo root because `create-expo-app` would not initialize over the existing `.gsd/` metadata. The `--non-interactive` Expo CLI flag was discovered to be removed mid-milestone; verification commands were updated to use `CI=1` from S03 onward. Route-adjacent pure helpers were moved from `app/game/` to `app/game/_lib/` during S03 to prevent Expo Router web from misclassifying them as routes. `app.json` port pinning was added during S03 verification after a non-interactive port prompt blocked startup proof. Runtime proof for M001 is a hybrid of automated Jest coverage plus Metro startup evidence rather than a captured live-device walkthrough, which is appropriate for the first-playable milestone bar but would not be sufficient for a full UAT submission.
## Follow-ups
Phase 2 work for the next milestone: hearts system (3 lives, -1 on collision, FAIL at 0), WIN/FAIL screen transitions with star rating based on `par`, level select screen with progress persistence (AsyncStorage), and expansion to 50 easy levels. The six-level M001 pack is intentionally minimal — the content volume and hearts/fail UX are gated on Phase 2 so the core loop could be proven first without those layers. Also: deprecated React Native Web `shadow*`/`pointerEvents` warnings in the web runtime were noted but not cleaned up — they do not affect the iOS target and can be deferred.
Review Queue, Then Rejection
I submitted 1.0 straight to the App Store. Almost no QA. At the time, I thought — what’s the point of QA at this scale? A few days later, the rejection email arrived. The reason: an ATT (App Tracking Transparency) handling bug.
My honest reaction was one line.
“I’ve done this so many times over a decade ago, and I still made such a simple mistake.”
We live in an era where you can build a game in five hours with AI. But we don’t live in an era where humans never need to step in. The fact that five hours was possible and the idea that five hours was enough are completely different stories. One rejection email pinpointed that difference precisely.
So What Changed
The series thesis — “Can AI really code? Can it go all the way to commercialization?” — the first half already has an answer from Part 1.
Yes, it’s possible. Five hours gets a full pipeline running. But only five hours where a senior is making decisions alongside the AI.
The second half — commercialization — begins in Part 2. Introducing QA for the first time, going through review twice, and the moment the first penny drops.
What You Can Do Right Now
Put off tool comparisons for now. If you’re getting into AI coding, trying one or two tools directly beats building a comparison matrix. Keep only what fits your hands. And spend your first five hours on a KPI-free side project. Spend five hours on something you’re genuinely curious about — not company work — and you’ll get your answer about what AI coding means for you.
이 글은 시리즈 마지막 3부다. 1부에서는 AI로 5시간 만에 게임을 만들고 거절당했고, 2부에서는 심사통화구 첫 1센트까지 도달했다. 3부는 그 출시 이후의 이야기다 — 마케팅, 새로 만들기 시작한 도구, 그리고 이미 시작해버린 두 개의 다음 실험.
출시하고 3일째, 나는 페이스북을 열었다
출시하면 사람들이 와줄 거라는 환상은 없었지만, 그래도 첫 3일은 조금 외로웠다. 다운로드가 거의 없었다. 본능적으로 페이스북과 링크드인에 글을 올렸다. 그러고는 Claude에 chat을 열고 단순한 질문을 했다 — 이런 작은 게임의 다운로드를 어떻게 늘리지? 여러 답이 돌아왔는데 그중 하나가 인디게임 커뮤니티의 contact 페이지에 들어가면 앱 리뷰를 받아주는 곳들이 있다는 것이었다.
지금은 매일 한두 곳씩 메일을 쓴다. 처음엔 솔직히 창피하기도 하고 어색했다. 본부장 명함을 내려놓는다는 것의 가장 작은 형태가 이런 메일 한 통이라는 걸 알았다. 그런데 며칠 하다 보니 다른 생각이 들었다 — 왜 이런 생각을 진작 못 했지?
그리고 떠오른 10년 전의 또 한 장면
라인 버블이 일본에서 수천만 다운로드를 찍던 시절에, 나는 매일 일본 게임 커뮤니티들을 직접 돌아다녔다. 무엇이 올라오는지, 누가 무슨 말을 하는지, 어떤 게임이 다음에 뜰지를 본인 눈으로 봤다. 그게 PD의 일이라고 생각했다. 10년이 지나, 본인 사이드 게임을 위해 다시 똑같은 일을 하고 있다는 걸 인디 커뮤니티 contact 페이지를 열다 깨달았다. 도구가 바뀌고 시대가 바뀌어도, 만든 사람이 사용자가 있는 곳까지 직접 걸어 들어가야 한다는 사실은 똑같았다.
YouTube Shorts, 그리고 Tool을 만들기 시작했다
Claude가 제안한 또 하나의 방법은 YouTube Shorts였다. 다른 채널과 다르게 올리자마자 실시간으로 반응이 보인다. 본인처럼 못 기다리는 사람에게는 묘하게 잘 맞는 형식이었다.
그러다 다른 일이 시작됐다. Shorts에 올릴 영상을 좀 더 쉽고 효율적으로 만들기 위한 도구를 아예 직접 만들기 시작한 것이다. 지금 동시에 진행 중인 앱과 서비스가 10개 정도 되는데, 그 10개 모두에 공통으로 들어갈 모듈로 이 도구를 설계하고 있다. 기능은 단순하다 — 실기기에서 앱을 실행해 짧은 영상을 촬영하고, 편집하고, 사운드를 얹고, 자막을 만드는 일련의 과정을 LLM으로 처리(Prompt)한다. 분명히 해두자면 이건 흔히 말하는 ‘콘텐츠 자동 생성’ 도구와는 완전히 다르다. 본인이 만든 앱의 진짜 화면을 실제로 촬영해서 가공하는 도구다. 동영상을 생성하는 개념이 아니고, LLM에게 prompt를 해서 게임의 가장 멋진 장면을 영상으로 촬영 하기 위함이다. 이것 때문에 게임의 씬을 모두 하이브리드로 제작하고 있다.
AI를 쓰다 보니 어느새 AI를 쓰는 도구 자체를 만들고 있다는 사실이, 시니어 입장에서 가장 흥미로운 변화였다.
HOL4B.com — 그 사이엔 이 일에 집중한다
솔직하게 한 줄을 적어둬야 할 것 같다. 나는 구직 중이다. 가능하면 엔터프라이즈 기업에서 C레벨 역할을 계속 하고 싶고, 얼마나 걸릴지는 나도 모른다. 그래서 그 사이에 이 일에 한번 제대로 집중해보기로 했다. HOL4B.com은 그 결심의 결과다. 아직 거의 비어 있는 한 페이지짜리 사이트인데, 앞으로 만들어가는 프로덕트들을 한 줄씩 붙여 나갈 자리로 쓸 생각이다. 본인 브랜드의 작은 작업장 같은 것이다.
1.3.0과 다음 두 개의 실험
Trains Out은 1.3.0을 준비 중이다. 가장 큰 변화는 게임 자체보다 비즈니스 쪽이다 — 사업자 등록을 마치고 광고 제거 유료 아이템을 붙여보는 일이다. 1센트짜리 광고 매출 옆에 처음으로 인앱 결제 버튼이 들어가는 셈이다. 같이 들어가는 작은 변화는 리더보드와 일일 챌린지 — 동기부여 장치 두 개다. 아마도 게임의 재미 요소나 기본 룰도 최대한 개선 해 볼것이다.
그리고 이미 시작해버린 다음 실험이 두 개 있다.
Gyeol (결 Seoul) — 카메라 앱이다. 사진을 원래도 좋아했고, 이번 기회에 본인이 좋아하는 필터 10종을 직접 카메라 앱으로 묶어보려 한다. 그런데 이 앱의 핵심은 11번째 필터다. 도미넌트 컬러를 실험적으로 필터화하는 것 — 사진의 지배색을 추출해 다시 사진 위로 돌려놓는 방식이다. 쓸 만한 결과가 나올지는 모르지만, 그게 사이드 프로젝트의 핵심이기도 하다. 이미 앱은 만들었고, 실제로 테스트 하기 위해서 서울의 곳곳을 돌아다니며 사진을 촬영해보고 있다.
Spell Stack — 단어 머징 게임이다. 아이가 단어를 다루는 앱을 갖고 싶다고 했는데, 그 요청을 1024 같은 숫자 머징 게임 메커니즘에 올려본 결과물이다. 숫자 대신 글자가 합쳐져서 단어가 된다. 가족이 첫 사용자였던 Trains Out과 마찬가지로, 이번에도 첫 사용자는 정해져 있다. 나름 공개된 학년별 중요 단어를 포함하고 있다.
그래서 — AI는 시니어의 무엇이 되었나
이 시리즈를 시작할 때 던진 질문은 단순했다. AI로 코딩은 진짜 가능한가, 상용화까지 갈 수 있나. 10여일간 게임 한 개와 첫 1센트로 답한 결론은 이렇다.
가능하다. 단, AI 단독으로는 끝까지 못 간다. 정책 심사(ATT, AdMob 어린이용 광고 등급), 게임성·손맛, 자기검열, 광고 수위, 사용자 있는 곳까지 직접 걸어 들어가는 마케팅 — 이 모든 영역에서 사람이 수없이 개입해야 했다. 다만, 그 사람이 25년차 시니어일 때, AI는 무서운 속도로 일했다. 도구를 고르는 일조차 위임할 수 있었고, 위임해도 결과가 나왔다. 10개 앱을 동시에 만들고 있다보니, 정말 AI의 힘은 대단하다는 생각이 든다.
요즘 자주 듣는 표현이 있다. ‘딸깍이’라거나, ‘코드에 0%도 손대지 않았다’라는. 그런 시기가 분명 오긴 할 것이다. 다만 본인이 10일을 체험해본 결론은 다르다. 지금 이 순간, AI는 시니어 개발자에게 무척 좋은 파트너가 생긴 것이다. 시니어나 누군가의 자리를 빼앗는 도구가 아니라, 시니어가 평생 쌓은 판단력을 최대한 끌어올려주는 파트너다. 라인 버블 시절 8개국 앱스토어 1위를 만들어봤고, 그 외에도 커머스·광고·모빌리티·제조 같은 도메인을 거쳤다. 그 모든 단계의 기억이 이번 5시간과 첫 1센트와 다음 두 개의 실험으로 다시 살아 돌아오는 중이다. 오래된 경험이 평준화의 대상이 아니라, AI 시대의 끝단을 책임지는 자리라는 것을 본인부터 다시 믿게 되었다.
3부 시리즈를 여기서 닫는다. Trains Out은 아직 시작이고, Gyeol과 Spell Stack은 곧 출시예정이다. 다운로드 링크와 짧은 플레이 영상은 영문 출시 페이지에 모아두었다.
지금 바로 할 수 있는 것
AI 시대일수록 사용자가 있는 곳을 찾아가라. 인디 커뮤니티든, 작은 페북 그룹이든, 회사 슬랙이든 — 만든 사람이 직접 가는 한 걸음을 AI는 대신 가주지 않는다.
한 가지 사이드 프로젝트를 진지하게 굴려봐라. AI가 본인의 무엇을 끌어올려주는지, 어떤 부분은 여전히 본인 손이 필요한지가 다 보인다.
이 글은 시리즈 2부다. 1부에서는 5시간 만에 게임을 만들고 앱스토어에서 거절당하기까지의 이야기였다. 2부는 그 거절 메일 이후부터 시작한다 — QA를 처음 도입하고, 두 번째 심사를 통과하고, 첫 1센트가 떨어지기까지. 그리고 그 1센트가 던진 이상한 질문에 대해서.
거절 메일 한 통에 멈칫했다
1.0의 거절 사유는 두 가지였다. 첫째는 ATT 처리 버그. 둘째가 더 당황스러웠다 — 50개로 만들어둔 레벨 중 11번째가 풀이 불가능하다는 것. 심사원이 직접 iPad로 풀어봤는데 안 풀리더라는 친절한 설명이 함께 왔다. 재심사 시에는 ATT가 동작하는 모습을 실기기에서 촬영해 영상으로 올려달라는 요청까지. 거절치고는 따뜻한 거절이었다.
1.1을 작업하면서 그제야 ‘QA’라는 단어를 처음 떠올렸다. 1.0 때는 이 정도 규모에 QA가 무슨 의미냐 싶었다. 그 자만이 정확히 어디서 깨졌는지 또렷하다. 거절 사유 두 가지를 꼼꼼히 짚으니 그 옆에 다른 것들이 줄줄이 보이기 시작했다. 추가로 3시간 정도가 더 들어갔다.
QA를 하면서 게임이 보이기 시작했다
가장 먼저 보인 것은 부끄러운 사실이었다. 게임이 너무 쉬웠다. ‘왜 이걸 하지?’라는 질문이 본인 입에서 나왔다. 한 번은 그냥 접을까 진지하게 생각했다.
그러다 마음을 가라앉히고 다시 봤다. 침착해지니 작은 것들이 보이기 시작했다. 한 손가락의 움직임, 첫 30초의 후킹, 손맛이라고 부르는 그 모호한 감각. 시니어 게임 PD 시절의 본능이 10여 년 만에 다시 깨어나는 느낌이었다. 지금도 이 게임이 정말 재밌느냐고 물으면 솔직히 말이 길어지지만, 그 짧은 시간 동안 정말 많은 것이 바뀌었고 방향성도 한 번 크게 틀었다.
1.1을 다시 올렸다. 심사 대기는 2일. 코드는 5시간이었는데 사람의 검토 시간은 그 9.6배다. 이 비율 자체가 AI 시대의 비대칭을 정확히 보여준다고 생각했다.
승인 메일을 받았다.
출시하고 나니 ‘너무 대충 만들었다’
승인의 기쁨은 짧았다. 1.1이 앱스토어에 올라간 그날, 본인 폰에서 게임을 켜자마자 든 감상 — 아무리 실험적이라도 너무 대충 만들었다.
이틀에 걸쳐 이것저것을 손봤다. 광고를 한 번 보면 생명력 두 개를 주는 시스템을 넣었는데, 그 광고들 중 수위가 꽤 높은 것이 끼어 있었다. AdMob 콘솔에 들어가 보니 10년 전과는 완전히 다른 화면이 기다리고 있었다. 설정해야 할 것이 많고, 정책도 복잡하고, 여기서 또 시간이 갔다.
손보고 나니 4세에서 9세 아이도 할 만큼 광고가 동화스러워졌다. 그게 1.2 작업의 결과 요약이다.
첫 $0.01
1.2가 올라간 뒤 어느 시점에 AdMob 대시보드의 숫자가 0에서 0.01로 바뀌었다.
라인 버블 시절 8개국 앱스토어 1위를 만들었던 사람의, 10여 년 후 첫 매출이 1센트였다.
그 1센트의 진짜 정체는 이거다. 첫 사용자는 와이프와 아이였다. 출시 직후에 두 사람이 폰을 켜고 게임을 했고, 그 광고가 1센트로 바뀌었다. 글을 쓰는 지금은 출시 약 4일이 지났고 누적은 $10을 조금 넘었다. 사용자는 아직 거의 없다. 그게 솔직한 수치다.
10여 년 전의 데자뷰
본부장이나 CTO, CISO, CPO로 일하던 시절에는 정말 큰 결정을 하거나 수백 명의 방향을 잡아야 했다. 지금은 다르다. 나 혼자, 나만 생각하면서 일한다. 무게가 덜하고, 처음으로 내 위주로 생각하는 게 가능해졌고, 그래서 내가 진짜 하고 싶은 일을 할 수 있다.
이번 사이드 프로젝트의 진짜 동기는 거기에 있었다는 걸 첫 1센트를 보고서야 깨달았다.
그리고 이상하게도, 라인 버블을 만들던 시절의 한 장면이 떠올랐다. 그때는 “누가 미국 인기 게임을 더 빨리 베끼나” 같은 묘한 챌린지가 있었다. Don’t Touch the Spikes라는 게임이 미국에서 인기를 끌고 있을 때, 나는 AI 도움 같은 것 없이 2~3시간 만에 그걸 베껴냈던 기억이 있다.
10여 년이 지나, AI를 옆에 두고 같은 종류의 일을 5시간에 했다. 같은 일이지만 같지는 않았다. 그때의 2~3시간은 손이 빨라서였고, 지금의 5시간은 도구가 빨라서다. 그리고 그 도구를 끝까지 끌고 가려면 결국 손이 빨랐던 시절의 본능이 필요했다.
그래서 무엇이 달라졌나
2부의 결론은 짧다. AI로 코딩은 가능한 시대지만, 상용화는 조금 다르다. 정책도, 게임성도, 광고 수위도, 자기검열도 — 전부 AI 단독으로 끝까지 갈 수 없는 영역이었다. 그리고 그 사람이 25년차 시니어일 때, AI는 정말 무서운 속도로 일한다.
3부에서는 그 다음 이야기 — 출시 후 마케팅, HOL4B.com, 그리고 시작해버린 다음 실험들 — 로 이어진다.
지금 바로 할 수 있는 것
AI로 만든 첫 빌드에 반드시 사람의 QA를 한 사이클 넣어라. 5시간 만에 만들었더라도 1시간은 본인 손으로 모든 화면을 굴려봐라. 시니어일수록 이 단계를 건너뛰기 쉽다. 그리고 AdMob을 마지막에 붙이지 말고 처음부터 광고 정책을 한번 훑어봐라. 10년 전과 완전히 다르다. 어린이 카테고리라면 광고 등급을 직접 좁혀야 한다.
이 글은 시리즈 1부다. 25년차 시니어 개발자가 잠깐의 공백 동안 사이드로 iOS 게임을 만들면서 “AI로 코딩이 진짜 가능한가, 상용화까지 갈 수 있나”라는 질문 하나를 직접 시험해본 회고다. 1부는 게임을 만들기 시작해서 첫 심사 거절을 받기까지의 이야기다.
회의 대신 Claude를 열었다
한 회사의 CTO/전무이사 임기를 마치고 잠깐의 공백이 생겼다. 다음 자리를 찾는 동안 무엇을 할까 한참 생각했는데, 이상하게도 손에 잡힌 것은 자기소개서가 아니라 Claude였다.
25년 만이었다. 본부장 회의도, 조직도, 분기 KPI도 없는 자리에서 코드를 짠다는 것. 그 감각이 어떤 건지 이미 기억이 흐릿했다. 그래서 그냥 한번 열어봤다.
다른 도구는 거의 쓰지 않았다
처음 며칠은 손풀기였다. AI 코딩이 어디까지 왔는지 짚어보는 단계. Prompt를 어떻게 쓰는지, context를 어떻게 관리하는지, harness가 무엇인지를 익혔다. GSD, gstack, PaperClip, OpenClaw 같은 것들을 이것저것 깔아보고 직접 써봤다. 손에 맞는 것만 남겼다.
직전까지 회사에서 AX 관련 에이전트를 설계하고 실행하고 있었지만, 개인 개발자로서의 AI 사용과는 범위도 관점도 많이 다르다.
최종적으로 손에 남은 것은 두 개. 전략과 기획을 잡을 때는 gstack, 그 외 모든 작업은 Claude — Chat과 Code 양쪽. React Native, Expo, AdMob 같은 스택도 내가 비교해서 고른 게 아니라 AI가 제안한 것을 OK했을 뿐이다.
25년차 개발자가 가장 어색했던 지점이 바로 여기다. 도구 선정마저 위임할 수 있다는 것.
지금은 로컬 맥북에 CI/CD까지 세팅했지만, 초기에는 Claude가 필요하다고 하면 그냥 줬다. EAS 스타터 킷 $19도 그렇게 결제했다.
곁다리 하나. gstack을 쓰다 보면 어느 순간 YCombinator 쪽 채용 권유 메시지가 화면에 뜬다. 좋은 도구를 무료로 제공하면서 그 도구를 쓰는 사람을 채용 후보로 보는 흐름이 인상적이었다. AI 시대의 인재 발굴은 이력서가 아니라 사용 흔적이구나. 짧은 메모를 남겼다. 실제로 회신이 되지는 않겠지만, 제안해줘서 고맙다고 답을 보냈다.
5시간 — 그리고 Trains Out
최초 아이디어는 혼자 생각했다. ‘무엇을 만들지?’
미국 앱스토어에서 한참 캐주얼 게임을 정찰하다가 Arrows Out이라는 작은 게임을 봤다. 작은 게임이라기엔 천만 다운로드가 넘었고, 이름만 살짝 바꾼 카피가 정말 많았다. 머릿속에서 자연스럽게 연쇄가 일어났다. Arrows를 Snakes로? Trains로? 그러다 일본에서 3년을 살았던 시절이 떠올랐다.
“그래, 일단 Trains로 시작해서 나중에 예쁜 일본식 기차 그래픽으로 바꾸자.”
Trains Out이라는 이름은 그렇게 정해졌다.
거기서부터 첫 빌드까지, 5시간. 정확히 말하면 전략·기획·개발이라는 단계를 한 호흡에 꿰어서 5시간이었다. 숫자만 들으면 대단해 보이지만, 그 5시간 동안 AI가 알아서 한 것이 아니다. AI는 끊임없이 질문했고, 나는 끊임없이 결정해야 했다. 25년의 경험과 지식에서 길어 올린 의견을 계속 쥐여줘야 했다. AI도 실수를 정말 많이 했다. 잘못된 라이브러리, 잘못된 API, 같은 자리를 한 번 더 도는 코드. 시니어가 “여기 틀렸어”라고 짚어주는 횟수만큼 5시간은 채워졌다.
그래도 빌드는 굴러갔다. 시뮬레이터에 설치된 첫 화면을 보면서 든 생각은 묘했다.
이게 정말 5시간으로 가능한 시대구나. 25년차 본인조차 받아들이는 데 시간이 걸렸다.
이 5시간의 설계도 — 실제 문서 공개
5시간을 가능하게 한 문서들이 있다. AI에게 건넨 프로젝트 지시서(CLAUDE.md), 원본 기획서(SPEC.md), 그리고 AI와 내가 같이 내린 53개의 결정 기록(DECISIONS.md)을 가공 없이 그대로 공개한다.
📄 CLAUDE.md — AI에게 건넨 프로젝트 지시서 전문 (클릭하여 펼치기)
# TrainsOut — 열차 탈출
## Status
**v1.2.0** — 한국 애플 앱스토어 출시 완료, 운영 중.
프로덕션 안정성이 최우선. **출시 빌드에 영향 줄 수 있는 변경은 반드시 사용자 확인.**
| 항목 | 내용 |
|---|---|
| 앱 이름 | 열차 탈출 - 퍼즐 게임 |
| 패키지명 | `com.hol4b.trainescapepuzzle` |
| 타겟 | iOS 전용 (한국 앱스토어). `android/` 폴더는 stale, 무시 |
| 카테고리 | 퍼즐 (4+) |
| 수익화 | AdMob only (배너 + 전면광고 + 리워드 광고) |
| 빌드 | **로컬 EAS 빌드만** (`eas build --local`). 클라우드 빌드 과금 금지 |
## Stack
- React Native + **Expo SDK 55** + TypeScript
- **Zustand** (`gameStore`, `devStore`)
- **Reanimated v4** (worklet 기반 애니메이션)
- **expo-audio** (BGM + SFX)
- **Expo Router** (파일 기반 라우팅)
- **react-native-google-mobile-ads 16.3.1** (서비스 추상화로 mock 폴백 지원)
- AsyncStorage (레벨 진행 저장)
## Directory
```
app/ Expo Router (index, levels, game/[level], settings)
components/ Grid, Train, Track, HUD, DevPanel, TutorialOverlay
store/ gameStore.ts, devStore.ts
engine/ collisionDetect.ts
hooks/ useSound, useInterstitialAd, useRewardedAd
services/ ad/ + sound/ (real + mock 이중 구현)
data/ generated.ts (200 레벨, gridSize 10×10 ~ 24×24)
assets/sounds/ BGM, move, collision, win, fail, bomb_explode
utils/i18n.ts 한국어 문자열
docs/SPEC.md 2026-03-27 원본 기획 스냅샷 (⚠️ 현재 코드와 다름, 참고용)
demo/ Shorts 녹화용 씬 코드 (프로덕션 빌드 제외) — 아직 없음, 도입 예정
shorts/ Shorts 녹화 툴링 (Node 스크립트) — 아직 없음, 도입 예정
```
## Data Model
```typescript
type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';
type TrainState = 'IDLE' | 'MOVING' | 'ESCAPED';
type TrainColor = 'RED' | 'BLUE' | 'GREEN' | 'YELLOW' | 'PURPLE' | 'ORANGE' | 'CYAN';
interface Train {
id: string;
row: number;
col: number;
direction: Direction;
color: TrainColor;
state: TrainState;
length: number; // 열차 칸 수
offsets?: [number,number][]; // L/S/U형 모양 오프셋 (직선 열차면 undefined)
}
interface Level {
id: number;
gridSize: number; // 실제 10~24
trains: Train[];
par?: number; // 3-star 기준 이동수
timeLimit?: number; // 일부 레벨 제한 시간
}
```
## 게임 메커닉 (v1.2.0 실제)
- **탭으로 열차 이동**: 방향대로 셀 단위로 이동, 경계 이탈 시 `ESCAPED`, 전체 탈출 시 `WIN`
- **충돌 감지**: 경로상 다른 열차 있으면 충돌 → 하트 -1, 원위치 복귀
- **하트**: 게임당 3개. `hearts === 0` 시 `FAIL`
- **폭탄**: Level 11+ 등장. 카운트다운, 시간 내 탈출 못 하면 폭발 (하트 감소)
- **타이머**: 일부 레벨에 제한 시간
- **별점**: `moves ≤ par → ⭐⭐⭐`, `≤ par+2 → ⭐⭐`, 이상 → `⭐`
- **튜토리얼 오버레이**: Level 1 (기본 조작), Level 11 (폭탄 설명)
- **드래그+줌**: 큰 그리드는 핀치 줌 + 팬, 작은 그리드는 화면에 맞춤 (`Grid.tsx`)
- **DevPanel**: 홈 로고 5연타 → `unlockAll`, 시간 조작 등 개발자 토글
## 수익화 (실제, SPEC 과 다름)
- **Banner**: 게임 화면 하단 (`SafeBannerAd` + `ErrorBoundary` 로 네이티브 모듈 누락 시 안전 fallback). Unit ID: `ca-app-pub-XXXXXXXXXX/XXXXXXXXXX`
- **Interstitial**: **5레벨 클리어마다** (`useInterstitialAd`). SPEC 의 "hearts=0 자동 광고" 와 다름
- **Rewarded**: 실패 오버레이의 **"광고 보고 하트 2개 복구"** 선택 버튼 (`useRewardedAd`). 강제 아님, 사용자 선택
- **SIMULATOR=1** env: AdMob 플러그인 자체 로드 안 함 (`app.config.js`). 시뮬레이터 개발/녹화 모드
- **Service abstraction** (`services/ad/`): 네이티브 모듈 없을 때 자동으로 mock 으로 폴백
## 애니메이션 타이밍
- 이동: 200ms (`Easing.out(Easing.quad)`)
- 탈출: 85ms/칸 linear
- 폭탄 긴급 펄스: 250ms 반복 (countdown ≤ 3)
- **`Date.now()` 등 비결정 시간 호출 없음** → 녹화 재현성 OK
---
## Shorts 파이프라인 (2026-04-12 도입, WIP)
마케팅용 YouTube Shorts 자동 제작 파이프라인. 주제 문장 → Claude Code 가 씬 코드 작성 → iOS 시뮬레이터 녹화 (`xcrun simctl io booted recordVideo`) → ffmpeg 후처리 → `out/<slug>.mp4`.
### 격리 원칙 (프로덕션 0 영향)
- 모든 Shorts 관련 코드는 **`/demo` 와 `/shorts`** 에만 존재
- 프로덕션 코드 (`app/`, `components/`, `store/`, `engine/`, `hooks/`, `services/`) 는 **`/demo` 를 절대 import 하지 않음**
- `.easignore` 가 `/demo`, `/shorts` 를 빌드 산출물에서 제외 (도입 시)
- 프로덕션 파일에 남는 유일한 수정은 **`testID` 프롭 추가** 뿐이며, 이는 런타임 영향 0 (메타데이터)
### testID 규칙 — **절대 제거하지 말 것**
Shorts 녹화의 `TapEmulator` 가 자동으로 탭할 엘리먼트를 찾기 위한 메타데이터입니다. 향후 E2E 테스트에도 재활용됩니다.
| 파일 | testID | 대상 |
|---|---|---|
| `components/Train.tsx` | `train-${id}` | 모든 열차 TouchableOpacity (L자형 `CellAnimated`, 직선 `StraightTrainAnimated` 양쪽) |
| `app/index.tsx` | `btn-logo` | 로고 탭 영역 (DevPanel 잠금 해제용) |
| `app/index.tsx` | `btn-start` | 시작하기/계속하기 버튼 |
| `app/index.tsx` | `btn-levels` | 레벨 선택 버튼 |
| `app/game/[level].tsx` | `btn-back` | 홈으로 돌아가기 |
| `app/game/[level].tsx` | `btn-next` | WIN 오버레이 다음 레벨 |
| `app/game/[level].tsx` | `btn-retry` | WIN 오버레이 다시 도전 |
| `app/game/[level].tsx` | `btn-watch-ad` | FAIL 오버레이 광고 시청 |
| `app/game/[level].tsx` | `btn-restart-one` | FAIL 오버레이 1레벨부터 다시 |
`testID` 는 React Native 에서 렌더·이벤트에 영향을 주지 않는 순수 메타데이터입니다. 번들 사이즈는 바이트 수준 증가, 런타임 비용 0.
### 디렉토리 (도입 시)
- **`/demo/`** — 앱 내 데모 모드
- `entry.tsx` — `SHORTS_MODE=1` 전용 루트
- `runtime/SceneHost.tsx`, `defineScene.ts`, `TapEmulator.tsx`, `Subtitles.tsx`, `signals.ts`
- `scenes/*.tsx` — 씬 파일 (수동 또는 LLM 생성)
- **`/shorts/`** — 호스트 머신 Node 스크립트
- `core/` — schema, postprocess (ffmpeg), cli, LLM 프롬프트
- `adapter-expo/` — boot, launch, record (simctl)
- `out/` — 완성된 mp4
- `.cache/` — raw 녹화 중간 파일
### 연출 자유도
마케팅 목적이라 **"가상 레벨" 구성 허용**. 실제 Level 1 은 10×10 이지만, 임팩트를 위해 3×3 씬을 씬 파일 내에서 임의 구성 가능. **게임 룰/방식은 유지**.
### BGM 및 자막
- BGM 은 `assets/sounds/bgm.mp3` 등 앱 자체 트랙을 ffmpeg 후처리 단계에서 믹싱 (시뮬레이터 오디오는 녹화에 안 잡힘)
- 자막은 ffmpeg `drawtext` 번인 기본, 특수 연출 시 앱 내 오버레이
### 해상도
- 1080×1920 (9:16), iPhone 15 Pro 시뮬레이터 기준 크롭
- 노치 포함, 홈 인디케이터 크롭
---
## gstack
Use /browse skill from gstack for all web browsing.
Never use mcp__claude-in-chrome__* tools.
Available skills:
/office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review,
/design-consultation, /design-shotgun, /review, /ship, /land-and-deploy,
/canary, /benchmark, /browse, /connect-chrome, /qa, /qa-only,
/design-review, /setup-browser-cookies, /setup-deploy, /retro,
/investigate, /document-release, /codex, /cso, /autoplan,
/careful, /freeze, /guard, /unfreeze, /gstack-upgrade
If skills not working: `cd .claude/skills/gstack && ./setup`
### Skill routing
When the user's request matches an available skill, ALWAYS invoke it using the Skill
tool as your FIRST action. Do NOT answer directly, do NOT use other tools first.
The skill has specialized workflows that produce better results than ad-hoc answers.
Key routing rules:
- Product ideas, "is this worth building", brainstorming → invoke office-hours
- Bugs, errors, "why is this broken", 500 errors → invoke investigate
- Ship, deploy, push, create PR → invoke ship
- QA, test the site, find bugs → invoke qa
- Code review, check my diff → invoke review
- Update docs after shipping → invoke document-release
- Weekly retro → invoke retro
- Design system, brand → invoke design-consultation
- Visual audit, design polish → invoke design-review
- Architecture review → invoke plan-eng-review
📄 SPEC.md — 원본 기획서 (2026-03-27 스냅샷, 클릭하여 펼치기)
# 열차 탈출 – Claude Code Project Spec
## Project Overview
선로 위 열차들을 충돌 없이 전부 탈출시키는 순서 퍼즐 게임.
React Native + Expo, iOS 개발.
**타겟: 한국 애플 앱스토어**
---
## 앱 기본 정보
| 항목 | 내용 |
|------|------|
| 앱 이름 | 열차 탈출 |
| 패키지명 | com.hol4b.trainescapepuzzle |
| 카테고리 | 퍼즐 |
| 연령 등급 | 전체 이용가 |
| 출시 스토어 | 애플 앱스토어 (한국) |
| 언어 | 한국어 |
---
## Tech Stack
| Layer | Choice | Reason |
|-------|--------|--------|
| Framework | React Native + Expo (SDK 51+) | 빠른 빌드, 단일 코드베이스 |
| Language | TypeScript | 타입 안전성 |
| State | Zustand | 경량 게임 상태 관리 |
| Animation | React Native Reanimated v3 | 60fps 열차 이동 |
| Ads | react-native-google-mobile-ads | AdMob 배너 + 전면광고 |
| Storage | AsyncStorage | 레벨 진행 저장 |
| Navigation | Expo Router | 파일 기반 라우팅 |
---
## Directory Structure
```
/app
index.tsx # 홈 화면
game/[level].tsx # 게임 플레이 화면
levels.tsx # 레벨 선택
settings.tsx # 설정 (사운드)
/components
Grid.tsx # 선로 그리드
Train.tsx # 열차 컴포넌트 (SVG + 애니메이션)
Track.tsx # 선로 배경 셀
HUD.tsx # 하트, 레벨, 이동수
BannerAd.tsx # 하단 배너 광고
CrashEffect.tsx # 충돌 이펙트
InterstitialGate.tsx # 생명 0 → 전면광고 게이트
/store
gameStore.ts # Zustand 게임 상태
/engine
levelValidator.ts # 레벨 풀이 검증 (BFS)
collisionDetect.ts # 충돌 감지
levelGenerator.ts # 레벨 데이터 파싱
/data
levels/
easy.ts # 레벨 1-50 (3x3~4x4)
medium.ts # 레벨 51-150 (4x4~5x5)
hard.ts # 레벨 151-300 (5x5~6x6)
/assets
sounds/
train_move.mp3
train_horn.mp3
crash.mp3
/hooks
useGameLoop.ts
useSound.ts
useAd.ts # 광고 로드/표시 훅
```
---
## Core Game Engine Spec
### 테마 매핑
| Arrow Out 원작 | 열차 탈출 |
|---------------|-----------|
| 화살표 | 열차 (색상으로 구분) |
| 이동 방향 | 열차 진행 방향 |
| 그리드 셀 | 선로 교차점 |
| 충돌 | 열차 충돌 사고 |
| 화면 밖 이탈 | 역 도착 / 탈출 성공 |
| 하트 | 생명 (게임당 3개) |
### Data Model
```typescript
type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';
type TrainState = 'IDLE' | 'MOVING' | 'ESCAPED';
type TrainColor = 'RED' | 'BLUE' | 'GREEN' | 'YELLOW' | 'PURPLE';
interface Train {
id: string;
row: number;
col: number;
direction: Direction;
color: TrainColor;
state: TrainState;
}
interface Level {
id: number;
gridSize: number; // 3~6
trains: Train[];
par?: number; // 3-star 기준 이동수
}
interface GameState {
level: Level;
trains: Train[];
hearts: number; // 게임 시작 시 3, 충돌 시 -1
moves: number;
status: 'PLAYING' | 'WIN' | 'FAIL';
}
```
### 게임 루프
```
게임 시작
→ hearts = 3
train[id] 탭
→ state = MOVING
→ 셀 단위 이동, 각 셀 진입 시 충돌 체크
충돌 감지:
hearts -= 1
CrashEffect 재생 (flash + haptics + 충돌음)
state = IDLE (원위치 복귀)
hearts === 0 → status = FAIL
정상:
계속 이동
→ 경계 이탈 → state = ESCAPED
→ 전체 ESCAPED → status = WIN
status === FAIL
→ InterstitialGate 발동
→ 전면광고 노출 (AdMob Interstitial)
→ 광고 종료 후 → hearts = 3, 현재 레벨 재시작
```
### 충돌 감지 규칙
- 셀 기반 이동 (픽셀 단위 아님)
- 이동 경로 전체 셀에 다른 열차 존재 여부 사전 체크
- 순차 탭만 허용 (동시 이동 불가)
---
## Level Data Format
```typescript
export const easyLevels: Level[] = [
{
id: 1,
gridSize: 3,
trains: [
{ id: 't1', row: 1, col: 0, direction: 'LEFT', color: 'RED', state: 'IDLE' },
{ id: 't2', row: 0, col: 1, direction: 'UP', color: 'BLUE', state: 'IDLE' },
{ id: 't3', row: 1, col: 2, direction: 'RIGHT', color: 'GREEN', state: 'IDLE' },
],
par: 3,
},
]
```
---
## Monetization: AdMob Only
```
광고 구성:
├── 배너 광고 (BannerAd)
│ 위치: 게임 화면 하단 상시 노출
│ 사이즈: BANNER (320x50)
│
└── 전면광고 (Interstitial)
트리거: 생명 3개 모두 소진 (hearts === 0)
플로우: FAIL 판정 → 광고 노출 → 광고 종료 → 생명 3개 복구 → 레벨 재시작
주의: 광고 로드 실패 시에도 생명 복구 후 재시작 (광고 강제 X)
인앱결제: 없음
부스터: 없음
```
### useAd.ts 구현 가이드
```typescript
// hooks/useAd.ts
import { useInterstitialAd, TestIds } from 'react-native-google-mobile-ads';
const AD_UNIT_ID = __DEV__
? TestIds.INTERSTITIAL
: 'ca-app-pub-XXXXXX/XXXXXX'; // 실제 AdMob Unit ID
export function useAd() {
const { isLoaded, isClosed, load, show, error } = useInterstitialAd(AD_UNIT_ID);
// 광고 사전 로드 (게임 시작 시 호출)
const preload = () => load();
// 생명 0 시 호출
const showOnFail = async (onComplete: () => void) => {
if (isLoaded) {
show();
// isClosed 감지 후 onComplete 호출
} else {
// 광고 없으면 바로 복구
onComplete();
}
};
return { preload, showOnFail, isLoaded };
}
```
### InterstitialGate 플로우
```
hearts === 0 발생
→ gameStore: status = 'FAIL'
→ InterstitialGate 컴포넌트 마운트
→ 광고 로드 완료 여부 확인
완료: 전면광고 표시
미완료: 스피너 0.5초 → 바로 복구
→ 광고 닫힘 이벤트 수신
→ gameStore: hearts = 3, status = 'PLAYING', 레벨 초기화
```
---
## UI/UX Spec
### 색상 팔레트 (다크 테마)
```
Background: #0f1923
Grid/Track: #1a2a3a
Track Line: #2d4a6e
Train RED: #e63946
Train BLUE: #457b9d
Train GREEN: #2a9d8f
Train YELLOW:#f4a261
Train PURPLE:#9b72cf
Heart full: #e63946
Heart empty: #2d3748
Text: #f0f4f8
```
### 화면 구성
1. 홈: 로고, 시작하기, 레벨 선택, 설정
2. 레벨 선택: 그리드, 별점, 잠금
3. 게임: 선로 그리드 중앙, 상단 HUD (하트/레벨/이동수), 하단 배너광고
4. 클리어: "탈출 성공! 🚂" + 별점 + 다음/재도전
5. 실패 (hearts=0): InterstitialGate → 광고 → 자동 재시작
### HUD 하트 표시
```
hearts = 3 → ❤️❤️❤️
hearts = 2 → ❤️❤️🖤
hearts = 1 → ❤️🖤🖤
hearts = 0 → 🖤🖤🖤 → InterstitialGate 발동
```
### 애니메이션
- 이동: 200ms linear
- 충돌: red flash + Haptics.impactAsync(Heavy) + 충돌음 + 하트 -1 애니메이션
- 탈출: 화면 밖 슬라이드 아웃
- 클리어: 별 3개 드롭 + 경적음
---
## Development Phases
### Phase 1 – Core Engine (1주)
- [ ] `npx create-expo-app TrainEscape --template expo-template-blank-typescript`
- [ ] 의존성: zustand, react-native-reanimated@3, expo-router, expo-haptics, expo-av
- [ ] Grid + Track 컴포넌트 (SVG 선로)
- [ ] Train 컴포넌트 SVG 직접 구현
- [ ] 충돌 감지 엔진
- [ ] Zustand 상태 관리 (hearts 포함)
- [ ] 레벨 10개 동작 검증
### Phase 2 – Game Loop & UX (1주)
- [ ] 하트 시스템 (3개, 충돌 시 -1)
- [ ] hearts === 0 → FAIL 판정
- [ ] WIN/FAIL 화면 전환
- [ ] 별점 시스템 (par 기반)
- [ ] 레벨 선택 화면
- [ ] AsyncStorage 진행 저장
- [ ] 레벨 50개
### Phase 3 – AdMob 연동 (2일)
- [ ] react-native-google-mobile-ads 설치 및 설정
- [ ] BannerAd 컴포넌트 (게임 화면 하단)
- [ ] useAd 훅 구현
- [ ] InterstitialGate 컴포넌트 구현
- [ ] 테스트 광고 ID로 동작 검증
- [ ] 실제 AdMob Unit ID 교체
### Phase 4 – Polish & Submit (3일)
- [ ] 효과음 (expo-av)
- [ ] 햅틱 피드백
- [ ] Daily Challenge (오늘의 퍼즐)
- [ ] 앱 아이콘 + 스플래시
- [ ] 개인정보처리방침 페이지 (blog.hol4b.com에 게시)
- [ ] 한국 앱스토어 메타데이터 작성
- [ ] TestFlight 베타 테스트 → 앱스토어 심사 제출
---
## 한국 앱스토어 메타데이터
- 이름: `열차 탈출 - 퍼즐 게임`
- 부제: `선로를 탈출하라`
- 키워드: `퍼즐,뇌훈련,열차,탈출,두뇌,논리,기차,퍼즐게임,두뇌게임,브레인`
- 연령: 4+
- 개인정보처리방침 URL: 필수 (AdMob 사용으로 인해 심사 필수 항목)
---
## Known Risks & Mitigations
| 리스크 | 대응 |
|--------|------|
| 전면광고 UX 거부감 | hearts=0 일 때만 노출 → 빈도 낮아 수용 가능 |
| 광고 로드 실패 | 실패 시에도 생명 복구 후 재시작 (강제 광고 X) |
| Reanimated 성능 | worklet 기반 애니메이션 |
| 레벨 제작 시간 | 50개 수동 → BFS 자동 생성기 |
| 심사 거절 | 개인정보처리방침 URL 사전 준비 (AdMob 사용 시 필수) |
---
## Claude Code 첫 프롬프트
```
CLAUDE.md를 읽고 Phase 1부터 시작해줘.
1. Expo + TypeScript 프로젝트 초기화
2. 의존성 설치 (zustand, reanimated@3, expo-router, expo-haptics, expo-av)
3. 3x3 그리드에 열차 3개 렌더링
4. 열차는 React Native SVG로 직접 구현 (이미지 에셋 없이)
5. 탭 → 방향으로 이동 → 경계 이탈 시 ESCAPED 기본 동작 확인까지
```
📄 DECISIONS.md — AI와 같이 내린 53개의 결정 (클릭하여 펼치기)
# Decisions Register
<!-- Append-only. Never edit or remove existing rows.
To reverse a decision, add a new row that supersedes it.
Read this file at the start of any planning or research phase. -->
| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |
|---|------|-------|----------|--------|-----------|------------|---------|
| D001 | M001 | integration | AdMob milestone placement | Keep AdMob integration out of M001 and schedule it for a later milestone that assumes a development/native build path | Current library guidance confirms AdMob flows are not an Expo Go-first path, so mixing monetization into the first milestone would dilute proof of the core puzzle loop and add native build complexity too early. | No | collaborative |
| D002 | M001 | architecture | M001 sequencing focus | Plan M001 around proving puzzle feel and readability before broader game-shell or monetization work | The user emphasized that the first playable must not feel sluggish or frustrating, so early slices should be player-visible vertical increments that prove responsiveness, clarity, and fun rather than invisible technical layers. | Yes — if later testing shows shell or UX framing must move earlier | collaborative |
| D003 | M001/S01 | requirement | R001 | validated | S01 delivered a real home→level-1→tap-to-move→escape loop with deterministic store transitions, SVG board rendering, and passing movement/store tests plus successful Expo Metro boot proof. | Yes | agent |
| D004 | M001/S01 | requirement | R003 | validated | S01 established immediate tap responsiveness for the first playable loop by wiring direct train press handling to deterministic movement/store updates, exposing visible status/move labels, and proving the runtime boots cleanly in Expo with green automated tests. | Yes | agent |
| D005 | M001/S02/T02 | observability | How movement results are retained in the game store | Persist the full engine MovementResolution as `lastResolution` alongside the compact `lastInteraction` feedback snapshot. | This keeps the pure engine as the single source of truth while giving downstream UI and debugging surfaces direct access to path and collision metadata without recomputation or duplicated derivation logic. | Yes | agent |
| D006 | M001/S02 | requirement | R002 | validated | S02 delivered deterministic cell-based collision/movement contracts in the pure engine, persisted structured movement resolutions and last-interaction feedback in Zustand, exposed blocked/escaped/no-op outcomes in the game UI, and re-verified the behavior with 15 passing movement/store tests plus Expo Metro startup evidence. | Yes | agent |
| D007 | M001/S03 | requirement | R004 | validated | S03 added pure board-presentation derivation, visible exit markers, directional train visuals, highlighted attempted/blocked paths, collision-cell overlays, board legend and feedback cards, then proved the contract with passing boardPresentation + movement Jest suites and clean non-interactive Expo Metro startup on pinned port 8081. | Yes | agent |
| D008 | M001/S04 | requirement | R005 | validated | S04 expanded the easy pack into multiple handcrafted solvable levels, wired the pack into the home and game flows with deterministic replay/next-level behavior and malformed-param fallback, and verified it with passing Jest suites plus successful Expo Metro startup proof. | Yes | agent |
| D009 | M001/S05 | requirement | R005 | validated | S05 locked the multi-level easy-pack flow with integrated regression coverage for home entry, malformed fallback, replay reset, next-level progression, completed-pack behavior, and verified Expo Metro startup on the pinned 8081 path. | Yes | agent |
| D010 | M002 | pattern | 레벨 해금 방식 | 순차 해금 — 이전 레벨 클리어해야 다음 레벨 열림 | 선형 진행이 퍼즐 난이도 곡선 관리에 유리하고, 구현이 단순함 | Yes | collaborative |
| D011 | M002/S01 | requirement | R006 | validated | S01 added hearts to the shared game snapshot, surfaced explicit PLAYING/FAIL copy plus a fail CTA on the game screen, locked board input while FAIL, and verified the behavior with passing heart-system, integrated-flow, movement, and screen-level Jest suites plus clean TypeScript checks. | Yes | agent |
| D012 | M002/S02 | requirement | R007 | validated | S02 added deterministic WIN-only par-based star summaries, rendered the result card with replay/next-level or pack-complete branching, and proved the behavior with passing starRating, integratedPlayFlow, and gameScreenFailLoopScreen Jest suites. | Yes | agent |
| D013 | M002/S03 | requirement | R008 | active | S03 delivered the AsyncStorage-backed progress contract, home/levels/game route integration, and passing progression/levels-screen coverage, but the slice-level verification contract is not fully complete because gameScreenFailLoopScreen.test.tsx remains failing and the planned chained typecheck did not run. Requirement evidence advanced substantially, but full validation proof is not yet closed. | Yes | agent |
| D014 | M002/S04 | requirement | R005 | validated | S04 expanded the easy content pack from 6 to 50 ordered levels, preserved deterministic anchor fixtures, updated progression/UI regressions for first/middle/last and late-pack behavior, and re-verified the 50-level contract with passing levelPack, integratedPlayFlow, levelsScreen, progressPersistence, and TypeScript checks. | Yes | agent |
| D015 | M002/S05 | requirement | R005 | validated | S05 closed the remaining M002 proof gap by passing the mounted game-screen regression harness, the focused integrated progression/persistence/result/fail regression set, and TypeScript checks against the current 50-level easy pack. This now proves the real home→levels→play→FAIL→restart→WIN→next-level→relaunch persistence loop with the expanded pack. | Yes | agent |
| D016 | | requirement | R005 | validated | S04/S05 now prove the expanded easy-pack gameplay loop with 50 playable levels, mounted/integrated regression coverage for progression/fail-restart/win/next-level/relaunch persistence, and passing `levelPack.test.ts`, `integratedPlayFlow.test.ts`, `levelsScreen.test.tsx`, `progressPersistence.test.ts`, `starRating.test.ts`, `heartSystem.test.ts`, `movement.test.ts`, plus `npx tsc --noEmit`. | Yes | agent |
| D017 | M003 S01 T01 | architecture | AdMob boundary exposure for game UI | Expose banner rendering through a typed runtime descriptor in hooks/useAd.ts and keep production ad unit IDs in Expo config extra instead of hardcoding SDK calls into screen components. | This keeps the screen and later banner wrapper mock-friendly, prevents silent production fallback to test IDs, and leaves a deterministic debug label that tests can assert without depending on native ad behavior. | Yes | agent |
| D018 | M003/S01 | requirement | R010 | active | M003/S01 integrated the bottom banner ad region into the game screen through a typed runtime descriptor, config-backed ad unit IDs, deterministic fallback messaging, and passing game-screen/levels-screen plus TypeScript verification. This advances the monetization integration requirement, but full validation still depends on the hearts=0 interstitial gate in later slices. | Yes | agent |
| D019 | M003 S02 planning | architecture | FAIL restart ownership during interstitial gating | Keep store/gameStore.ts as the source of FAIL and restart semantics, but let a dedicated InterstitialGate component own when restartLevel() fires by consuming a typed interstitial descriptor/controller from hooks/useAd.ts. | The store already provides correct hearts=0 and restart behavior. Putting ad lifecycle branching in hooks/useAd.ts and restart timing in a gate component preserves the S01 ad-boundary pattern, keeps app/game/[level].tsx mock-friendly, and prevents early restart bypassing the interstitial or duplicating fallback logic in the screen. | Yes | agent |
| D020 | M003/S02 | requirement | R011 | validated | S02 proved that when the interstitial ad cannot be shown or errors during the FAIL gate, the game screen releases through the fallback path and restores PLAYING with hearts=3, moves=0, cleared lastInteraction/lastResolution, and unchanged persisted progress. Evidence: passing gameScreenFailLoopScreen.test.tsx, heartSystem.test.ts, and npx tsc --noEmit. | Yes | agent |
| D021 | M003/S03 | requirement | R010 | validated | S03 closed the monetization proof boundary by passing the mounted GameScreen regression plus persistence/result suites and TypeScript. Evidence now proves banner presence on the game screen, hearts=0 interstitial gate lock-and-release behavior, restart back to PLAYING with hearts reset, no durable progress writes during FAIL/restart, and surviving WIN/progression behavior via `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx progressPersistence.test.ts integratedPlayFlow.test.ts` and `npx tsc --noEmit`. | Yes | agent |
| D022 | M004 discussion | scope | M004 scope boundary for repeat-play and release prep | Keep M004 repeat-play work on-device and privacy-minimal: no account system, no personal-information product features, no backend storage, and no analytics-driven expansion as part of this milestone. | The user explicitly wants repeatable on-device content and does not want the app to receive personal information. Locking this boundary now prevents M004 from expanding into live-ops, backend state, or disclosure-heavy product systems that would dilute the release-finish milestone. | Yes | collaborative |
| D023 | M004 discussion | pattern | M004 feedback feel bar | Target clear, restrained feedback rather than loud spectacle: subtle movement cues, stronger collision/clear moments, and verification focused on clarity at the existing game-screen boundary. | The user emphasized 'clear' rather than flashy. This gives S01 a concrete product bar and keeps audio/haptic work aligned with the puzzle-first tone established by the current board and result shell. | Yes | collaborative |
| D024 | | requirement | R013 | validated | M004/S01 shipped a typed feedback derivation helper, Expo audio+haptics runtime seam, mounted GameScreen feedback diagnostics, and passing slice verification via `npm test -- --runInBand -- gameFeedback.test.tsx gameScreenFailLoopScreen.test.tsx heartSystem.test.ts movement.test.ts boardPresentation.test.ts && npx tsc --noEmit`, proving move/collision/clear/fail-recovery feedback integration without regressing gameplay or monetization flows. | Yes | agent |
| D025 | M004/S02 | requirement | R014 | validated | M004/S02 delivered a local challenge catalog, isolated challenge persistence, source-aware home/challenge entry, and mounted GameScreen reuse for challenge play with replay-after-win and fail-gate recovery. Evidence: `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx levelsScreen.test.tsx challengeMode.test.ts integratedPlayFlow.test.ts` passed 28/28 and `npx tsc --noEmit` passed, proving repeatable on-device content can be opened and replayed multiple times per day without backend or account dependence. | Yes | agent |
| D026 | | requirement | R012 | validated | M004/S03 established a single release-metadata/privacy contract across `app/release/_lib/storeMetadata.ts`, `docs/release/app-store-korea.md`, `app.json`, and the mounted `/release` screen reachable from home, then re-verified the Korean launchability surface with passing `npm test -- --runInBand -- releaseMetadata.test.ts levelsScreen.test.tsx gameScreenFailLoopScreen.test.tsx challengeMode.test.ts integratedPlayFlow.test.ts` and `npx tsc --noEmit`. | Yes | agent |
| D027 | M005 discussion | pattern | M005 result readability bar | Define result-surface success by one-glance understanding: the player can tell win/loss, quality, and next action without explanation, and the screen must not contradict run memory. | The user defined M005 closeout around one-glance comprehension rather than around raw presence of stars, moves, or CTAs. The result surface must therefore be validated against this product bar. | Yes | collaborative |
| D028 | M005/S01 | requirement | R006 | validated | M005/S01 re-proved the mounted GameScreen state-truth contract across PLAYING, BLOCKED, ESCAPED, FAIL, and WIN through visible copy plus deterministic diagnostics, while the closure suite (`npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx heartSystem.test.ts integratedPlayFlow.test.ts starRating.test.ts boardPresentation.test.ts`) passed 38/38 and workspace TypeScript diagnostics reported no issues. | Yes | agent |
| D029 | | requirement | R007 | validated | M005/S02 mounted fail-recovery verification now proves the continuity loop end to end: after hearts reach 0 and the interstitial gate releases, GameScreen returns to a fresh PLAYING read with hearts 3/3, moves 0, FAIL UI removed, grid re-enabled, and baseline board/debug copy restored. Evidence: `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx heartSystem.test.ts boardPresentation.test.ts gameFeedback.test.tsx` and `npx tsc --noEmit`. | Yes | agent |
| D030 | M005/S03/T02 | requirement | R007 | validated | S03 closed the mounted result-surface contract without further runtime edits: the mounted GameScreen WIN branches, pure helper suites, and integrated progression tests now agree on quality wording, next-action semantics, and diagnostic state lines. Evidence: passing `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx starRating.test.ts integratedPlayFlow.test.ts` and `npx tsc --noEmit`. | Yes | agent |
| D031 | M005/S03 | requirement | R007 | validated | S03 closed the mounted result-surface contract without runtime edits: GameScreen WIN branches, deriveStarRatingSummary(), and deriveScreenLevelContext() now agree on one-glance quality wording, next-action semantics, and deterministic feedback/debug lines. Proof: passing `npm test -- --runInBand -- gameScreenFailLoopScreen.test.tsx starRating.test.ts integratedPlayFlow.test.ts` and `npx tsc --noEmit`. | Yes | agent |
| D032 | M006 | scope | M006 scope boundary | Treat M006 as a browser-local verification milestone, not a web product expansion milestone. | The user wants to run the game locally in this PC's browser and directly verify the current product, not add web monetization, web deployment, or parity with the native target. | Yes | collaborative |
| D033 | M006 | architecture | Browser ad/runtime strategy | On web, disable or replace native ad behavior behind a web-safe import boundary instead of trying to run `react-native-google-mobile-ads`. | The actual investigation showed the current Expo web bundle fails at import time on the native ads package, so runtime fallback is not enough. The browser path must avoid bundling unsupported native ad modules while preserving the existing native-focused contract. | Yes | collaborative |
| D034 | M007 | architecture | 가변 길이 열차 좌표 기준 | Train 좌표를 열차의 head(진행 방향 쪽 맨 앞칸) 기준으로 정의하고, 몸통 점유 셀은 방향 반대편으로 계산한다. | 앞부분이 둥근 실루엣, 방향 판독, 탈출 애니메이션, 경계 이탈 타이밍 모두 head 기준이 가장 자연스럽다. 기존 단일 셀 row/col 의미를 앞칸으로 해석하면 길이 확장 시 엔진/렌더링 계산이 단순해진다. | Yes | collaborative |
| D035 | M006/S01 | architecture | M006/S01 web runtime import boundary | Keep ads and gameplay feedback behind shared descriptor hooks plus `.native`/`.web` platform files, and preserve mounted `banner=` / `phase=` / `fallback=` / feedback debug lines as the regression seam. | This slice proved that Expo web export succeeds only when unsupported native packages are removed from the web module graph at import time. Preserving the existing mounted diagnostics lets browser-local verification reuse the same GameScreen and InterstitialGate contracts instead of inventing web-only assertions. | Yes | agent |
| D036 | M006/S01 | requirement | R010 | active | M006/S01 preserved the monetization UI contract across web-safe runtime boundaries: banner diagnostics still render, interstitial phase/fallback diagnostics still drive FAIL recovery, `gameScreenFailLoopScreen.test.tsx` and `npx tsc --noEmit` pass, and `CI=1 npx expo export --platform web` now succeeds without crashing on `react-native-google-mobile-ads`. This advances browser-local compatibility for the monetization seam but does not newly validate native AdMob behavior itself. | Yes | agent |
| D037 | M006/S01 | requirement | R013 | active | M006/S01 preserved gameplay feedback behavior while moving `expo-audio` and `expo-haptics` behind platform files. Focused feedback tests, TypeScript, and web export now prove the browser path no longer crashes on feedback imports, but this slice did not change the already validated native feedback capability contract. | Yes | agent |
| D038 | M006/S02 | architecture | M006/S02 browser proof route handling | Use a dedicated route-only proof flag and stable train selectors for the browser WIN seam; keep normal unlock rules and existing GameScreen diagnostics unchanged. | Live web verification showed the dedicated home CTA fell back to level 1 on cold start because hydration correctly enforced normal easy-pack unlocks. Adding an explicit proof=browser-win route flag let the browser proof open deterministic level 3 without weakening progression rules, while stable train selectors and the existing screen-boundary debug lines kept browser automation and mounted regression coverage aligned. | Yes | agent |
| D039 | M007/S01/T01 | architecture | M007/S01 movement engine seam for variable-length trains | Treat `Train.row`/`col` as head coordinates, derive occupied cells backward from direction, and derive escape/collision checks from the head-forward path only. | This keeps single-cell behavior unchanged while making length-based occupancy, blocking, and escape timing deterministic for downstream store and rendering slices. Collision checks now use the same explicit helper contract that tests can exercise directly. | Yes | agent |
| D040 | M007/S02 | architecture | M007/S02 variable-length rendering seam | Use a shared pure render-geometry helper backed by engine occupancy for Grid placement, Train silhouette sizing, highlight bounds, and tap-target expansion. | S02 proved that rendering and interaction stay deterministic when both Grid and Train consume getTrainRenderGeometry(), which itself reuses getTrainOccupiedCells(). This keeps mounted FAIL/WIN/restart behavior aligned with the head-based engine contract and gives S03 one geometry seam for long-train animation work. | Yes | agent |
| D041 | M007/S03 planning | architecture | M007/S03 long-train exit animation state boundary | Keep long-train exit animation as Grid-owned presentation state derived from render geometry and ESCAPED transitions, and clear it immediately on reset/restart/level changes instead of persisting animation state in Zustand. | S01/S02 already proved that engine/store truth flips to ESCAPED immediately and resetLevel()/restartLevel() restore the original level snapshot synchronously. A presentation-only overlay preserves those rule-truth contracts while allowing the UI to animate the tail off-screen and reset cleanly without changing movement, WIN timing, persistence, or mounted debug seams. | Yes | agent |
| D042 | | architecture | M007/S03 long-train exit overlay lifetime | Keep exit overlays as Grid-local transient presentation state keyed off ESCAPED transitions, with reset-on-replay/restart/level-change and no store persistence. | The slice proved the pure animation seam and the runtime overlay/reset behavior on disk without changing engine semantics. Store-level ESCAPED/WIN truth remains immediate, while replay/fail recovery can clear overlay residue synchronously. The remaining instability is in the mounted Jest fake-timer harness, not in the gameplay contract itself. | Yes | agent |
| D043 | | requirement | R009 | validated | S04 rebalanced easy and challenge boards around calibrated mixed-length layouts, preserved stable proof anchors and public ids, and re-verified meaningful par-driven star feedback through passing `levelPack.test.ts`, `starRating.test.ts`, `challengeMode.test.ts`, `heartSystem.test.ts`, `integratedPlayFlow.test.ts`, `gameScreenFailLoopScreen.test.tsx`, and `npx tsc --noEmit`. This now proves players receive durable move-quality feedback against the shipped harder boards rather than trivial single-car layouts. | Yes | agent |
| D044 | M008/S01 planning | architecture | M008/S01 native proof surface | Use SDK-55 dependency normalization plus a checked-in `scripts/verify-ios-native-run.sh` and `docs/ios-native-run.md` as the canonical iOS simulator proof/diagnostic seam for the milestone. | Research showed the first blocker is native build health, not route logic. Closing S01 requires both fixing the package/config contract enough for `npx expo run:ios` to reach simulator install/boot and leaving a repeatable phase-classified operator path that later slices can reuse without rediscovering CLI, port, prebuild, pods, and xcodebuild behavior. | Yes | agent |
gstack 마일스톤 — 10개의 단계, 258개의 문서
이 프로젝트에는 gstack이 자동으로 생성하고 관리한 마일스톤 문서가 있다. 10개의 마일스톤, 총 258개의 마크다운 파일. 각 마일스톤은 컨텍스트, 로드맵, 슬라이스(작은 작업 단위), 검증 결과, 요약을 포함한다. AI와 사람이 같이 일하면 자동으로 이 정도의 문서가 쌓인다.
마일스톤
제목
상태
M001
플레이 가능한 코어 퍼즐 루프
완료
M002
하트·별점·진행 저장
완료
M003
AdMob 광고 연동
완료
M004
폴리시와 출시 준비 (사운드·챌린지·한국 메타)
완료
M005
결과 UX 마감 (WIN/FAIL 화면)
완료
M006
브라우저 로컬 검증 (웹 빌드 호환)
완료
M007
가변 길이 열차와 퍼즐 난이도 재설계
완료
M008
iOS 시뮬레이터 네이티브 실행 검증
진행 중
M009
easy 전체 난이도 곡선 재설계
계획
M010
최종 마감 및 출시 직전 검증
계획
📄 M001 요약 — 플레이 가능한 코어 퍼즐 루프 (클릭하여 펼치기)
---
id: M001
title: "플레이 가능한 코어 퍼즐 루프"
status: complete
completed_at: 2026-03-27T13:44:08.014Z
key_decisions:
- Stay on Expo SDK 55 and align package versions until `expo-doctor` is clean before treating the baseline as stable — avoids carrying SDK drift into later milestones.
- Use Expo Router from the start so later slices build on real route files instead of migrating from a temporary single-screen app.
- Keep movement resolution in a pure engine module (`engine/movement.ts`) that returns deterministic outcomes reusable by UI, store, tests, and future animation work.
- Expose `status`, `moves`, and per-train `state` directly in the Zustand store so runtime state is inspectable from the UI without recomputation.
- Represent movement results with shared typed reason codes and collision metadata (`MovementResolution`) instead of ad-hoc booleans.
- Persist both a concise `lastInteraction` and the full engine `lastResolution` in the store so downstream code can inspect exact blocker coordinates without recomputing gameplay rules.
- Keep board readability as a pure derived presentation layer (`boardPresentation.ts`) over existing store snapshots — no persisted UI state added.
- Keep route-adjacent pure helpers under `app/game/_lib/` so Expo Router does not misclassify them as routes during web runtime.
- Pin `expo.port` to 8081 in `app.json` so CI-style Metro startup proof is deterministic across verification runs.
- Preserve earlier fixture level ids (1, 3, 4) when expanding content packs so engine/UI regression suites keep trustworthy deterministic anchors.
key_files:
- engine/movement.ts
- store/gameStore.ts
- types/game.ts
- app/game/[level].tsx
- app/game/_lib/boardPresentation.ts
- app/game/_lib/levelProgression.ts
- app/game/_lib/levelParam.ts
- components/Grid.tsx
- components/Train.tsx
- data/levels/easy.ts
- app/index.tsx
- __tests__/movement.test.ts
- __tests__/boardPresentation.test.ts
- __tests__/levelPack.test.ts
- __tests__/integratedPlayFlow.test.ts
- app.json
lessons_learned:
- Expo scaffold gotcha: when a repo already contains `.gsd/` metadata, `create-expo-app` must be generated in a temp directory and copied in — it will not initialize cleanly over existing non-Expo files.
- Expo Router/Jest gotcha: keep route-param coercion and other boundary helpers in pure modules under `_lib/` — this avoids pulling Expo Router runtime into Jest and keeps edge-case coverage independent of the router.
- Expo CLI gotcha: `--non-interactive` is no longer supported; use `CI=1 npx expo start --offline --clear` for non-interactive startup proof.
- Expo port prompt gotcha: pin `expo.port` to `8081` in `app.json` to prevent alternate-port prompts in CI-style verification when Metro was previously shifted to another port.
- First-playable game-store pattern: keep movement resolution in a pure engine module and expose `status`, `moves`, and per-train `state` directly from Zustand so UI and later animation slices inspect gameplay state without recomputing it in components.
- Collision UX pattern: keep `lastInteraction` for concise player-facing copy and `lastResolution` for full engine truth; have screens read both directly from the store instead of re-deriving blockers, path length, or mutation state in UI code.
- Board readability pattern: derive all presentation signals (highlighted train, path, blocked, collision, exits) from existing store snapshots in a pure helper — adding no persisted UI state keeps the seam testable and the store authoritative.
- Content pack pattern: keep next/previous/completed-pack adjacency in a pure data helper (`levelProgression.ts`) so screens consume one contract; preserve fixture ids when expanding so earlier regression suites remain valid.
---
# M001: 플레이 가능한 코어 퍼즐 루프
**Delivered a fully playable tap-to-escape puzzle loop with deterministic collision rules, readable board presentation, a six-level handcrafted pack, and integrated regression coverage — proving the core game feel before any polish layer is added.**
## What Happened
M001 built the foundation of 열차 탈출 from an empty repository to a short but genuinely playable puzzle session across five sequenced slices.
**S01 — 첫 플레이어블 루프** established the architectural skeleton: an Expo Router app shell, a pure `engine/movement.ts` resolving tap-to-escape in deterministic cell steps, a Zustand store exposing `status`/`moves`/per-train `state` directly, and reusable SVG Grid and Train components that render the board without image assets. The home screen routes into `app/game/[level].tsx`, level params are coerced safely in a pure helper, and the WIN state fires when the last train escapes. A 11-case Jest suite locked the escape/win/reset/boundary contracts from the start.
**S02 — 충돌 규칙과 상태 전이** made blocked moves feel explainable rather than arbitrary. Movement outcomes gained shared typed reason codes, traversed path data, and blocker cell metadata. The Zustand store persists both a concise `lastInteraction` for UX copy and a full `lastResolution` snapshot so UI and future diagnostics never recompute engine truth. An on-screen feedback card and compact debug line show outcome/reason/path length/blocker coordinates in the live game screen. A level-4 collision fixture was exposed from the home screen for fast runtime repro. The suite grew to 15 cases covering blocked paths, no-ops, repeated inputs, and WIN/move-count protections.
**S03 — 가독성 있는 보드/열차 표현** added a pure presentation layer on top of the S01/S02 store contracts. `boardPresentation.ts` derives highlighted train, attempted path, blocked path, collision cell, escaped count, exit marker visibility, and hint tone without adding any persisted UI state. Grid gained exit markers, path overlays, blocked-cell emphasis, and collision-cell highlighting. Train gained directional nose geometry and per-state visual styling (selected, blocked, escaped). The game screen now pairs the board with a legend, hint card, and result card. A `boardPresentation.test.ts` suite locked the derived presentation contract with seven regression cases. The Expo port was pinned to 8081 in `app.json` to make CI-style Metro startup deterministic.
**S04 — 짧지만 진짜 풀 수 있는 레벨 묶음** replaced the minimal starter set with six hand-crafted 3×3 and 4×4 easy levels that form a short connected playable arc. A pure `levelProgression.ts` helper handles next/previous/completed-pack lookups so screens consume one authoritative contract. The home screen became a level-entry card list; the game WIN screen shows replay/next buttons and completed-pack messaging. A `levelPack.test.ts` suite with 15 cases locked ordering, adjacency, coercion, last-level, and empty-pack edge cases. Earlier regression suites (movement + board presentation) continued passing unmodified.
**S05 — M001 통합 정리와 플레이 검증** closed the milestone by composing all prior seams into an integrated regression suite (`integratedPlayFlow.test.ts`, 14 cases), aligning game-screen UI badges and copy to the helper-driven progression contract, and verifying that `CI=1 npx expo start --offline --clear --port 8081` reaches `Waiting on http://localhost:8081` deterministically. TypeScript diagnostics were confirmed clean via LSP (a stale relative import in `boardPresentation.ts` was fixed). The authoritative verification contract for the milestone is now: `npm test -- --runInBand -- movement.test.ts boardPresentation.test.ts levelPack.test.ts integratedPlayFlow.test.ts` passing plus pinned-port Metro boot.
## Success Criteria Results
## Success Criteria Results
- **탭 기반 열차 이동** ✅ — S01 ships `engine/movement.ts` + Zustand store + SVG board. Tapping a train resolves its direction, traverses cells, and escapes the board. `npm test movement.test.ts` passes 15 cases including escape, no-op, reset, and WIN transition.
- **셀 기반 충돌 규칙** ✅ — S02 adds structured `MovementResolution` with reason codes, traversed path, and blocker metadata. Level-4 collision fixture is reachable from home. 15 movement contract tests prove blocked/escaped/no-op/WIN-protection determinism.
- **읽기 쉬운 보드 표현** ✅ — S03 ships `boardPresentation.ts` pure helper, Grid/Train overlays, and in-screen legend/hint/result cards. `boardPresentation.test.ts` passes 7 regression cases covering neutral baseline, blocked movement, escaped state, malformed input, and fallbacks.
- **여러 개의 초기 레벨** ✅ — S04 delivers six handcrafted 3×3 and 4×4 easy levels with home entry cards, WIN-screen replay/next controls, and completed-pack messaging. `levelPack.test.ts` passes 15 cases.
- **코어 퍼즐 루프 통합 재검증** ✅ — S05 ships `integratedPlayFlow.test.ts` (14 cases) composing home-entry, fallback, collision, progression, and completed-pack contracts. Metro boots deterministically on port 8081.
- **"짧게라도 재밌다" 증명** ✅ — The six-level pack, readable board, feedback cards, and explicit UAT scripts across all five slices provide sufficient mixed-evidence proof for a first-playable milestone bar. Subjective UX evidence is intentionally hybrid (automation + manual UAT scripts) rather than a recorded device session.
## Definition of Done Results
## Definition of Done Results
- **All slices marked complete** ✅ — S01, S02, S03, S04, S05 all show `[x]` in the roadmap and have corresponding SUMMARY.md + UAT.md artifacts on disk.
- **All slice summaries exist** ✅ — Verified: S01-SUMMARY.md, S02-SUMMARY.md, S03-SUMMARY.md, S04-SUMMARY.md, S05-SUMMARY.md — all present.
- **Non-.gsd/ code changes present** ✅ — `git diff --stat HEAD~5 HEAD -- ':!.gsd/'` shows 14 changed source/test files, 1,211 insertions and 168 deletions across engine, store, components, app screens, data, and test suites.
- **Jest regression suites passing** ✅ — `movement.test.ts` (15 cases), `boardPresentation.test.ts` (7 cases), `levelPack.test.ts` (15 cases), `integratedPlayFlow.test.ts` (14 cases) — all passing per S02/S03/S04/S05 verification evidence.
- **Metro runtime boots deterministically** ✅ — `CI=1 npx expo start --offline --clear --port 8081` reaches `Waiting on http://localhost:8081` with no blocking errors after S05 pinned the port in `app.json`.
- **TypeScript diagnostics clean** ✅ — S05 confirmed LSP-verified clean diagnostics after fixing a stale relative import in `boardPresentation.ts`.
- **Cross-slice integration consistent** ✅ — S02/S03/S04/S05 all consumed prior slice contracts (pure movement seam, store snapshots, board helper, progression helper) without replacing them; fixture ids 1, 3, and 4 were preserved across all content expansions.
- **Validation verdict** ✅ — M001-VALIDATION.md records `verdict: pass` with detailed per-slice delivery audit, cross-slice integration assessment, and requirement coverage analysis.
## Requirement Outcomes
## Requirement Outcomes
- **R001** (첫 플레이어블 루프) — **Active → Validated.** Evidence: `app/index.tsx` routes into `app/game/[level].tsx`; tapping trains updates store state through `engine/movement.ts` until all escape and `status = WIN`; `movement.test.ts` proves the full escape/win flow.
- **R002** (충돌 규칙과 상태 전이) — **Active → Validated.** Evidence: `engine/movement.ts` returns `MovementResolution` with typed reason codes, path data, and blocker metadata; `store/gameStore.ts` persists `lastResolution`/`lastInteraction`; `movement.test.ts` (15 cases) proves blocked/escaped/no-op/WIN determinism.
- **R003** (즉각적인 탭 반응과 상태 가시성) — **Active → Validated.** Evidence: `components/Grid.tsx`, `components/Train.tsx`, and feedback/debug cards on the game screen make every state change immediately inspectable; `npm test movement.test.ts` confirms store updates are synchronous and deterministic.
- **R004** (가독성 있는 보드/열차 표현) — **Active → Validated.** Evidence: `boardPresentation.ts` pure helper, Grid overlays (exit markers, path, blocked, collision), Train direction visuals, in-screen legend/hint/result cards; `boardPresentation.test.ts` (7 cases) proves the presentation contract.
- **R005** (여러 개의 플레이 가능한 레벨) — **Active → Validated.** Evidence: six handcrafted levels in `data/levels/easy.ts`, pure `levelProgression.ts` helper, home multi-level entry cards, WIN-screen replay/next/completed-pack flow; `levelPack.test.ts` (15 cases) and `integratedPlayFlow.test.ts` (14 cases) prove progression contracts end-to-end.
## Deviations
Scaffolding had to be generated in a temp directory and merged into the repo root because `create-expo-app` would not initialize over the existing `.gsd/` metadata. The `--non-interactive` Expo CLI flag was discovered to be removed mid-milestone; verification commands were updated to use `CI=1` from S03 onward. Route-adjacent pure helpers were moved from `app/game/` to `app/game/_lib/` during S03 to prevent Expo Router web from misclassifying them as routes. `app.json` port pinning was added during S03 verification after a non-interactive port prompt blocked startup proof. Runtime proof for M001 is a hybrid of automated Jest coverage plus Metro startup evidence rather than a captured live-device walkthrough, which is appropriate for the first-playable milestone bar but would not be sufficient for a full UAT submission.
## Follow-ups
Phase 2 work for the next milestone: hearts system (3 lives, -1 on collision, FAIL at 0), WIN/FAIL screen transitions with star rating based on `par`, level select screen with progress persistence (AsyncStorage), and expansion to 50 easy levels. The six-level M001 pack is intentionally minimal — the content volume and hearts/fail UX are gated on Phase 2 so the core loop could be proven first without those layers. Also: deprecated React Native Web `shadow*`/`pointerEvents` warnings in the web runtime were noted but not cleaned up — they do not affect the iOS target and can be deferred.
심사 대기, 그리고 거절
1.0을 그대로 앱스토어에 올렸다. QA는 거의 하지 않았다. 이 정도 규모에 무슨 QA냐고 그때는 생각했다. 며칠 후 거절 메일이 왔다. 사유는 ATT(App Tracking Transparency) 처리 버그.
솔직한 감상은 한 줄이었다.
“10년도 더 전이지만 정말 많이 해본 일인데, 이렇게 단순한 실수를 했구나.”
AI로 5시간 만에 게임을 만들 수 있는 시대다. 그런데 사람이 한 번도 안 들어가도 되는 시대는 아니었다. 5시간이 가능하다는 것과 5시간으로 끝났다는 것은 완전히 다른 이야기다. 거절 메일 한 통이 그 차이를 정확히 짚어주었다.
그래서 무엇이 달라졌나
시리즈의 명제 — “AI로 코딩이 진짜 가능한가? 상용화까지 갈 수 있나?” — 첫 절반에 대한 답은 1부에서 이미 나왔다.
가능하다. 5시간이면 풀 파이프라인이 굴러간다. 단, 시니어가 옆에서 결정을 계속 내려주는 5시간이다.
두 번째 절반 — 상용화 — 의 답은 2부에서 시작한다. QA를 처음 도입하고, 심사를 두 번 받고, 첫 1센트가 떨어지는 순간까지의 이야기다.
지금 바로 할 수 있는 것
도구 비교는 잠깐 미뤄라. AI 코딩 입문이라면 비교 매트릭스보다 한두 개 깔아서 직접 굴려보는 게 빠르다. 손에 맞는 것만 남기면 된다. 그리고 첫 5시간은 KPI 없는 사이드에 써봐라. 회사 일이 아닌, 본인이 진짜 궁금한 무언가에 5시간을 써보면 AI 코딩이 본인에게 무엇인지 답이 나온다.