로블록스 기획서의 “작게”와 코드의 102스터드

지난 게임에서는 하네스를 마지막 날에 붙였다. 이번엔 코드보다 먼저 놨다. 하루에 12,903줄이 나왔고 테스트 354단언이 통과했는데, 맵은 어이없게 작았다.

저주대전 저장소를 열어보면 순서가 그대로 남아 있다. 첫 커밋이 2026-08-05, 마지막 커밋이 08-19. 그 사이 19개 커밋이 쌓였고 src/ 는 44파일 27,847줄이 됐다. .claude/agents/ 의 첫 파일이 생긴 시각은 08-19 12:34다. 14일치 코드를 다 쓰고 나서 에이전트를 만들었다는 뜻이다. 그 저장소에는 tests/ 디렉터리가 아예 없다. 검증이 없었던 건 아니다. 교차 참조 검사기를 23개까지 만들어 붙였고 규칙 문서는 MCP 서버로 잘라서 에이전트에 물렸다. 다만 그게 전부 코드를 다 쓴 뒤에 따라온 것들이었다.

하네스를 붙이던 커밋 메시지에 스스로 진단이 적혀 있다.

chore: 하네스 구성 — 정적 검증은 L3 인데 실기 검증만 L0(산문)이었다

교차 참조 검사 24종이 커밋을 물리적으로 막을 만큼 서 있는 동안, "스튜디오에서 실제로 봤는가"는 인계 문서의 산문 표에 47행까지 쌓여 있었다. 확인했다고 적을 자리가 없어서 매 세션이 47개를 다시 읽고도 무엇이 끝났는지 몰랐다. 코드를 다 쓴 다음에야 그 비대칭이 보였다.

같은 커밋의 마지막 줄은 이렇게 끝난다. "하네스 자체는 실제 작업으로 돌려본 적이 없다. 정적 검증까지다."

그래서 오늘은 순서를 바꿔봤다. 로블록스 게임 두 개를 같은 날 시작하면서 기획서와 하네스를 코드보다 먼저 놨다. 하루 만에 12,903줄이 나왔고 테스트 354단언이 통과했고 둘 다 실행됐다. 그래도 돌아가긴 하네, 싶었다. 그런데 열어보니 맵이 작았다. 354개 중에 그걸 물어본 단언은 하나도 없었다.

밝은 책상에 펼쳐진 도면에서 치수 칸 하나만 비어 있고 그 옆에 줄자가 접힌 채 놓인 클로즈업

12시 24분에 기획서가 먼저 있었다

오늘 로블록스 게임 두 개를 동시에 시작했다. 「우리 학교가 이상하다」(1~4인 협동 관찰 공포, 전투 없음)와 「몬스터 군도 탐험대」(1~4인 협동 액션 RPG). 먼저 출시한 저주대전의 코드는 한 줄도 가져오지 않았다.

파일 생성 시각이 순서를 그대로 기록하고 있다.

12시 24분에 docs/03 게임 기획서가 생겼다. 14분 뒤인 12시 38분에 docs/04 기술설계서, 그러니까 계약 문서가 나왔다. 12시 45분부터 51분 사이에 .claude/agents/ 5개와 .claude/skills/ 7개가 만들어졌고, 12시 51분에 default.project.jsonrokit.tomlselene.tomlstylua.toml 이 붙었다. 첫 Luau 파일은 12시 52분, 첫 테스트 스펙은 12시 53분이다.

군도는 30분 뒤에 같은 순서를 밟았다. 12:57 기획서, 13:18 기술설계서, 13:20~13:26 에이전트 5 + 스킬 8, 13:26 뼈대, 13:27 첫 코드.

기획서는 미리 ChatGPT에 맡겨둔 것을 그대로 가져왔다. 여기서 중요한 건 내용이 아니라 위치다. 지난번엔 코드가 최상류였고 문서가 코드를 뒤따라갔다. 이번엔 코드 위에 두 층이 먼저 서 있었다. 무엇을 만드는지를 정하는 기획서, 그리고 그것을 서비스 경계와 리모트 스키마로 번역한 계약 문서.

