[태그:] Claude Code

  • (1/3) I Built a Game in 5 Hours with AI, Then Got Rejected by the App Store (Trains Out Dev Log Part 1)

    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) Complete
    M005 Result UX finalization (WIN/FAIL screens) Complete
    M006 Browser local verification (web build compat) Complete
    M007 Variable-length trains & puzzle difficulty redesign Complete
    M008 iOS simulator native execution verification In progress
    M009 Easy pack full difficulty curve redesign Planned
    M010 Final polish & pre-launch verification Planned
    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.

    Related Posts

    Sources

  • (1/3) AI로 5시간 만에 게임을 만들었다, 그리고 앱스토어 심사에서 거절…

    이 글은 시리즈 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 코딩이 본인에게 무엇인지 답이 나온다.

    관련 글

    출처

  • Cursor 60조 밸류에이션 해부: 지금 계속 써야 하는가 5가지 판단 프레임워크

    Cursor 60조 밸류에이션 해부: 지금 계속 써야 하는가 5가지 판단 프레임워크

    Cursor 만드는 회사 Anysphere의 밸류에이션이 1년 만에 6배 가까이 뛰었다. 2025년 초 $9.9B, 2025년 11월 Series D에서 $29.3B, 그리고 2026년 3월 기준 새 라운드 논의에서 $50~60B이 언급된다. 매출은 2025년 5월 ARR $500M → 10월 $1B → 2026년 2월 $2B을 돌파했다. 숫자만 보면 실리콘밸리 역사상 가장 빠른 성장 곡선 중 하나다.

    그런데 Fortune이 3월 21일 커버스토리에서 붙인 제목은 흥미롭게도 “Cursor’s crossroads: the rapid rise, and very uncertain future“였다. 뭐가 불확실하다는 걸까. 이 글은 숫자와 헤드라인이 아니라, Cursor 유료 구독자/팀 도입 담당자가 “지금 계속 써야 하는지, 갈아타야 하는지”를 판단할 수 있도록 5가지 관점으로 구조화한 프레임워크다.

    관점 1. 가격 구조 — Claude Code의 역공

    Fortune이 “pricing problem”으로 명시한 부분이다. Cursor의 비즈니스 구조는 단순하다. Anthropic·OpenAI·Google 모델을 API로 호출해서 구독료와의 차액을 먹는 모델이다. 문제는 Claude Code. Anthropic이 자체 모델을 자체 툴로 제공하니 원가 구조부터 Cursor가 따라갈 수 없다. 2026년 들어 Claude Code가 Cursor보다 낮은 가격대로 비슷한 경험을 제공하기 시작했다.

    판단 기준: 본인의 월 Cursor 비용이 $20 Pro로 충분히 감당되면 전환 필요성 낮음. 팀 플랜($40/사용자 × 사용자 수)을 쓰거나 usage 초과로 월 청구가 들쭉날쭉하다면, Claude Code + OpenClaw 같은 대안과의 비교가 실질적 선택지가 된다.

    관점 2. 워크플로우 잠금 — 에디터냐 에이전트냐

    Cursor 3는 Agent 중심으로 재설계되었지만, 여전히 강점은 “VS Code 포크 기반의 풍부한 에디터 경험”에 있다. 반대로 Claude Code는 터미널 네이티브라 에디터 UI가 약하다. 여기서 갈라지는 포인트가 명확하다. 본인의 개발 루틴이 (a) 에디터 안에서 생각하고 수정하는 흐름 중심인지, (b) 에이전트에 태스크만 던지고 결과를 리뷰하는 흐름 중심인지.

    판단 기준: 하루 작업의 절반 이상이 에디터에서 벌어지면 Cursor 유지. 절반 이상이 “태스크 위임·결과 검토”라면 Claude Code나 Codex 기반 도구가 효율적. 이 두 흐름을 섞어야 하는 팀이라면 둘 다 쓰는 게 비용 대비 생산성 측면에서 타당할 수 있다.

    관점 3. 모델 의존성 리스크

    Cursor의 가장 큰 구조적 약점은 “본인들이 모델을 안 만든다”는 것이다. 2026년 현재 Anthropic, OpenAI, Google 모두 자체 코딩 툴을 밀고 있고, 이들이 언제든 Cursor에 가는 API 단가를 조정할 수 있다. Anysphere는 최근 자체 모델 개발을 시사했지만, 인프라 투자는 OpenAI/Anthropic과 비교도 안 되게 작다.

    판단 기준: Cursor가 2026년 안에 자체 모델이나 주요 파트너십(예: Nvidia+Cursor 프리미엄 계약)을 발표하면 긍정 신호. 반대로 “Claude 3.7만 Pro 전용” 같은 식의 모델 차등 제한이 반복되면 API 의존 구조가 불리하게 돌아가는 신호다.

    관점 4. 팀 도입 관점 — 계약/보안 친화성

    Fortune 보도 이후 한국 대기업·금융권·공공 고객이 Cursor 도입을 재검토하는 움직임이 있다. 이유는 두 가지. 첫째, 밸류에이션이 높아질수록 엔터프라이즈 계약 조건이 공격적으로 변할 가능성. 둘째, 데이터 처리·보안 감사 기준에서 “어떤 모델이 호출되는지, 로그는 어디 남는지”를 명확히 해야 하는 기업 요구사항.

    판단 기준: 개인 개발자·스타트업은 가격·품질만 보면 된다. 50명 이상 팀이나 민감 코드베이스를 다루는 조직은 Business/Enterprise 티어 계약서를 다시 읽어보고, 보안 부서와 함께 “코드가 어떤 모델 호출을 거쳐 어디에 저장되는지” 구체적으로 확인해둬야 한다. 이건 Cursor만의 이슈는 아니고 Copilot·Claude Code도 마찬가지다.

    관점 5. 커뮤니티·생태계 성숙도

    Cursor의 가장 견고한 자산은 이미 자리잡은 커뮤니티다. JetBrains 1만 명 조사(2026년)에서 Cursor는 상위권을 유지했고, 한국 개발자 커뮤니티에서도 “일단 Cursor부터 써본다”가 기본값이 됐다. 이건 단기간에 뒤집히지 않는다. Reddit r/cursor, YouTube 튜토리얼, 프롬프트 라이브러리, 내장 rules 파일 생태계까지 합치면 이동 비용이 꽤 크다.

    판단 기준: 지금 Cursor로 생산성을 내고 있고, .cursorrules나 팀 컨벤션이 잘 녹아 있다면, 가격·경쟁 이슈에도 불구하고 2~3개월은 유지하면서 시장을 관찰하는 게 합리적. 갈아타는 비용이 당장 얻을 절감액보다 클 가능성이 높다.

    그래서 한국 유저에게 뭐가 달라지나

    밸류에이션 논의가 한국 유저에게 직접 영향을 주지는 않는다. 하지만 그 뒤의 질문 — “Cursor가 앞으로도 본인 포지션을 지킬 수 있을까” — 은 실질적 구독 결정과 직결된다. 세 가지 시나리오가 가능하다.

    • 시나리오 A (유지): Cursor가 자체 모델 또는 차별화 UX로 반격에 성공. 현재 구독 유지.
    • 시나리오 B (조용한 잠식): 가격·품질에서 Claude Code·Copilot Agent Mode가 Cursor를 슬며시 앞서기 시작. 이 경우 6개월쯤 뒤 자연스럽게 전환.
    • 시나리오 C (급변): 파트너십 붕괴나 대규모 가격 인상. 이땐 즉시 전환 결정.

    현재 한국 개발자에게 가장 실용적인 포지션은 “Cursor를 유지하되 Claude Code와 Copilot Agent Mode를 나란히 셋업해두는 것”이다. 전환 비용은 한 달 전에 준비해두면 없는 것과 같고, 유지 비용은 $20/월 수준이다. 결정이 필요한 순간이 왔을 때 바로 움직일 수 있는 준비가 진짜 경쟁력이다.

    지금 바로 할 수 있는 것

    • 월 Cursor 비용 파악 — Pro/Business 티어 청구서 지난 3개월치를 꺼내서 usage 초과 여부를 본다.
    • 대안 하나 병행 셋업 — Claude Code 또는 Copilot Agent Mode 중 하나를 사이드 프로젝트에 설치해본다. 전환 시나리오의 리허설이다.
    • 팀 도입 담당자는 계약서 재확인 — 2026년 상반기 중 Cursor 쪽 가격 조정 가능성을 염두에 두고, 자동 갱신 조건과 보안 조항을 다시 본다.

    관련 글

    출처

  • Claude Code /powerup 명령어 완전 가이드: 4월 신기능 인터랙티브 학습 시스템 5분 정복

    Claude Code /powerup 명령어 완전 가이드: 4월 신기능 인터랙티브 학습 시스템 5분 정복

    왜 Anthropic은 갑자기 터미널 안에 강의실을 만들었나

    4월 1일자 Claude Code v2.1.90 릴리스 노트의 한 줄. “/powerup 명령어 추가.” 별것 아닌 듯 지나갔지만, 이건 Anthropic이 처음으로 인정한 사실이다. 사람들이 우리가 만든 기능의 절반을 쓰지 않고 있다는 것.

    사용자 입장에서 Claude Code는 묘한 도구다. 깔고 며칠만 쓰면 당장 코드를 짜 주니 편하다. 그런데 한 달이 지나도 처음 깔았을 때와 똑같은 명령어 다섯 개만 굴리고 있다. 왜 그럴까. 새 기능이 나와도 어디서 배워야 할지 몰라서다. 깃허브 README를 다시 뒤지자니 부담스럽고 공식 문서는 영어인 데다 길다. /powerup은 그 빈틈을 정확히 노렸다.

    업데이트 후 터미널에서 명령어를 치면 메뉴가 뜬다. 화살표로 고르고 엔터. 다른 창으로 이동할 필요 없이 그 자리에서 애니메이션과 함께 기능 하나를 익힌다. 한 모듈에 보통 3~10분.

    18개 모듈, ‘알고 있던 줄 알았던’ 기능들

    레슨 18개의 큐레이션 기준이 흥미롭다. 화려한 신기능이 아니라 “사용자 대부분이 모르고 지나치는 것”이다. 반대로 말하면 18개 중 한 개도 모르는 게 없다고 자신할 사람은 거의 없다는 뜻이기도 하다.

    비기너 영역은 의외로 컨텍스트 관리에 집중돼 있다. /clear/compact의 차이를 정확히 아는 사람은 많지 않다. CLAUDE.md 메모리 시스템이 어떻게 로드되는지도 마찬가지. 매일 쓰면서도 잘못 쓰고 있던 부분이 적지 않다는 얘기다.

    중급으로 가면 진가가 드러난다. Skills, hooks, 서브에이전트 오케스트레이션. 문서로 읽으면 추상적이라 손이 잘 안 가는 영역이다. /powerup은 이걸 터미널 위에서 실시간으로 굴려 보여준다. “아 이렇게 쓰는 거였구나”가 5분 만에 일어난다.

    고급 영역은 git worktrees 병렬 작업, 자동 모드, 클라우드 태스크. 이쯤 되면 사실상 Claude Code의 진짜 사용법이 여기 있다고 봐도 된다.

    한국 팀에 더 유리한 이유

    첫째는 영어 문서를 읽지 않아도 된다는 점이다. 텍스트 번역 없이 동작 영상을 보며 손으로 따라치면 의미가 직관적으로 들어온다. 둘째는 협업 차원의 효과다. 팀원 모두가 같은 18개 모듈을 거치고 나면 누가 어떤 기능을 쓰는지 자연스럽게 표준화된다. 사내 슬랙에 “이거 어떻게 해?”라고 묻는 횟수가 줄어든다는 뜻이다.

    4월 4일 추가된 v2.1.92에서는 MCP 결과 영속화와 Bedrock 셋업 마법사까지 들어왔다. 학습 범위는 계속 늘어나는 중이다.

    그래서 — 매일 쓰는 사람일수록 가치가 크다

    처음 깐 사람보다 6개월 쓴 사람에게 더 의미 있다. 익숙해진 워크플로 안에서는 새 기능을 일부러 찾지 않게 되기 때문이다. /powerup은 “당신이 모르고 있을 가능성이 가장 높은 것”만 골라 보여준다. 하루 5분, 점심시간 커피 한 잔과 바꿀 만한 거래다.

    지금 할 일

    업데이트가 먼저다. 터미널에서 claude --version으로 v2.1.90 이상인지 확인하고, 아직이면 npm i -g @anthropic-ai/claude-code. 다음으로 /powerup을 실행해 본인이 가장 자주 쓰는 영역(예: Skills 또는 hooks)부터 한 모듈만 끝내 본다. 거기서 막히면 그게 본인의 첫 학습 포인트다.

    관련 글

    출처

    대표이미지 출처: Anthropic Claude Code 공식 페이지

  • Kiro vs Cursor vs Claude Code: 2026년 AI 코딩툴 3파전 비교

    “AI 코딩툴 뭐 써야 해요?” 2026년 한국 개발자 커뮤니티에서 가장 많이 반복되는 질문이다. 답이 어려운 이유는 단순하다. 셋 다 잘 만든 도구지만 철학이 다르다. Cursor·Claude Code·Kiro 중에서 본인에게 맞는 쪽을 고르는 건 “어느 게 더 좋은가”가 아니라 “어느 게 내 작업 방식과 맞는가”의 문제다.

    이 글은 그 결정을 도와주는 한 장짜리 가이드다.

    먼저 철학부터 — 같은 카테고리, 다른 작업 흐름

    표면적으로는 모두 ‘AI 코딩 도구’지만, 실제 작업 흐름은 정반대에 가깝다.

    • Cursor: IDE 중심. 내가 운전하고 AI가 조수석에서 보조한다. 코드 한 줄씩 쓰면서 AI 제안을 받아 가는 패턴.
    • Claude Code: 에이전트 중심. 원하는 걸 말하면 AI가 자율적으로 실행한다. 사람은 결과물을 리뷰한다.
    • Kiro: 스펙 중심. 코딩에 들어가기 전에 설계 문서가 먼저 나오고, 그 위에서 AI가 체계적으로 구현한다.

    이 세 가지 중 어느 흐름이 본인 손에 더 맞느냐가 출발점이다.

    한눈에 보는 차이

    항목 Cursor Claude Code Kiro
    개발사 Cursor Inc. Anthropic Amazon (AWS)
    AI 모델 Claude/GPT 선택 Claude Sonnet Claude Sonnet
    인터페이스 VS Code 기반 IDE 터미널 CLI 전용 IDE
    자율 실행 제한적 높음 스펙 기반 실행
    컨텍스트 윈도우 광고 200K, 실제 70~120K 200K (실제 제공) 미공개
    무료 플랜 제한적 없음 프리뷰 무료
    월 요금 $20 (Pro) $20 (Claude Pro) $19 (Pro)

    표 안에서 가장 중요한 한 줄은 컨텍스트 윈도우 차이다. Cursor가 광고하는 200K와 실제 사용 가능한 70~120K 사이의 격차는 장시간 세션에서 결과물 품질에 직접 영향을 준다. Claude Code가 200K를 실제로 제공한다는 점은 깊은 추론 작업에서 의미가 크다.

    상황별 결정 가이드

    Cursor가 맞는 사람

    VS Code 환경을 그대로 유지하고 싶고, 인라인 코드 완성을 자주 쓰고, 커뮤니티 플러그인이나 확장에 의존하는 워크플로라면 Cursor가 가장 자연스럽다. 이미 익숙한 IDE 위에서 AI 기능만 더하는 식의 사용자에게 진입 장벽이 가장 낮다.

    Claude Code가 맞는 사람

    대규모 리팩토링, 복잡한 버그 수정, 멀티 파일 작업처럼 깊은 추론이 필요한 작업이 많다면 Claude Code가 빛난다. 터미널 기반 작업이 익숙해야 한다는 진입 장벽이 있지만, 그걸 넘으면 토큰 효율과 자율 실행 능력에서 차이가 난다. 이미 Claude Pro를 구독 중이라면 추가 비용 0원이라는 점도 결정에 영향을 준다.

    Kiro가 맞는 사람

    기능 명세를 먼저 정리하고 코딩에 들어가는 스타일, 팀 단위로 일관성 있는 결과물이 필요한 환경, 또는 6개월 후에도 본인이 손볼 코드를 만들고 있는 사람이라면 Kiro의 스펙 주도 개발이 진가를 발휘한다. 현재 프리뷰가 무료이므로 시도 비용도 없다.

    토큰 효율 — 잘 알려지지 않은 지표

    독립 측정에 따르면 동일한 작업을 수행할 때 Claude Code가 Cursor보다 평균 5.5배 적은 토큰을 사용한다. 한 번의 작업에서는 차이가 작아 보이지만 한 달, 6개월 단위로 누적되면 비용 차이가 꽤 커진다. 무엇보다 토큰 효율이 좋다는 건 같은 컨텍스트 안에 더 많은 정보를 담을 수 있다는 뜻이고, 결과물 품질에도 간접 영향이 있다.

    그래서

    처음 시작하는 사람이라면 추천 순서는 분명하다. Kiro(무료) → Claude Code(Claude Pro 있다면) → Cursor(유료 지불 의향 있다면). 2026년 기준으로 셋 다 완성도가 높아서 어느 쪽을 골라도 충분히 쓸 만하다. 핵심은 본인 워크플로에 얼마나 잘 맞느냐이고, 그건 실제로 일주일씩 굴려 봐야 알 수 있다.

    지금 할 일

    먼저 본인이 이미 구독 중인 서비스를 확인하자. Claude Pro가 있으면 Claude Code는 추가 비용 없이 시작할 수 있다. Kiro는 kiro.dev에서 지금 무료로 받을 수 있다. 가장 좋은 비교 방법은 같은 소규모 프로젝트를 각 도구로 1주일씩 진행해 본인 손에 어느 쪽이 맞는지 직접 측정하는 것이다. 표 위의 결론보다 손에서 나오는 결론이 훨씬 정확하다.

    관련 글

    출처

  • Claude 작업 공간을 시작하지 못했습니다: 원인과 해결법 총정리

    이 글을 검색해서 들어왔다면 이미 알고 있을 것이다. Claude Code 터미널 어딘가에서 한 줄짜리 메시지가 떠올랐다. “Claude 작업 공간을 시작하지 못했습니다.” 어제까지 멀쩡히 돌아가던 도구가 갑자기 열리지 않는다. 메시지가 짧아서 어디부터 손대야 할지 막막하다. 결론부터 말하면 — 대부분의 경우 5분 안에 해결된다. 원인이 보통 셋 중 하나로 좁혀지기 때문이다.

    왜 이 오류가 나는가

    Claude Code의 “작업 공간을 시작하지 못했습니다” 오류는 거의 다음 세 가지 중 하나로 귀결된다.

    • 인증 만료 — OAuth 토큰이 만료됐거나 계정 상태에 변화가 생긴 경우
    • IDE 연결 실패 — VS Code, JetBrains 같은 IDE와 Claude Code 사이의 연결이 끊어진 경우
    • 실행 환경 문제 — WSL 설정, PATH 누락, Node.js 버전 충돌

    중요한 사실 한 가지. 이 오류는 거의 Claude Code 자체의 버그가 아니다. 대부분 환경 설정 차원의 문제이고, 그래서 짧은 조치 몇 가지로 해결된다. 아래 순서대로 시도해 보면 90% 이상의 케이스가 잡힌다.

    해결법 1 — 가장 먼저 시도할 것: 로그아웃 후 재로그인

    인증 토큰이 만료된 경우가 가장 흔한 원인이다. Claude Code 터미널에서 다음을 입력한다.

    /logout

    로그아웃이 끝나면 Claude Code를 완전히 종료한다. 터미널을 새로 열고 다시 실행한다.

    claude

    로그인 화면이 나타나면 안내에 따라 브라우저 인증을 완료한다. 브라우저가 자동으로 안 열리는 경우가 있는데, 이때는 c 키를 눌러 URL을 클립보드에 복사한 다음 직접 브라우저에 붙여 넣으면 된다. 이 단계만으로 해결되는 비율이 가장 높다.

    해결법 2 — PATH 문제 (command not found 포함)

    설치는 됐는데 실행이 안 되는 경우다. 터미널에서 다음을 입력해 PATH를 점검한다.

    echo $PATH | tr ':' '\n' | grep local/bin

    아무것도 출력되지 않는다면 PATH에 설치 경로가 빠져 있는 것이다. 사용 중인 셸에 맞춰 추가한다.

    macOS (zsh)

    echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
    source ~/.zshrc

    Linux (bash)

    echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
    source ~/.bashrc

    이후 claude --version으로 정상 실행 여부를 확인한다.

    해결법 3 — WSL 환경에서의 추가 조치

    Windows에서 WSL로 Claude Code를 쓰는 경우 별도 설정이 필요하다. 자주 부딪히는 세 가지 케이스가 있다.

    브라우저 로그인이 안 되는 경우. WSL이 Windows 브라우저를 못 띄우는 상황이다. BROWSER 환경 변수를 직접 지정한다.

    export BROWSER="/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
    claude

    Node를 못 찾는 경우. WSL이 Windows 경로의 Node.js를 잘못 참조하는 게 원인이다. which node를 입력했을 때 /mnt/c/로 시작하면 이 문제가 맞다. nvm으로 Linux용 Node를 별도로 설치한다.

    source ~/.nvm/nvm.sh
    nvm install --lts

    JetBrains IDE가 감지 안 되는 경우. WSL2 방화벽이 IDE 연결을 차단하는 케이스다. PowerShell을 관리자 권한으로 열고 다음을 실행한다(WSL2 IP는 wsl hostname -I로 확인).

    New-NetFirewallRule -DisplayName "Allow WSL2" -Direction Inbound -Protocol TCP -Action Allow -RemoteAddress 172.21.0.0/16 -LocalAddress 172.21.0.0/16

    해결법 4 — 마지막 수단: 구성 파일 초기화

    위 세 가지로 안 잡힌다면 Claude Code 설정 파일을 통째로 초기화한다. 세션 기록은 사라지지만 기능은 그대로 복구된다.

    rm ~/.claude.json
    rm -rf ~/.claude/

    이후 claude를 다시 실행하면 초기 상태로 돌아간다. 처음 설치한 것과 같은 상태에서 다시 로그인하면 된다.

    그래서

    Claude Code는 업데이트 속도가 빠른 도구다. 그만큼 간헐적인 환경 충돌이 생긴다. 다만 거의 모든 경우 재로그인 → PATH 확인 → 구성 초기화 순서로 풀린다. 이 흐름을 머리에 넣어 두면 다음 번에는 5분 안에 끝낼 수 있다.

    지금 할 일

    가장 먼저 할 일은 Claude Code 터미널에서 /doctor를 실행하는 것이다. Anthropic 공식 문서가 권장하는 1차 진단 도구다. 결과가 어디에 문제가 있는지 직접 알려 준다. 진단 결과에 따라 위 4가지 해결법 중 해당하는 것부터 시도한다. 같은 오류가 반복적으로 발생한다면 운영 환경(WSL 사용 여부, Node 버전, 셸 종류) 정보를 메모해 두자. 다음 번 troubleshooting이 훨씬 빨라진다.

    관련 글

    출처

  • Claude Code 처음 시작하는 법: 설치부터 실전까지 2026 완전 가이드

    Claude Code를 처음 써 보려는 사람에게 가장 큰 벽은 도구 자체가 아니다. “터미널에서 시작한다”는 한 줄 때문에 마음이 멈춘다. Cursor나 GitHub Copilot처럼 익숙한 IDE 화면이 아니라 검은 터미널 창에서 출발해야 한다는 사실이 막막함을 만든다. 그런데 막상 깔아 보면 5분이면 끝난다. 이 글은 “어디서부터 시작해야 하지”라는 첫 막막함을 해소하기 위한 한국어 단계별 가이드다.

    Claude Code가 정확히 뭔가

    Claude Code는 Anthropic이 만든 AI 코딩 에이전트다. 터미널에 자연어로 지시를 입력하면 코드를 작성하고, 버그를 수정하고, Git 커밋까지 알아서 처리한다. 2026년 기준으로 VS Code, JetBrains, 데스크톱 앱, 웹(claude.ai/code)에서도 같은 흐름으로 쓸 수 있다. 즉 “터미널 전용”이라는 인상은 정확하지 않다. 입구가 터미널일 뿐, 본인이 편한 환경을 골라 쓰면 된다.

    가격 면에서 결정적인 사실 하나. Claude Code는 Claude Pro($20/월) 이상 구독자라면 추가 비용 없이 그대로 쓸 수 있다. Cursor Pro와 같은 가격이지만 작동 방식이 다르다. Cursor가 코드 완성 중심이라면 Claude Code는 에이전트 방식으로 작업 단위를 통째로 처리한다.

    설치 전 준비

    • Claude Pro·Max·Teams·Enterprise 구독 (또는 Anthropic Console API 계정)
    • 터미널 환경 — Mac은 기본 터미널 또는 iTerm2, Windows는 PowerShell 또는 Git Bash
    • Windows 사용자라면 Git for Windows 사전 설치

    운영체제별 설치

    macOS / Linux

    curl -fsSL https://claude.ai/install.sh | bash

    Windows PowerShell

    irm https://claude.ai/install.ps1 | iex

    macOS Homebrew

    brew install --cask claude-code

    Homebrew와 WinGet으로 설치하면 자동 업데이트가 안 된다는 점 한 가지만 기억해 두자. 주기적으로 brew upgrade claude-codewinget upgrade Anthropic.ClaudeCode를 실행해 최신 버전을 유지한다.

    첫 실행과 로그인

    설치가 끝나면 작업할 프로젝트 폴더로 이동해 실행한다.

    cd /path/to/your/project
    claude

    처음 실행 시 로그인 화면이 나타난다. 브라우저가 자동으로 열리면 Claude 계정으로 로그인하고 끝. 만약 브라우저가 안 열린다면 c 키를 눌러 URL을 복사해 직접 붙여 넣으면 된다. 이 한 번의 로그인이 끝이다. 이후로는 claude만 입력하면 바로 시작된다.

    기본 사용법 — 어렵게 생각하지 말 것

    Claude Code는 대화 방식으로 작동한다. “어떻게 명령을 써야 하지”를 고민하지 말고, 하고 싶은 걸 평소 말하는 대로 입력하면 된다. 다음은 자주 쓰는 패턴 다섯 가지다.

    • 코드 이해: “이 프로젝트가 뭘 하는 건지 설명해 줘”
    • 기능 추가: “로그인 폼에 입력값 유효성 검사 추가해 줘”
    • 버그 수정: “빈 폼을 제출해도 통과되는 버그가 있어, 수정해 줘”
    • Git 커밋: “변경 사항을 설명적인 메시지로 커밋해 줘”
    • 테스트 작성: “이 함수에 대한 단위 테스트 작성해 줘”

    중요한 안전장치 하나. 파일을 수정하기 전에 항상 사용자의 승인을 요청한다. 모든 변경이 본인의 OK 후에야 적용되기 때문에 마음 편히 시켜도 된다.

    알아두면 유용한 명령어

    명령어 기능
    claude 대화형 모드 시작
    claude "작업 내용" 일회성 작업 실행
    claude -c 직전 대화 이어서 시작
    /clear 대화 기록 초기화
    /help 사용 가능한 명령 전체 보기
    /doctor 설치 상태 자가 진단

    입문 단계에서 가장 자주 쓰게 되는 건 /clear/help 두 개다. 막혔을 때 /doctor를 기억해 두면 troubleshooting이 쉬워진다.

    그래서 — 5분이면 충분하다

    Claude Code는 단순한 코드 완성 도구가 아니다. 프로젝트 전체 맥락을 파악하고, 파일 사이의 연결을 이해하면서 작업한다. 이게 다른 도구와의 가장 큰 차이다. 기존에 Claude Pro를 구독 중인 사람이라면 추가 비용 없이 지금 당장 시작할 수 있다는 점도 결정 비용을 0에 가깝게 만든다. 설치 5분, 첫 대화 1분이면 충분하다.

    지금 할 일

    위에 정리된 한 줄 명령어로 Claude Code를 5분 안에 설치한다. 이미 진행 중인 프로젝트 폴더로 들어가 claude를 실행한 다음, 첫 질문은 “이 프로젝트 설명해 줘”로 시작해 보자. AI가 본인 코드베이스를 어떻게 이해했는지 한 화면에 드러난다. 그 다음 단계는 평소 미뤄 두고 있던 작은 버그나 기능 추가를 자연어로 시켜 보는 것. 그 한 사이클을 끝내면 Claude Code의 진짜 매력이 손에 잡힌다.

    관련 글

    출처

  • Claude Opus 4.6 완전 분석: 앤트로픽 Cowork 출시, AI 에이전트 시대 본격화

    앤트로픽이 2026년 첫 주요 모델을 내놨다

    2026년 2월 5일, 앤트로픽은 Claude Opus 4.6을 출시했다. 코딩·추론·에이전트 작업에서 현재 가장 높은 성능을 기록한 모델로, 같은 달 GPT-5.2를 GDPval-AA 벤치마크에서 144 Elo 포인트 차이로 앞섰다. 경제적으로 가치 있는 지식 업무—금융 분석, 법률 검토, 복잡한 연구—에서의 실전 격차가 처음으로 수치로 드러난 것이다.

    그런데 모델 성능보다 더 중요한 변화가 같은 시기에 조용히 시작됐다. Claude Cowork의 등장이다.

    Cowork: “코딩 없는 사람”을 위한 Claude Code

    Claude Code가 개발자를 위한 터미널 기반 에이전트였다면, Cowork는 비개발자를 위한 GUI 기반 에이전트다. 2026년 1월 리서치 프리뷰로 출시된 Cowork는 Claude 데스크탑 앱에서 직접 실행되며, 격리된 가상 환경 안에서 로컬 파일과 MCP 연동을 처리한다.

    실제로 할 수 있는 일은 명확하다. 재무 보고서를 받아 엑셀 분석을 만들고, 회의록을 요약해 파워포인트 초안을 완성하고, 반복 작업을 스케줄러로 등록해 자동 실행한다. 코드를 모르는 기획자나 분석가가 ‘디지털 동료’처럼 AI를 쓸 수 있는 첫 번째 제대로 된 인터페이스다.

    3월 23일에는 한 발 더 나아갔다. Claude Computer Use Agent가 리서치 프리뷰로 공개됐다. 화면을 보고, 버튼을 클릭하고, 앱을 열고, 여러 단계 워크플로를 사람 없이 완수하는 기능이다. Pro·Max 구독자는 Cowork와 Claude Code를 통해 사용할 수 있다.

    Opus 4.6의 기술적 특징: 개발자가 알아야 할 것들

    성능 수치보다 실제 작업 방식의 변화가 더 의미 있다.

    먼저 1M 토큰 컨텍스트 창(베타)이 추가됐다. Opus급 모델에서는 처음이다. 대형 코드베이스 전체를 컨텍스트에 올리거나, 수백 페이지 분량의 문서를 한 번에 분석하는 작업이 가능해졌다. 출력은 128K 토큰까지 늘었다—이전 64K의 두 배다.

    Adaptive Thinking은 모델이 질문의 복잡도를 스스로 판단해 ‘생각하는 시간’을 조절하는 기능이다. 간단한 질문엔 빠르게, 복잡한 추론에는 더 많은 컴퓨팅을 쓴다. API에서는 thinking: {type: "adaptive"}로 활성화할 수 있다.

    Claude Code에서는 이제 에이전트 팀을 구성해 하나의 작업에 병렬로 투입할 수 있다. 컨텍스트 압축(compaction) 기능도 추가돼 긴 작업에서 한계에 부딪히지 않고 지속적으로 실행된다.

    가격은 그대로다. API 기준 입력 $5, 출력 $25 (백만 토큰당).

    그래서 지금 Claude Code 쓰는 한국 개발자에게 뭐가 달라지나

    Claude Code의 에이전트 팀 기능은 단순히 ‘더 빨라졌다’는 의미가 아니다. 복잡한 리팩토링이나 테스트 작성 같은 작업을 여러 에이전트에 나눠 병렬로 처리할 수 있다는 의미다. 월 $200으로 부담스럽다고 느꼈다면, 이제 그 비용이 이전보다 훨씬 넓은 범위의 작업을 커버한다.

    비개발자라면 Cowork가 선택지가 됐다. 엑셀·파워포인트 연동, 스케줄 자동화, 컴퓨터 사용 에이전트까지—AI를 ‘채팅 도구’로 쓰던 단계에서 ‘자율 실행 도구’로 전환하는 가장 접근하기 쉬운 경로다.

    지금 바로 할 수 있는 것

    • Claude Code 사용자: claude-opus-4-6 모델로 에이전트 팀 기능 테스트. 병렬 작업이 필요한 대형 리팩토링에 적용해볼 것.
    • 비개발자: Claude.ai Pro 또는 Max 구독 후 Cowork 리서치 프리뷰 신청. 반복 문서 작업 자동화부터 시작.
    • API 개발자: Adaptive Thinking(thinking: {type: "adaptive"}) 모드로 추론 비용 최적화 실험.

    관련 글


    출처

  • OpenCode 리뷰: GitHub 스타 12만, 오픈소스 AI 코딩 에이전트

    올해 초, AI 코딩 커뮤니티에서 소동이 있었다. Anthropic이 Claude Pro·Max 구독 토큰을 서드파티 툴에서 사용하지 못하도록 기술적으로 차단했다. 월 100~200달러를 내던 사용자들이 갑자기 기존 방식으로 쓸 수 없게 됐다.

    그 2주 동안 GitHub 스타를 1만 8천 개 더 받은 툴이 있다. OpenCode다. 논란이 사실상 최고의 마케팅이 됐다. 나는 이 소동을 지켜보면서 툴 자체가 궁금해졌다. 소란이 아니라, 실제로 쓸만한가.

    OpenCode란

    Anomaly Innovations가 개발한 오픈소스 AI 코딩 에이전트다. 터미널, IDE, 데스크톱 앱 세 가지 환경을 모두 지원한다. 2024년 말 등장 이후 빠르게 성장했다. 현재 GitHub 스타 12만 개 이상, 월간 활성 개발자 500만 명 규모다.

    이 툴의 핵심 철학은 두 가지다. 어떤 AI 모델이든 연결해서 쓸 수 있다. 그리고 코드는 서버에 저장하지 않는다. Anthropic, OpenAI, Google Gemini, AWS Bedrock, Groq 등 75개 이상의 프로바이더를 지원한다. Ollama를 통한 로컬 모델 연동도 된다.

    단순 코드 자동완성 툴과 다른 점이 있다. 에이전트로 작동한다는 것이다. 프로젝트 파일을 읽고, 코드를 작성하고, 명령을 실행한다. git과도 상호작용한다. 스니펫 제안에서 멈추지 않고 코드베이스에 직접 행동한다. 마치 주니어 개발자에게 태스크를 위임하는 감각이다.

    출처 @opencode git

    좋았던 점

    모델을 고를 수 있다는 것 자체가 전략이다. 세션 중간에도 모델을 전환할 수 있다. 대규모 리팩토링에는 컨텍스트 윈도우가 넓은 모델을 쓴다. 빠른 반복 작업에는 비용이 낮은 모델을 쓴다. 두 가지를 혼합하는 전략이 가능하다. 특정 프로바이더에 고정된 Copilot이나 Cursor와 비교하면 선택지 자체가 다르다.

    LSP 통합이 환각을 줄인다. Language Server Protocol 서버와 직접 통합한다. Rust, TypeScript, Python 등 주요 언어의 LSP를 자동으로 구성한다. 타입 정보와 심볼 맥락이 모델에 전달된다. 단순 LLM 프롬프팅 대비 환각이 줄어드는 게 실제로 느껴진다.

    코드가 서버 밖으로 나가지 않는다. 코드와 컨텍스트 데이터를 일절 저장하지 않는 구조다. 의료, 금융, 국방처럼 코드를 외부로 전송할 수 없는 환경에 적합하다. 로컬 모델을 붙이면 규정 준수와 AI 지원을 동시에 가져갈 수 있다. 오픈소스이기 때문에 내부 코딩 표준을 강제하거나 보안 정책에 맞게 수정하는 것도 가능하다.

    단일 인터페이스로 여러 환경을 커버한다. CLI, macOS·Windows·Linux 데스크톱 앱, VS Code·Cursor·Windsurf IDE 확장을 모두 제공한다. 서버 모드를 통한 WebUI도 있다. 여러 VPS 인스턴스를 프로젝트별로 띄우고 WebUI로 통합 관리하는 사례도 나오고 있다.

    아쉬운 점

    “무료”는 절반만 맞는 말이다. 소프트웨어 자체는 무료다. 하지만 API 비용은 전액 사용자 부담이다. Claude Sonnet이나 GPT-4o로 집중 작업하면 월 20~50달러가 나올 수 있다. 작업 강도가 높으면 그 이상도 보고된다. Copilot 구독과 단순 비교하면 오히려 비쌀 수 있다. 본격 도입 전에 자신의 작업 패턴 기준으로 비용을 직접 계산해봐야 한다.

    아직 성숙 중인 툴이다. 대형 코드베이스에서 컨텍스트 한계에 부딪혔을 때 처리가 불투명하다. 에이전트가 중간에 실수하면 롤백이 번거롭다. 메모리 점유가 과하다는 지적도 커뮤니티에서 꾸준히 나온다. 2026년 초 기준으로 문서화가 기능 출시 속도를 따라가지 못하고 있다. 동작 방식을 파악하려면 소스코드나 GitHub 이슈를 직접 뒤져야 할 때도 있다.

    이런 분께 추천

    Copilot이나 Cursor에 만족하고 있다면 굳이 갈아탈 필요는 없다. OpenCode는 다음 상황에서 실질적인 가치를 낸다. 코드를 외부 서버로 전송할 수 없는 규정 준수 환경이 첫 번째다. 개발자 50명 이상 조직에서 라이선스 비용 압박이 있는 경우도 해당된다. 특정 AI 프로바이더에 종속되는 것을 피하고 싶은 팀, 터미널 중심 워크플로에 익숙하고 직접 설정을 즐기는 개발자에게도 맞다.

    반대로 GUI 환경에서 바로 생산성을 내야 하는 입문자, 설정과 유지보수에 시간을 쓰기 어려운 소규모 팀에게는 맞지 않는다.

    So what — Kevin의 코멘트

    국내 엔터프라이즈 시장을 보면 오픈소스와 상용 솔루션의 경계가 꽤 명확하다. “무료”나 “오픈소스”라는 단어가 오히려 채택을 망설이게 만드는 경우를 자주 봤다. 지원 체계가 있냐, 책임 소재가 어디냐, 보안 검토를 통과할 수 있냐. 이 세 가지 질문 앞에서 오픈소스는 늘 불리한 출발선에 선다.

    OpenCode는 기술적으로 흥미롭다. 기여자 800명, Fortune 500 내부 포크 사례까지 나오고 있으니 발전 속도도 기대된다. 하지만 국내 대기업이나 금융권에서 “오픈소스 코딩 에이전트를 도입하자”는 제안이 결재를 통과하기까지는 시간이 걸릴 것이다. 당장은 스타트업, 개인 개발자, 또는 사내 IT 부서의 내부 툴링 용도가 현실적인 진입점이라고 본다. 엔터프라이즈 채택은 OpenCode가 상용 지원 모델을 갖추거나, 검증된 레퍼런스가 쌓인 이후의 이야기다.

    최종 평가

    OpenCode는 논란 덕분에 알려졌다. 하지만 기술적 완성도도 무시할 수준이 아니다. 벤더 락인을 피하려는 흐름이 본격화되는 지금, 모델 유연성과 프라이버시 아키텍처는 단순 기능이 아니라 포지셔닝 자체다. 다만 “오픈소스라 무료”라는 인식과 실제 운영 비용 사이의 간극을 사전에 계산하지 않으면 실망으로 이어질 수 있다. 통제권과 유연성이 우선순위인 팀에게는 현재 시장에서 가장 설득력 있는 선택지 중 하나다.

    출처

    • Hacker News — OpenCode – Open source AI coding agent (2026.03.24)
    • DEV Community — OpenCode: The Open Source AI Coding Agent Reviewed (2026.03.21)
    • InfoQ — OpenCode: an Open-source AI Coding Agent Competing with Claude Code and Copilot (2026.02)
    • abit.ee — OpenCode: The Open-Source AI Coding Agent with 126,000 GitHub Stars (2026.03.21)
    • BattleAITools — OpenCode Review — 8.4/10 AI Coding Tool Rating (2026)
    • opencode.ai — 공식 홈페이지