AI와 넉 달, 사이드 프로젝트 네 개를 굴렸다

새벽 한 시쯤이었습니다.
아들 방 책상 위에 반쯤 조립된 아크릴 샤시가 올라가 있었습니다. 모터 두 개를 PWM으로 돌리는 코드를 Claude Code에게 받아서 올렸는데, 한쪽 바퀴만 한 방향으로 돌고 다른 쪽은 멈춰 있다가 가끔 떨었습니다. 아들은 "이거 왜 이래?" 하면서 모니터를 들여다보고 있었고, 저는 시리얼 모니터에 찍히는 값을 멍하니 보고 있었습니다.
코드는 깔끔했습니다. 그건 의심하지 않았습니다. L298N 드라이버를 쓰는 표준 패턴이고, 라이브러리 호출도 정상이고, PWM 값도 범위 안이었습니다. 작동만 안 했습니다.
핀맵을 다시 봤습니다. IN1은 8번, IN2는 9번, ENA는 10번. IN3는 11번, IN4는 12번, ENB는 13번. 제가 Claude에게 불러준 대로 적혀 있었습니다.
두 시간을 봤습니다. 코드를 뜯고, 배선을 뜯고, 멀티미터를 대보고, 다시 코드를 짜고. 그러다 ENA에 지정한 10번 핀이 Timer1을 건드리고 있다는 걸 알았습니다. 같은 보드에서 서보 라이브러리도 쓰고 있었고, 둘이 같은 타이머를 두고 다투는 중이었습니다. Claude가 짠 코드는 코드만 보면 멀쩡했습니다. 제 보드에 서보가 있다는 걸 모르고 짰을 뿐입니다.
화는 안 났는데 머리가 조금 차가워졌습니다.
AI가 못한 걸까요. 그건 아니었습니다. ENA가 Timer1에 묶인다는 건 데이터시트에 있고 Claude는 그걸 압니다. AFMotor 라이브러리가 Timer1과 Timer2를 점유해서 충돌 위험이 있다는 것도 압니다. 다만 그날 새벽의 그 세션에는 제 보드 사정이 들어 있지 않았습니다. 서보가 있다는 것도, ENA를 옮길 수 있다는 것도, 13번은 애초에 PWM이 안 되는 핀이라는 것도요.
같은 자리에 세 번 더 멈췄다는 건 한참 지나서야 알았습니다. 도메인은 다 달랐습니다. 사주, 타로, 미연시, 로봇. 이렇게 멀면 막히는 모양도 달라야 정상입니다. 처음엔 실제로 각각 다른 이유로 정리해뒀습니다. 사주는 도메인이 복잡해서, 타로는 의사결정이 안 돼서, 미연시는 감정이 원래 어려운 영역이라서, 로봇은 임베디드라서. 네 개를 옆에 놓고 나서야 비슷한 모양이라는 게 보였습니다.
사주는 22글자로 시작해서 35페이지로 끝났습니다
사주를 만들기로 했을 때는 솔직히 만만하게 봤습니다.
타로를 막 끝낸 직후였습니다. 78장 카드, 정방향과 역방향 합해서 156개 해석, 리딩 레이아웃 몇 개. 그 정도 도메인은 한 세션 안에 들어왔습니다. 사주도 비슷할 줄 알았습니다. 천간 10개, 지지 12개. 스물두 글자. 타로의 78장보다 한참 적습니다.
그게 첫 번째 착각이었습니다.
"혹시 사주 프로그램을 만들고 싶어. 데이터는 좀 있어?"
이 한 문장을 던졌더니 10분도 안 돼서 도메인 윤곽이 정리돼 돌아왔습니다. 60갑자, 오행 상생상극, 십신 분석, 지장간, 12운성, 합충형파해. 각 요소가 뭔지, 서로 어떻게 엮이는지, 코드에서 어떤 구조로 잡아야 하는지까지요.
처음엔 감탄했습니다. 독학하려면 몇 주 걸릴 분야를 10분으로 압축해준다면 거의 모든 도메인이 손 닿는 거리에 들어온다는 뜻이니까요.
코드를 쓰기 시작하니 다른 그림이 나왔습니다.
기존 사주 앱들을 벤치마킹하다가 황당한 걸 봤습니다. 같은 생년월일시를 넣었는데 앱마다 결과가 달랐습니다. 시주가 다르고, 월주가 다르고, 어떤 경우는 일주까지 달랐습니다. 제가 입력을 잘못했나 싶어 다시 넣었는데 같았습니다. 사주 팔자 여덟 글자 자체가 앱마다 다르게 나왔습니다.
같은 입력에 다른 출력이 나오는 건 프로그래밍의 기본 전제를 깨는 일입니다. 사주에서는 그 전제가 성립하지 않았고, 원인이 세 가지였습니다.
진태양시. 한국 표준시는 동경 135도 기준인데 서울은 실제로 약 127도입니다. 30분쯤 차이가 납니다. 시주는 두 시간 단위로 잡히니까 30분이 시주를 통째로 바꿔버립니다. 이걸 보정하는 앱과 안 하는 앱의 결과가 갈렸습니다.
야자시. 밤 11시부터 자정 사이에 태어난 사람의 일주를 당일로 볼 것인가 다음 날로 볼 것인가. 학파마다 견해가 다릅니다. 일주가 바뀌면 십신 분석의 기준점인 일간이 바뀌고, 해석 전체가 흔들립니다.
서머타임. 한국은 1950년대와 60년대에 일광절약시간을 시행한 적이 있습니다. 그 시기에 태어난 사람의 출생 시각을 진짜 태양 시간으로 되돌리려면 역보정이 필요합니다. 어떤 앱은 하고 어떤 앱은 무시합니다.
이 세 가지를 Claude에게 한 번 설명해서 코드를 받는 걸로 끝나지 않았습니다. 다음날 새 세션을 열면 다시 처음부터였습니다. "이 프로젝트는 진태양시 보정을 적용해, 야자시는 당일 처리야, KASI 절기 데이터를 분 단위로 써. 서머타임은 1950~60년대만 보정해." 매번 이걸 다시 썼습니다. 한두 번은 괜찮았습니다. 다섯 번째쯤 되니 진이 빠졌습니다.
절기 데이터도 걸렸습니다. 사주에서 월주는 음력 날짜가 아니라 절기로 정해집니다. 입춘이 지나야 새해고 경칩이 지나야 2월입니다. 절기의 정확한 시각은 천문학적 계산이 필요한데, 한국천문연구원이 분 단위로 공개한다는 걸 알고 나서야 입춘 당일에 태어난 사람의 월주가 제대로 잡혔습니다. 그 전에는 대략적인 날짜로 처리해서 절기 경계 사례가 이상하게 어긋났습니다.
해법은 문서로 갔습니다. docs/ 폴더에 네 개를 만들었습니다. 아키텍처 문서에는 계산 엔진과 해석 레이어를 왜 분리했는지를 적었습니다. 만세력 계산은 결정론적이어야 하고 해석은 LLM이 매번 다르게 써도 되는 영역이라, 둘을 섞으면 같은 사주에 같은 여덟 글자가 안 나옵니다. 도메인 모델 문서에는 천간·지지·오행·십신의 관계와 코드에서의 표기를 넣었습니다. 계산 엔진 문서에는 진태양시·야자시·서머타임·절기의 처리 순서를 적었습니다. 순서가 중요한 게, 서머타임 역보정을 먼저 하고 진태양시를 적용해야 결과가 맞습니다. 반대로 하면 30분이 두 번 어긋납니다. AI 해석 문서에는 프롬프트에 어떤 필드를 넘기고 어떤 톤으로 받을지를 뒀습니다.
그중 도메인 모델 문서가 35페이지가 됐습니다. 사이드 프로젝트치고 과한 분량인 건 압니다. 그게 있어야 새 세션에서 @docs/ 한 번으로 전체 맥락을 아는 상태로 시작할 수 있었습니다.
문서가 없으면 새 세션마다 컨텍스트 세팅에 15분쯤 걸렸습니다. 문서를 로드하면 1~2분이었습니다. 사주 프로젝트는 수십 번의 세션을 거쳤으니 시간만 봐도 차이가 컸는데, 더 큰 건 컨텍스트의 품질 쪽이었습니다. 구두로 설명하면 매번 빠뜨리는 게 생깁니다. 문서를 로드하면 빠뜨릴 게 없습니다.
코드 품질에서도 차이가 났습니다. 십신 분석 함수를 새로 짤 때 AI가 이미 일간 계산 함수의 반환 형태와 오행 매핑 테이블을 알고 있으니 인터페이스가 자연스럽게 맞물렸습니다. 문서 없이 함수 하나씩 따로 요청했을 때는 같은 개념을 다른 이름으로 부르는 일이 잦았습니다. "일간"이 어떤 함수에선 dayStem이고 다른 함수에선 mainStem이 되는 식입니다. 문서 안에 표기를 한 번 박아두니 그런 게 사라졌습니다.
여기가 사주에서 처음 막힌 자리였습니다. 정확히는 막혔다기보다 같은 설명을 반복하다가 비효율이 견딜 수 없이 쌓인 거였고, 그 원인은 AI가 똑똑하지 않아서가 아니라 제 프로젝트의 결정들이 AI 바깥에 있어서였습니다.
타로는 78장이 아니라 156개였습니다
타로는 사주보다 먼저 한 프로젝트입니다.
"타로카드를 보는 웹페이지를 만들고 싶어." 이 한 문장으로 시작했습니다. 기획서도 PRD도 없었습니다. 평소에 관심이 있었고 웹으로 만들면 재밌겠다는 생각이 전부였습니다.
10분 만에 스펙이 잡혔습니다. Claude가 던진 질문 세 개에 답하다 보니 그렇게 됐습니다. 리딩 모드는 원카드, 쓰리카드, 켈틱 크로스 세 가지. 자유 선택 모드는 뺐습니다. 타로에 익숙하지 않은 사람한테 "몇 장 볼까요"를 묻는 건 친절이 아니라 부담입니다. 해석은 LLM API를 매번 호출하는 대신 고정 텍스트로. 스택은 React + Vite + TypeScript. 이름은 "타로 마스터"로 했고 5초 안 걸렸습니다.
여기까지는 사주와 비슷했습니다. 도메인이 짧으면 AI가 빠르게 윤곽을 잡아주고, 의사결정 축을 나열해주면 제 우선순위가 또렷해집니다.
발을 잡은 건 두 가지였습니다.
하나는 작업량이었습니다. "타로는 78장"으로 시작했는데 정방향과 역방향이 별도 해석이라는 걸 도중에 알았습니다. 78장이 아니라 156개의 해석 텍스트가 필요했습니다. 단순한 좋고 나쁨도 아니었습니다. 정방향은 카드의 에너지가 자연스럽게 발현된 상태고, 역방향은 그 에너지가 억눌리거나 과잉되거나 왜곡된 상태입니다. The Magician의 역방향은 "무능력"이 아니라 "속임수, 재능 낭비, 조작"입니다. 능력은 있는데 잘못된 방향으로 쓰이고 있다는 뉘앙스입니다. 이걸 156개 다 써야 했습니다.
다른 하나는 저작권이었습니다. 라이더-웨이트-스미스 덱이 가장 유명한데, 1909년 원본은 퍼블릭 도메인이지만 1971년에 US Games Systems가 재색칠한 버전은 별도 저작권입니다. 인터넷에서 검색하면 나오는 타로 이미지 대부분이 1971년 버전이거나 거기서 파생된 것이었습니다. 색상 톤이 미묘하게 다르고 디테일이 수정돼 있어서 전문가가 아니면 구분이 어렵습니다. 무심코 가져다 썼다가는 문제가 생기는 구조였습니다.
이 두 가지를 AI가 도중에 알려줬다는 게 흥미로웠습니다. "카드 수 → 이미지 소싱 → 저작권 → 원본 대 재색칠"이 한 흐름의 대화 안에서 풀렸습니다. 구글링으로 같은 깊이까지 가려면 탭을 열 개는 열었을 겁니다.
막힌 자리는 다른 데였습니다.
해석 텍스트 156개를 누가 쓸 것인가. 사람이 직접 쓰면 시간이 너무 걸립니다. AI가 매번 생성하면 API 비용이 사용량에 비례해 늘어납니다. 수익이 나기 전에 비용이 나가는 구조는 사이드 프로젝트에서 오래 못 갑니다. 게다가 카드를 뒤집고 결과를 보는 순간의 즉각성이 타로의 핵심인데, API 호출로 2~3초 대기가 생기면 그 몰입이 깨집니다.
결국 미리 작성된 고정 텍스트로 갔습니다. AI가 한 번에 생성한 텍스트를 사람이 다듬어서 정적 데이터로 박는 방식입니다.
다듬는 데서 손이 제일 많이 갔습니다. 한 번에 156개를 받으면 뒤로 갈수록 문장 형태가 서로 닮았습니다. "이 카드는 ~을 의미합니다"가 앞쪽 스무 개에 몰려 있고, 중간부터는 "~한 시기입니다"가 반복되는 식이었습니다. 그래서 22장짜리 메이저 아르카나를 먼저 받아 문장 형태를 정하고, 나머지를 슈트 단위로 나눠 받았습니다. 완드는 완드끼리, 컵은 컵끼리. 그러니 슈트 안에서는 결이 맞고 슈트 사이에서는 달라졌습니다.
이 결정 자체는 옳았다고 봅니다. 흥미로웠던 건 결정에 이르는 과정 쪽입니다.
AI에게 "어떻게 해야 하지?"라고 막연히 물었으면 "둘 다 가능합니다"가 돌아왔을 겁니다. 틀린 답은 아닌데 도움이 안 됩니다. "API 비용", "응답 지연", "몰입감 유지" 같은 제 프로젝트의 제약을 같이 넘겨줘야 비로소 결정이 됐습니다. AI는 선택지를 잘 펼칩니다. 그 선택지 사이에서 어느 쪽이 제 프로젝트에 맞는지는, 그 프로젝트의 결정들이 AI 손에 쥐어져 있어야 답이 나옵니다.
두 번째 막힌 자리가 여기였습니다. 도메인이 단순해서 컨텍스트 붕괴는 안 일어났고, 대신 다른 형태의 정체가 생겼습니다. 의사결정 축은 펼쳐졌는데 그 축 위에서 제가 어디로 가야 하는지는 우선순위를 명시적으로 던지기 전까지 나오지 않았습니다.
사주의 막힘과 표면적으로는 달라 보였습니다. 사주는 같은 설명을 매번 반복하는 비효율이었고 타로는 제 우선순위가 AI에 없는 비효율이었으니까요. 한 발 떨어져 보면 둘 다 같은 자리의 다른 얼굴이었습니다.
미연시 코드는 30분, 감정은 아직 모르겠습니다
세 번째는 좀 의외였습니다.
"아빠가 요즘 취미로 게임을 만들고 있는데, 너도 같이 만들어볼래?"
아이에게 물었더니 눈이 커졌습니다. "진짜? 나도 할 수 있어?" 어떤 장르를 좋아하냐고 다시 물었습니다. 슈팅이나 퍼즐, 아니면 마인크래프트 같은 걸 말할 줄 알았습니다.
잠깐 생각하더니 아이가 말했습니다. "미연시."
당황했습니다. 어디서 접한 건지 친구들 사이에 유행하는 건지 모르겠지만 표정은 꽤 진지했습니다. 그 대답이 이상하게 머리에서 안 떠났습니다. 중학생 여자아이의 취향에 놀란 것도 있지만, 그보다 개발자로서 뭔가가 반응한 것 같습니다.
"미연시 게임을 만들고 싶어. 어디서부터 시작해야 할까?"
돌아온 답에서 Ren'Py라는 이름을 처음 봤습니다. Python 기반 비주얼 노벨 엔진인데, 게임 엔진이라기보다 미연시 제작 도구에 가까웠습니다. 대화, 선택지, 분기, UI가 이미 다 들어 있고 script.rpy 한 파일만 수정하면 동작합니다.
최소 예시가 이거였습니다.
define e = Character("Eileen")
label start:
scene black
e "안녕하세요."
e "첫 미연시 테스트입니다."
menu:
"선택지 A":
jump route_a
"선택지 B":
jump route_b
label route_a:
e "A를 선택했네요."
return
label route_b:
e "B를 선택했네요."
return
이게 전부입니다. label로 장면 만들고 menu로 선택지 만들고 jump로 분기합니다. 10줄이면 미연시 하나가 돌았고 엔진 이해는 30분에 끝났습니다.
여기까지가 너무 빨랐습니다. 사이드 프로젝트로는 이상적인 출발입니다. 코드 진입장벽이 거의 없으니 시나리오에 집중하면 되고, 시나리오야말로 AI와 잘 협업할 수 있는 영역 아닌가 싶었습니다.
들어가 보니 그게 아니었습니다.
"감정 시스템을 어떻게 만들면 좋겠어?"라고 물었습니다. 호감도를 올리고 내리는 단순한 구조를 예상했는데 돌아온 답이 의외였습니다. 감정을 계층으로 나누자고 했습니다. 성향과 순간 감정과 관계 상태를 분리하고, 사랑이라는 감정을 단일 변수로 두지 말고 신뢰와 친밀감과 상실 공포의 조합으로 보자고요.
좀 놀랐습니다. 도메인 표면을 잡는 건 알았는데 감정 모델을 결대로 쪼개주는 건 예상 밖이었습니다. 제가 혼자 설계했으면 호감도 하나로 시작했을 겁니다. 0에서 100까지의 단일 변수, 선택지를 누를 때마다 +5 혹은 -5. 그게 끝이었을 겁니다. 신뢰와 친밀감과 상실 공포를 따로 두자는 제안을 듣고 나니, 같은 사랑이라도 어떤 사랑인지에 따라 다른 결말이 나오는 구조가 보였습니다. 신뢰는 높은데 친밀감이 낮으면 멀게 느껴지는 관계가 되고, 친밀감은 충분한데 상실 공포가 강하면 매번 확인을 요구하는 관계가 됩니다.
실제로 잡을 때는 Ren'Py 변수 세 개로 시작했습니다. trust, intimacy, fear_of_loss. 선택지마다 세 값이 따로 움직이게 했습니다. 약속을 지키는 선택은 신뢰를 올리고 친밀감은 안 건드립니다. 갑자기 거리를 좁히는 선택은 친밀감을 올리는 대신 상실 공포도 같이 올립니다. 엔딩 분기는 세 값의 조합으로 갈리게 두고, 총합이 아니라 어느 값이 높은지를 봤습니다.
돌려보니 단일 호감도였으면 안 나왔을 결말이 몇 개 생겼습니다. 셋 다 높은데 상실 공포만 유독 높은 경우가 그랬습니다. 잘 지내는 것처럼 보이는데 계속 불안한 관계. 그걸 코드로는 조건문 세 줄로 표현할 수 있었습니다.
며칠 뒤에 아이한테 말했습니다.
"미연시 만들어보자. 네가 선택하면 이야기가 달라지는 거."
"내가 고르는 거야?"
"응. 뭘 고르느냐에 따라 이야기가 달라져."
"그러면 나쁜 걸 고르면 어떻게 돼?"
"슬퍼질 수도 있지."
"왜?"
그 질문에 바로 대답하지 못했습니다.
왜 슬퍼질까요. 가상의 캐릭터, 가상의 이야기인데요. 왜 사람은 거기서 진짜 감정을 느낄까요. 아이가 던진 단순한 질문이 사실 이 프로젝트의 핵심이었습니다.
그래서 그대로 AI에게 옮겼습니다. "왜 사람은 가상 관계에서 진짜 감정을 느끼는가?" 돌아온 건 "감정적 몰입이 가능하기 때문", "선택이 의미를 부여하기 때문" 같은 일반론이었습니다. 틀린 말은 아닌데 그 안에 답은 없었습니다.
거기서 멈췄습니다. 코드가 막힌 건 아니었습니다. Ren'Py가 다 해주니까요. 도메인 표면도 막힌 게 아니었습니다. 감정 모델까지는 잘 잡았으니까요. "왜 그런가"에 들어가니 답의 깊이가 갑자기 얕아졌습니다. 표면에서 한 단계 아래로 내려가는 그 순간에 AI가 따라오지 않았습니다.
세 번째 막힌 자리가 여기였습니다. 정보 정리, 구조 설계, 코드 작성. 이런 영역에서 AI는 정말 강합니다. "이 구조가 왜 사람의 감정을 건드리는가"처럼 경험과 기억에서 길어 올려야 하는 질문 앞에서는 답이 갑자기 평평해졌습니다.
처음엔 이게 도구의 한계라고 생각했습니다. 모델이 더 좋아지면 풀릴 문제라고요. 한참 지나서 생각이 조금 바뀌었습니다. 한계가 아니라 질문의 성격이 달랐던 겁니다. 정보로 답할 수 있는 질문과 경험으로 답해야 하는 질문은 다른 종류인데, 그 둘을 같은 도구로 풀려고 하니 한쪽이 안 풀리는 게 당연했습니다.
로봇에서 같은 자리에 네 번째로 멈췄습니다
올봄에 아이한테 물었습니다. "아빠랑 로봇 만들어볼래? 카메라 달고, 센서 달고, AI가 뭘 볼지 판단하는 로봇." 2초 정도 생각하더니 "할게!"였습니다. 그날 저녁에 혼자 유튜브에서 로봇 영상을 찾아보더니, 다음 날 "내가 로봇 몸통 담당할게" 했습니다.
역할이 그렇게 정해졌습니다. 아들은 하드웨어를 맡았습니다. 조립, 결선, 센서 연결, 본체. 저는 소프트웨어였습니다. LLM 서버, 코딩 에이전트, 통신 레이어. 같이 하는 부분은 에이전트를 자연어로 지휘하는 자리였고, 넉 달쯤 이어졌습니다.
LLM은 클라우드 API 대신 집에 있는 Mac에서 로컬로 돌리기로 했습니다. 로봇이 실시간으로 판단할 때마다 인터넷을 거치면 지연이 생기는데, 집 안 LAN에서 처리하면 그게 거의 없습니다. Mac mini M1 16GB, Mac mini M4 24GB, MacBook Pro M4 Pro 24GB. 세 대를 비교하면서 쓰는 구조였습니다. Apple Silicon의 통합 메모리가 로컬 추론에 의외로 잘 맞았습니다.
세 대를 놓고 보니 갈린 건 속도보다 모델 크기였습니다. 16GB에서는 쓸 만한 크기의 모델을 올리면 다른 걸 못 띄웠습니다. 브라우저 탭 몇 개만 열어도 스왑이 시작됐습니다. 24GB에서는 같은 모델을 올리고도 개발 환경이 같이 돌았습니다. 로봇이 판단을 요청하는 간격이 초 단위라 절대 속도는 셋 다 충분했고, 결국 "모델을 올린 채로 다른 일을 할 수 있냐"가 기준이 됐습니다. 다만 이 결론은 저희 용도에 한정된 겁니다. 요청이 몰리는 구조였으면 다르게 갈렸을 것 같습니다.
여기까지가 부드러웠고, 그다음이 글 처음에 쓴 그 새벽입니다.
ENA 핀 10번 Timer1 충돌. 두 시간 디버깅. 한쪽 바퀴만 돌던 장면.
그날 두 시간이 끝나갈 무렵 떠오른 생각이 이거였습니다. AI가 못해서가 아닙니다. 모르는 것도 아닙니다. 다 알 수 있는 정보인데 지금 이 세션에 그 정보가 없을 뿐입니다.
그래서 정보를 들고 다니기로 했습니다. CLAUDE.md라는 파일에요. 처음엔 10줄짜리로 핀맵만 적었습니다. Timer1 충돌 이후에 "라이브러리 선택 이유" 섹션이 붙었습니다. AFMotor를 안 씁니다, Timer1과 Timer2를 점유하기 때문입니다. NewPing을 씁니다, 비동기 핑이라 루프를 안 멈춥니다. 이유까지 적은 건 "AFMotor 안 씀"만 적어두면 다음에 다시 쓰는 코드가 나오기 때문입니다. 이유가 붙어야 그 제약을 지킵니다.
ENA는 10번에서 5번(Timer0)으로, ENB는 13번에서 6번(Timer0)으로 옮겼습니다. 13번은 애초에 PWM이 안 되는 핀인데 처음엔 그것도 모르고 박혀 있었습니다. 두 줄 바꾸고 나니 모터가 멀쩡하게 돌았습니다.
CLAUDE.md는 그 뒤로 계속 자랐습니다. ROS2 토픽 명명 규칙을 처음 요청할 때 그 섹션이 생겼습니다. 동작 제약이 들어갔습니다. HC-SR04 측정값 20cm 이하면 전진 명령 무시, 15cm 이하면 정지 후 후진. 코딩 규칙도 붙었습니다. Timer1 사용 금지, loop() 안에 delay() 금지, 시리얼 디버그는 9600 baud로 통일. 한 번 당할 때마다 한 줄씩 늘어서 지금은 120줄쯤입니다.
어떤 부분은 제가 안 짜고 Claude가 직접 넣었습니다. 처음 하네스 구성을 부탁하면서 NewPing 레포 URL과 Adafruit Motor Shield 레포 URL을 같이 넘겼는데, Claude가 두 레포의 코드를 읽고 NewPing이 비동기 핑이라는 이유를 스스로 적었습니다. Adafruit 코드에서 타이머 점유 방식을 보고 "AFMotor Timer 충돌 주의"를 제약으로 넣었습니다. 제가 데이터시트를 읽고 정리한 게 아니라 Claude가 코드를 읽고 판단한 겁니다. 그게 좀 신선했습니다.
진짜 보상은 며칠 뒤에 왔습니다. 아들이 Claude Code 창에 처음으로 직접 타이핑한 게 이거였습니다.
"초음파 센서 값 20cm 이하면 멈추게 해줘"
문법이 틀려도 되고 설명이 부족해도 된다, 네 말로 그냥 써라. 그게 규칙이었습니다. 그 한 줄로 Claude Code가 코드를 짰습니다. CLAUDE.md에 HC-SR04 핀맵이 있으니 Trig와 Echo를 알아서 맞게 썼고, 동작 제약에 20cm 기준이 있으니 로직도 그걸 따랐습니다.
코드를 저장하고 제가 옆에서 Arduino IDE 업로드를 도왔습니다. 로봇 앞에 손을 뻗었습니다. 20cm 지점에서 정확히 멈췄습니다.
"된다!"
12살이 자연어 한 줄로 로봇을 멈춘 순간이었습니다. 아들이 핀맵을 외울 필요가 없었습니다. CLAUDE.md에 있었으니까요. 라이브러리가 왜 그건지 알 필요도 없었습니다. 맥락은 하네스에 있었으니까요. 아이는 원하는 동작만 자기 말로 설명했습니다.
네 번째 자리가 여기였습니다. 사주에서는 매번 설명을 반복하다 지친 자리, 타로에서는 제 우선순위를 명시하기 전엔 결정이 안 되던 자리, 미연시에서는 표면 한 단계 아래로 못 내려가던 자리, 로봇에서는 제가 안다고 가정한 제약을 AI가 모른 채로 자신 있게 코드를 짰던 자리.
네 번 다 모양은 달랐는데 발이 멈춘 위치는 비슷했습니다.
처음엔 이 네 가지가 다 별개라고 생각했습니다. 사주는 도메인이 복잡해서, 타로는 의사결정 마비라서, 미연시는 감정이 원래 어려워서, 로봇은 임베디드가 특수해서. 각각 다른 이유로 정리해뒀습니다.
네 개를 옆에 놓고 보니 패턴이 보였습니다.
가장 먼저 눈에 들어온 건 새 세션마다 도메인 지식을 처음부터 다시 설명해야 했다는 점입니다. 사주에서는 진태양시, 야자시, 서머타임. 타로에서는 1909년 원본만 써야 한다는 사실. 미연시에서는 감정 계층 모델. 로봇에서는 핀맵과 라이브러리 선택 이유. 다 처음 한 번은 AI 도움으로 잡혔는데, 그 결정이 어디에도 안 적혀 있으면 새 세션에서 다시 0부터였습니다.
해결책도 네 번 다 같은 모양이었습니다. 바깥에 적기. 사주의 35페이지짜리 도메인 모델 문서, 로봇의 120줄짜리 CLAUDE.md, 타로의 고정 텍스트 데이터. 다 같은 방향입니다. AI가 매번 안에서 기억하게 두지 말고 바깥에 적어두고 들고 들어가게 하기.
비슷한 결로 하나 더 있었습니다. AI가 모르는 제약을 자신 있게 코드로 옮겼다는 점입니다. ENA 10번 Timer1 충돌이 가장 명확한 사례입니다. 코드만 보면 멀쩡한데 제 보드에 서보가 있다는 걸 몰랐습니다. 사주도 비슷했습니다. 처음 받은 만세력 계산 코드는 표준적인 음양력 변환이었고, 학파별 야자시 처리 같은 제약은 빠져 있었습니다. 알려주면 들어갔고 안 알려주면 빠진 게 있는지조차 모르는 코드가 나왔습니다.
AI가 못 짠 게 아니라 정보가 없는 상태에서 짠 겁니다. 문제는 AI가 정보 없다는 사실을 말해주지 않는다는 데 있습니다. 모르는 채로 짠 코드가 모르는 채로 나옵니다. 사람이 모르는 채로 짠 코드보다 이게 더 까다로운 이유는, AI 코드가 평균적으로 깔끔해 보이기 때문입니다. 깔끔한데 틀린 코드는 처음에 의심이 안 듭니다. 그러다 한쪽 바퀴만 도는 새벽 한 시를 만납니다.
마지막 하나는 표면 아래로 한 단계 들어가면 답이 평평해진다는 점입니다. 사주의 도메인 지형도는 10분에 잡혔지만 "이 학파를 따를 때 그 제약이 어떻게 바뀌는가"는 사람이 정리해야 했습니다. 타로의 카드 구조는 한 답변에 정리됐지만 "내 프로젝트의 비용과 지연을 고려할 때 어느 쪽이 맞는가"는 제가 우선순위를 던져야 했습니다. 미연시의 감정 모델은 계층까지 들어갔지만 "왜 사람이 가상에서 진짜 감정을 느끼는가"는 일반론에서 멈췄습니다. 로봇의 코드 패턴은 정상이었지만 "내 보드의 다른 라이브러리와 어떻게 충돌하는가"는 제가 짚어야 했습니다.
표면에서 한 단계 더 들어가는 그 자리. 거기서 네 번 다 발이 멈췄습니다.
좀 더 풀어보면 이렇습니다. 표면은 도메인의 전체 윤곽, 익숙한 구조, 자주 나오는 패턴입니다. AI는 학습 데이터에서 이걸 수없이 봤으니 빠르게 잡아줍니다. 그 아래는 항상 두 종류로 갈립니다. 하나는 누가 봐도 알 수 있는데 제가 안 적어준 정보입니다. 핀맵, 라이브러리 충돌, 학파별 차이, 우선순위 같은 것. 이건 바깥에 적으면 풀립니다. 다른 하나는 정보가 아니라 경험에서만 길어 올릴 수 있는 답입니다. "왜 사람이 가상에서 진짜 감정을 느끼는가" 같은 질문. 이건 적어서 풀리지 않습니다. 적을 정보 자체가 제 안에도 없으니까요. 이 두 종류가 같은 자리에서 같은 모양으로 막혔다는 게, 처음엔 한 가지로 보였다가 한참 지나서야 갈라 보이기 시작했습니다.
패턴이 보이고 나서 한동안 멍하니 앉아 있었습니다. 이게 도구의 결함은 아닙니다. 이 자리가 원래 그런 자리입니다. 사람과 도구가 만나는 자리. 도구는 일반적인 것을 안고, 사람은 자기 프로젝트의 구체적인 것을 안고. 그 둘이 한 자리에 모이지 않으면 항상 같은 곳에서 발이 빠집니다.
도구가 아무리 좋아져도 이 자리는 안 사라질 것 같습니다. 모델이 커지면 표면을 더 빨리 잡고 더 정교한 코드를 짜겠지요. npx 한 줄로 깔리는 멀티 에이전트 플랫폼의 거버넌스 구조를 뜯어본 적이 있는데 거기서 미리 준비돼 있는 것도 일반적인 골격까지였습니다. 제 보드에 어떤 라이브러리가 올라가 있는지, 제가 어떤 학파를 따르기로 했는지, 제 사용자가 카드 뒤집기에서 느끼는 즉각성을 얼마나 중요하게 보는지. 이런 건 모델이 알 길이 없습니다. 알려줘야 알 수 있고, 알려주려면 제가 그걸 어디엔가 적어둬야 합니다.
발이 멈춘 그 자리는 막힌 자리가 아니라 만나는 자리였습니다. 도구가 끝나고 제 프로젝트가 시작되는 자리요.
그 자리를 발견하기 전과 후가 좀 달라졌습니다. 발견 전에는 두 시간을 디버깅하다 화가 났습니다. 왜 이렇게 깔끔해 보이는 코드가 안 도느냐고요. 발견 후에는 같은 두 시간이 와도 화가 덜 났습니다. 어디가 막힌 건지 모양이 보이니까요. "아, 이건 내가 컨텍스트를 안 줘서 그렇구나" 하는 진단이 먼저 오면 그 뒤의 작업이 디버깅이 아니라 컨텍스트 정리가 됩니다. 같은 시간이 들어도 머릿속의 무게가 다릅니다.
네 번 막힌 다음부터는 매번 다시 시작하지 않기로 했습니다. 사주의 35페이지 도메인 모델, 타로의 정적 데이터 정리, 로봇의 120줄짜리 CLAUDE.md. 미연시는 아직 진행 중인데 감정 계층 모델을 별도 문서로 빼는 중입니다. 네 번 다 모양은 달랐지만 결국 같은 일이었습니다. 제 프로젝트의 결정을 AI 바깥에 적어두는 일이요.
같은 일을 매 프로젝트마다 손으로 만들고 있다는 것도 그러다 알게 됐습니다. 핀맵 표를 짜고, 라이브러리 선택 이유를 적고, 동작 제약을 정리하고, 코딩 규칙을 추가하고. 어느 순간 이게 비슷한 모양으로 반복되고 있었습니다. 그래서 그 패턴을 따로 정리하는 중입니다. 다음 프로젝트에서 같은 구조를 처음부터 다시 만들지 않게요. 60일에 60만 줄을 혼자 짰다는 YC 대표의 작업 방식을 뜯어봤을 때도 눈에 남은 건 줄 수보다 23개짜리 슬래시 커맨드 묶음 쪽이었습니다. 옮길 만한 게 있다면 그 묶음입니다.
미연시에서 만난 자리는 바깥에 적어서 풀리지 않는다는 것도 그냥 받아들이기로 했습니다. "왜 사람은 가상에서 진짜 감정을 느끼는가"는 제가 적어 넣을 답이 없는 자리입니다. 도구를 바꿔서 풀 게 아니라 옛날에 했던 게임들을 다시 떠올리거나, 아이가 던진 "왜?"에 시간을 두고 답을 찾는 식으로 풀 일입니다. 이런 자리가 다른 자리라는 걸 인정하는 것만으로도 시간이 덜 듭니다.
그날 새벽의 두 시간을 두 시간보다 짧게 줄일 수 있다고는 아직 말 못 하겠습니다. 다만 다음에 같은 자리가 오면 두 시간이 어디로 갈지는 조금 알 것 같습니다.
댓글
댓글 쓰기