docs/01(플랫폼 학습노트)과 docs/02(제작 워크플로우)는 두 저장소 모두 파일 생성 시각이 08-04 21:25와 21:26이다. 저주대전 시절에 쌓아둔 지식이 그대로 이월됐고, 오늘 2026 기준으로 갱신만 했다. 새 프로젝트를 두 개 여는 데 학습 비용이 거의 안 든 이유다. 첫 게임 때는 만들기 전에 플랫폼부터 공부시키느라 이 두 문서를 만드는 데만 하루를 썼다.

하루에 12,903줄이 나왔고 돌아갔다

결과부터 말하면 됐다.

학교는 Luau 파일 39개에 5,496줄, 문서 5편 2,149줄, 에이전트 5개와 스킬 7개, 테스트 단언 94개가 나왔다. 군도는 파일 46개에 7,407줄, 문서 4편 2,202줄, 에이전트 5개와 스킬 8개, 테스트 단언 260개다.

두 개 합쳐 12,903줄이다. 저주대전이 14일에 27,847줄이었으니 하루 평균으로는 그때의 6.5배가 나왔다. 그리고 둘 다 실행됐다. 학교는 Rojo 없이도 열리도록 tools/build_rbxlx.py.rbxlx 를 직접 뽑아 스튜디오에서 열었고, 군도는 Open Cloud REST로 올렸다.

하네스가 실제로 무슨 일을 했는지도 파일에 남아 있다. 각 저장소의 오케스트레이터 스킬은 본문 맨 앞이 트리아지 표다. 영향 범위·변경 성격·가역성·검증 결정성 네 신호 중 가장 높은 것이 tier를 정하고, T0은 에이전트 호출 없이 직접 편집한 뒤 diff만 보고 끝낸다. Config 수치 하나 바꾸는 데 전체 검증을 도는 것 자체를 결함으로 규정해뒀다.

서비스 경계도 표로 먼저 못 박혀 있었다. 군도 쪽은 SaveService 가 DataStore I/O와 세션 락을 소유하되 게임 로직 판단은 하지 않고, CombatService 가 히트 판정과 데미지 공식을 소유하되 스킬이 무엇을 하는지는 모른다. 이 표가 코드보다 40분 먼저 있었기 때문에, 14개 서비스가 순서대로 쏟아져 나오는 동안 "일단 여기 넣자"가 한 번도 안 나왔다.

여기까지가 의도한 대로 된 부분이다.

테스트를 실제로 돌려봤다

두 저장소의 CLAUDE.md 변경 이력에는 테스트 수가 적혀 있다. 군도는 "테스트 260개", 학교는 "테스트 97 단언". 이런 숫자는 확인이 필요하다. 적어둔 쪽과 검사받는 쪽이 같으면 그 숫자는 주장이지 측정이 아니다.

문제는 러너가 lune 을 요구한다는 것이다. 그리고 이 기기에는 rojo·stylua·selene·lune·rokit 이 전부 미설치다. 두 저장소의 docs/02 에는 "미설치로 건너뛴 검증은 반드시 명시한다"고 적혀 있는데, 명시할 대상이 검증 전체였다.

luau 인터프리터는 설치돼 있어서 러너만 직접 짜서 스펙 모듈을 물려봤다. 군도부터.

Formulas     36 단언
RateLimit     6 단언
Util         19 단언
Data        199 단언
passed 260 / failed 0
  1. 변경 이력에 적힌 숫자와 정확히 같다. 199개가 루프에서 생성되는 데이터 검증이라 우연히 맞을 숫자가 아니다. 이 스위트는 어느 시점엔가 실제로 돌았다고 봐야 한다.

학교는 달랐다.

RoundSchedule    11 단언
RateLimit         5 단언
Grade             8 단언
LOAD FAIL AnomalySpawn: ../Shared/Config.luau:137: attempt to index nil with 'fromRGB'
LOAD FAIL Config:       ../Shared/Config.luau:137: attempt to index nil with 'fromRGB'
SchoolLayout     70 단언
합계: 통과 94 / 실패 0

6개 스위트 중 2개가 로드조차 안 된다. Config.luau 137번째 줄이 Color3.fromRGB(5, 5, 10) 이다. 조명 색을 밸런싱 수치와 같은 파일에 넣어뒀고, Color3 는 로블록스 런타임 밖에 존재하지 않는다.

이건 오타가 아니라 규칙 두 개가 부딪힌 자리다. 학교 CLAUDE.md 의 절대 규칙 4번은 "밸런싱 숫자는 Shared/Config.luau 에만 둔다"이고, docs/02 의 테스트 원칙은 "순수 로직만 테스트한다. 로블록스 API에 의존하는 코드는 유닛 테스트하지 않는다"이다. 조명 색은 밸런싱 수치이면서 동시에 로블록스 타입이다. 두 규칙을 다 지키면 Config.spec 은 존재할 수 없다.

그런데 Config.spec 은 존재하고, 15개 단언이 들어 있고, 한 번도 실행된 적이 없다. 실행됐다면 첫 줄에서 터졌을 테니까. 같은 저장소의 AnomalySpawn.spec 은 이 문제를 이미 알고 있다는 흔적까지 남겼다. 주석에 "AnomalyDefs 자체는 Roblox API 를 require 하므로 lune 에서 못 읽는다"고 적고 정의 테이블을 스펙 안에 손으로 복사해뒀다. 우회는 한 파일에서 하고, 바로 옆 파일에서 같은 벽에 부딪혔다.

여기까지는 하네스 얘기다. 그런데 오늘 실제로 걸린 문제는 이게 아니었다.

맵이 작았다

두 게임 다 열어보면 바로 보인다. 맵이 작다.

학교 쪽은 좌표가 데이터 파일 하나에 모여 있어서 계산이 된다. SchoolLayout.luau 를 읽어 재보면 이렇다.

복도가 20 × 120 으로 바닥 2,400 이다. 교실은 40 × 32 로 1,280, 과학실과 음악실이 각각 36 × 32 로 1,152 씩이다. 화장실이 32 × 28 로 896, 경비실과 정문이 각각 24 × 24 로 576 이다.

방 7개, 전체 바운딩 박스 102 × 176 스터드, 바닥 합계 8,032 제곱스터드. Config.Move.walkSpeed 가 14니까 복도를 끝에서 끝까지 걷는 데 8.6초다. 학교 전체를 대각선으로 가로질러도 15초가 안 걸린다.

한 판은 8~12분으로 설계돼 있다. 8.6초짜리 복도에서 8분을 보내는 게임이다.

그리고 이 맵에는 회귀 테스트가 붙어 있다. SchoolLayout.spec 이 70단언이다. 94개 중 74%가 이 파일 하나에 있다. 스펙 상단 주석에 왜 만들었는지가 적혀 있다.

실제로 났던 버그: 방 벽에 문만 뚫고 방↔복도 사이 통로 바닥을 만들지 않아, 모든 방 입구가 2~6스터드짜리 구멍이 되어 플레이어가 추락사했다.

좋은 테스트다. 실제로 난 사고를 좌표로 고정했고, 새 방을 추가할 때 같은 실수가 다시 나지 않는다. 통로가 방 벽에 닿는지, 복도 벽에 닿는지, 통로 폭이 문 너비와 같은지, 복도 개구부 수가 통로 수와 맞는지, 방끼리 겹치지 않는지를 전부 검사한다.

70개 중 맵이 충분히 큰지 묻는 건 하나도 없다.

기획서를 열어봤다

그래서 기획서를 봤다. §6 맵 구성. 이 절이 맵에 대해 말하는 내용의 전부가 첫 줄이다.

맵은 길을 잃지 않을 정도로 작게 만든다.

그다음은 아스키 다이어그램 하나와 방별 설명이다. 다이어그램은 어느 방이 어느 복도에 붙는지를 그리고, 방 설명은 무엇이 놓이는지를 적는다. 일반 교실이면 "책상, 의자, 칠판, 시계". 과학실이면 "인체 모형, 비커, 해골 모형".

책상이 몇 개인지는 없다. 교실이 몇 평인지도 없다. 복도가 몇 미터인지, 플레이어가 경비실에서 음악실까지 몇 초 걸려야 하는지도 없다.

기획서 전체를 스터드·크기·면적·거리로 검색하면 잡히는 줄이 다섯 개고, 그중 넷은 MVP 범위를 적은 목록("학교 맵 1개", "복도와 교실 1개")이다. 나머지 하나가 저 "작게"다.

군도도 같은 모양이다. §5.2 메인섬에 주요 지역 11개가 목록으로 나열돼 있다. 개척항, 탐험 길드, 제작소와 창고, 선착장, 초보자 숲, 폐허가 된 성, 거대한 고목, 폭포 뒤 동굴, 별빛 관측소, 봉인된 지하 신전, 엔드게임 폭풍 관문. 26개 절 어디에도 스터드 값이 없다. Vertical Slice 범위는 "메인섬 일부"라고 적혀 있다.

코드는 그 "일부"를 420 × 420 스터드 섬 하나로 정했다. 어디서 나온 숫자인지는 아무 데도 안 적혀 있다.

확정된 결정 12개 중 크기는 0개였다

기획서가 대충 만들어졌다는 얘기가 아니다. 오히려 반대다.

학교 기획서에는 §22 「기술 검토로 확정된 제약」이 붙어 있다. v0.2에서 추가된 절이고, 내용이 꽤 촘촘하다.

  • 콘텐츠 등급을 Mild 로 고정한다. Moderate 가 되면 Roblox Select(9–15)와 16+로 밀려 주 타깃 절반이 사라진다.
  • 조사 대상에 ProximityPrompt 를 쓰지 않는다. 뜨는 순간 정답을 알려준다. 대신 화면 중앙 조준 + 1초 홀드.
  • StreamingEnabled = false. 켜면 이상현상 파트가 늦게 도착해 "분명 저기 뭐가 있었는데" 하는 가짜 버그가 생긴다.
  • 손전등은 플레이어당 정확히 1개. 활성 라이트 개수가 프레임 성능의 단일 최대 변수다.
  • Max Players = 4.
  • 사운드를 그래픽보다 먼저 만든다.

기술설계서 §13에도 확정 결정이 6개 더 있다. 맵을 코드로 생성할 것인가(코드 생성), 이상현상 정의를 어디에 둘 것인가(서버 전용 폴더), 로비를 별도 Place로 뺄 것인가(단일 Place), 음성 채팅(없음), 입력 방식(ContextActionService).

합쳐서 12개다. 전부 잘 벼려진 결정이고, 근거가 붙어 있고, 대부분 나중에 뒤집기 비싼 것들이다.

그리고 12개가 전부 범주형이다. Mild냐 Moderate냐, 프롬프트냐 레이캐스트냐, 켜느냐 끄느냐, 코드냐 에셋이냐. 얼마나 크냐를 정한 결정은 하나도 없다.

Max Players = 4손전등 1개 는 숫자처럼 보이지만 사실 열거값이다. 4는 협동 인원의 상한이고 1은 있음/없음의 다른 표기다. 둘 다 "크기"가 아니라 "종류"를 고른 것이다.

하네스도 같은 모양을 물려받았다. 학교 오케스트레이터 스킬에는 「이 프로젝트에서 반복 발생하는 실패」가 여섯 개 적혀 있다. 이상현상 cleanup 누락, 정답 이름 노출, canReport 우회, CanQuery=true 이펙트, Config 밖 숫자 리터럴, Mild 초과 연출. 하나같이 있으면 사고고 없으면 정상인 항목이다. 군도 쪽 절대 규칙 10개도 그렇다. 서버가 다시 계산하는가, 아이템을 UID로 참조하는가, UpdateAsync 를 쓰는가. 예/아니오로 답할 수 있어서 검사로 옮길 수 있었고, 실제로 옮겼다.

여기서 규칙을 만든 순서가 보인다. 예/아니오로 답할 수 있는 것부터 규칙이 됐다. 그건 검사로 옮기기 쉬워서였지 그게 더 중요해서가 아니었다. 맵이 얼마나 커야 하는가는 예/아니오가 아니라서 규칙 목록에 한 번도 오르지 못했고, 오늘 실제로 게임을 망친 건 그쪽이었다.

유일한 정량 기준을 계산해봤다

기획서 전체에서 크기에 관한 정량 기준을 딱 하나 찾았다. 군도 §21 Vertical Slice의 통과 기준 1번이다.

플레이어가 15분 안에 직업 스승을 발견할 수 있다.

이 기준이 왜 있는지도 적혀 있다. §23 주요 위험의 1번이 "직업 스승을 못 찾고 이탈"이다. 시작 화면에서 직업을 고르지 않는 게 이 게임의 차별점인데, 그 대가로 스승을 못 찾으면 플레이어가 무기 없이 헤매다 나간다. 그래서 15분이라는 상한을 걸었다.

좌표가 데이터 파일에 있으니 재볼 수 있다. 스폰 지점은 (0, 24), 수호자 스승 발트는 폭포 뒤 동굴 (-140, -150). 직선거리 223스터드다. baseWalkSpeed 가 16이니 걸어서 14초.

기획서가 의도한 경로대로 소문 NPC 셋을 다 들르면 이렇게 된다.

스폰에서 길드 접수원까지 36, 거기서 늙은 선원까지 55, 다시 행상 코라까지 57 이다. 마지막 동굴의 발트까지가 245 라 합계 393 이 나온다.

393스터드, 걸어서 25초. 대화 시간과 시험 시간은 뺀 순수 이동 시간이다.

기준은 900초였다. 36배 여유로 통과한다.

통과하긴 했는데, 이건 검증이 아니라 맵이 작다는 증거다. 15분 기준은 "스승이 너무 멀지 않은가"를 물으려고 만든 것이고, 실제로 나온 답은 "25초"였다. 이 정도로 크게 빗나가면 그 기준은 더 이상 아무것도 못 거른다. 통과했다는 사실 자체가 정보가 아니게 된다.

하네스가 검사할 수 있는 것은 관계뿐이다

여기까지 오니 354개 단언이 무엇을 검사하고 있었는지가 다르게 보인다.

Config.spec 의 15개를 그대로 옮기면 이렇다. chaseSpeed < sprintSpeed, 스프린트가 탈출 수단이 되려면 괴물보다 빨라야 하니까. walkSpeed < chaseSpeed, 그래도 추격은 성립해야 하니까. serverRange > aimRange, 서버 검증 거리가 클라 조준 거리보다 좁으면 정상 신고가 거부되니까. 스폰 개수 가중치 합이 1. 공포 임계값이 오름차순. 정답은 게이지를 낮추고 오답은 올린다.

전부 두 숫자 사이의 관계이거나 부호이거나 이다. 절대값이 적절한지 묻는 단언은 하나도 없다.

SchoolLayout.spec 의 70개도 같다. 존재하는가, 중복되지 않는가, 닿는가, 겹치지 않는가, 개수가 맞는가. 군도의 260개도 같은 모양이다.

그럴 수밖에 없다. 관계는 문서 없이도 검사할 수 있다. 스프린트가 탈출 수단이라는 서술 한 줄만 있으면 chaseSpeed < sprintSpeed 라는 단언이 자동으로 따라 나온다. 가중치 합이 1이어야 한다는 것도 확률 분포라는 말에서 바로 나온다. 방이 겹치면 안 된다는 것도 물리 세계의 기본값이다.

크기는 다르다. 102 × 176이 학교로 적절한지는 코드 안 어디를 봐도 알 수 없다. 그건 문서에 숫자가 있어야만 검사할 수 있고, 문서에는 "작게"라고 적혀 있었다.

내부 정합성은 하네스가 잘한다. 외부 적정성은 명세가 있어야만 검사 대상이 된다. 오늘 두 저장소가 증명한 게 정확히 이 경계다.

그리고 테스트가 임의값을 계약으로 승격시킨다

한 가지 더 걸리는 게 있다.

학교 기획서의 아스키 다이어그램에는 복도가 세 개 그려져 있다. 서쪽 복도, 중앙 복도, 동쪽 복도. 노드를 세면 아홉 개다.

실제 SchoolLayout.rooms 에는 방이 일곱 개고 복도는 Hallway 하나다. 복도 셋이 하나로 접혔다.

그 축소를 기록한 문장은 어디에도 없다. 기술설계서는 "학교 7구역 절차 생성"이라고만 적는다. 왜 9가 7이 됐는지, 복도 셋을 하나로 합쳐도 되는 이유가 무엇인지는 두 문서 어디에도 없다. 코드가 조용히 정했다.

그런데 테스트는 그 결과를 이미 계약으로 취급하고 있다. SchoolLayout.spec 은 복도를 제외한 모든 방에 통로가 정확히 하나 있어야 한다고 검사하고, 복도 개구부 수가 통로 수와 같아야 한다고 검사한다. 지금 기획서대로 복도를 셋으로 되돌리면 이 단언들이 먼저 깨진다.

순서를 다시 보면 이렇게 된다. 기획서에 숫자가 없다 → 코드가 임의로 채운다 → 회귀 테스트가 그 값을 고정한다 → 그 값이 계약이 된다. 어느 단계에서도 규칙을 어기지 않았고, 세 번째 단계는 오히려 잘한 일이다. 그런데 결과적으로 아무도 결정한 적 없는 숫자가 저장소에서 가장 단단한 것이 됐다.

순서를 바꾼 건 유효했다

오해를 살까 봐 적어두면, 하네스를 먼저 세운 건 되돌릴 생각이 없다.

저주대전에서는 맵이 작다는 걸 알아도 어디를 고쳐야 하는지 찾는 게 일이었다. 좌표가 코드 여기저기에 흩어져 있었으니까. 오늘 학교는 SchoolLayout.luau 파일 하나를 열면 방 일곱 개의 좌표가 아홉 줄 안에 다 있다. 숫자를 바꾸고 테스트를 돌리면 어디가 깨지는지 바로 나온다. 군도도 World.luau 가 "게임 로직은 Workspace 인스턴스가 아니라 이 파일을 읽는다"고 선언하고 좌표를 독점한다.

달라진 건 결과물의 품질이 아니라 어디를 봐야 하는지 아는 속도였다.

손볼 곳은 하네스 위층이다. 기획서에 스칼라를 적는 칸을 강제해야 한다.

재밌는 건 시간 축은 이미 적혀 있었다는 점이다. 학교 §1에 "한 판 8~12분", 군도 §21에 "15분 안에". 시간에 대해서는 감이 서 있었고 공간에 대해서는 안 서 있었다. 8~12분과 8.6초짜리 복도가 같은 문서에 나란히 있다. 다음 기획서에는 최소한 이 세 줄이 필요하다. 플레이어가 A에서 B까지 몇 초, 한 구역에 상호작용 대상 몇 개, 한 판에 방문할 구역 몇 개.

형용사는 코드로 옮겨지지 않는다. "길을 잃지 않을 정도로 작게"는 102가 될 수도 있고 1,020이 될 수도 있는데, 둘 다 그 문장을 만족한다. 어느 쪽을 골라도 위반이 아니니 하네스는 통과시킨다.

아직 모르는 것

맵을 키우면 재밌어지는지는 아직 모른다. 오늘은 맵 수치를 하나도 안 고쳤다. 이 글에 나온 102 × 176은 지금 저장소에 들어 있는 값 그대로다.

스튜디오 실행 검증도 아직이다. 두 저장소의 CLAUDE.md 출시 차단 항목에 그렇게 적혀 있고, 학교 쪽은 사운드 13종이 전부 빈 문자열이라 소리 없는 공포게임 상태다. 테스트를 luau 로 돌린 것도 정식 경로가 아니다. 러너는 lune 을 요구하고 lune 은 설치돼 있지 않다. 군도의 260단언이 정확히 맞았다는 사실은 그 스위트가 언젠가 돌았다는 뜻이지, 오늘 정식 게이트가 돌았다는 뜻이 아니다.

다음에 할 일은 정해져 있다. 기획서에 크기를 숫자로 적고, 그 숫자로 맵을 다시 짓는다. 그때 SchoolLayout.spec 의 70단언 중 몇 개가 깨지는지가 궁금하다. 많이 깨지면 그 테스트들이 실은 맵의 모양이 아니라 오늘 우연히 정해진 크기를 지키고 있었다는 뜻이고, 하나도 안 깨지면 관계만 검사한다는 이 글의 주장이 맞았다는 뜻이다.

둘 중 어느 쪽이든 후속 글이 된다.

댓글

이 블로그의 인기 게시물

아이들은 우리 게임에 들어오지 못한다

AI가 썼다는 브라우저 300만 줄에서 112만 줄은 남의 코드였다

AI가 준 선택지, 초5 아들은 어떻게 골랐을까?