Claude Code로 앱을 다시 짓다 — 1인개발자 에이전트 주도 개발 3원칙
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
- 1편 떠오른 생각을 Notion에 박제하고 싶었다 발행
- 2편 다 만들고도 제출조차 못 한 첫 버전 발행
- 3편 준비 중독, 그리고 마케팅 공포 발행
- 4편 그래서 나는 또 도망쳤다 — v2 전면 재작성 발행
- 5편 Claude Code로 앱을 다시 짓다 현재 편
- 6편 앱스토어 2회 반려가 진짜로 말한 것 발행
- 7편 너무 느려서 내가 안 썼다 — 온디바이스 STT 피벗 발행
- 8편 매출 $0에서 다시 정한 우선순위 발행
4편을 쓰면서 "왜 다시 짰나"를 정리했습니다. 그 이유는 단순했습니다. v1이 제출도 못 한 채 잠든 사이, 저는 같은 손으로 v2를 다시 쌓기 시작했고, 이번에는 혼자가 아니었습니다. Claude Code 에이전트를 처음으로 진짜 동료처럼 쓴 것이 v2 재작성이었습니다. 이 글은 "어떻게 재작성했나"입니다. 구체적으로 무엇을 에이전트에게 맡겼고, 어떤 방식으로 지시했고, 혼자 할 때와 무엇이 달랐는지를 사건 순서대로 적겠습니다.
당시 저는 Claude Code를 터미널에서 직접 구동하는 방식으로 작업했습니다. 에디터 플러그인이 아니라 CLI입니다. 프로젝트 루트에서 claude를 실행하면 에이전트가 코드베이스 전체를 컨텍스트로 가져갑니다. 저는 거기에 작업 지시를 텍스트로 넣었습니다. 지시가 명확할수록 결과가 빨랐고, 지시가 뭉뚱그려질수록 에이전트는 예상치 못한 방향으로 달려나갔습니다. 그 차이를 인식하는 데 초반 며칠이 걸렸습니다.
이 글은 자책이 아니라 도구 이야기입니다. 에이전트 주도 개발이 1인개발자에게 어떤 조건에서 진짜 곱셈기가 되는지, 그리고 어떤 조건에서 빠른 속도로 틀린 방향을 향하는지를 있는 그대로 적겠습니다.
무엇을 에이전트와 함께 짰나
말로만 "AI로 개발했다"는 흔합니다. 실제로 어떤 기능을 에이전트와 함께 만들었는지가 중요합니다. v2 재작성에서 Claude Code가 투입된 작업은 크게 다섯 가지였습니다. 각각 단독으로도 며칠짜리 작업이었고, 저 혼자였다면 순서가 달랐을 겁니다.
첫 번째는 커스텀 노트 타입 시스템이었습니다. JustBlurt의 핵심 아이디어는 음성을 단순히 텍스트로 변환하는 것이 아니라, 사용자가 미리 정의한 "노트 타입"에 맞게 AI가 구조화해 주는 것입니다. 예를 들어 "업무 메모" 타입을 정의하면 AI가 발화 내용에서 action item과 context를 뽑아 지정된 Notion 템플릿에 맞게 채웁니다. 이 시스템을 v1에서는 하드코딩으로 처리했는데, v2에서는 사용자가 직접 타입을 만들고 수정할 수 있도록 전체를 다시 설계했습니다. 저는 이 작업을 22개의 세부 단위로 쪼갰고, 신규 테스트 18개가 붙었습니다. 에이전트는 각 단위를 받아 구현하고 테스트를 작성했습니다.
두 번째는 DB 마이그레이션이었습니다. Supabase 백엔드에 커스텀 노트 타입을 지원하려면 스키마를 새로 잡아야 했습니다. 테이블 3개 신규 생성, RLS(Row Level Security) 정책 4개, CHECK 제약 조건, 트리거를 한 마이그레이션 파일에 담았습니다. 특히 slug 생성 로직에서 한국어 입력이 들어올 때 충돌이 발생하는 케이스를 별도로 처리해야 했습니다. 에이전트는 slug 충돌 처리를 한국어 문자열 fallback 방식으로 구현했고, 저는 실제 테스트 케이스를 직접 돌려 확인했습니다.
세 번째는 AI 파이프라인 리팩토였습니다. 음성 녹음이 Supabase Edge Function을 통해 처리되는 구조인데, 이 Edge Function이 v1에서는 단일 로직으로 되어 있었습니다. 노트 타입이 추가되면서 분류, 정제, 구조화, 존댓말 처리, 코드스위칭(한국어/영어 혼용) 각 단계를 노트 타입에 따라 다르게 가져가야 했습니다. Edge Function은 1,048줄에서 1,318줄로 늘었습니다. 단순히 코드가 길어진 것이 아니라 노트 타입 설정값을 읽어 프롬프트를 동적으로 조립하는 구조로 바뀌었습니다. 이 변화가 나중에 7편에서 음성 처리 방식을 통째로 교체할 때 결정적인 역할을 했습니다.
네 번째는 iOS 위젯과 딥링크였습니다. 홈 화면 위젯에서 원탭으로 녹음을 시작하고, 딥링크를 통해 앱이 열리면 즉시 녹음 모드로 진입하는 기능입니다. React Native(Expo) 프로젝트에 iOS 네이티브 위젯을 붙이려면 config plugin을 작성해야 합니다. Expo의 managed workflow에서 네이티브 코드를 직접 건드리는 구간인데, 에이전트가 config plugin 구조를 잡고 Swift 위젯 코드를 작성했습니다. 딥링크는 URL scheme 등록부터 앱 내 navigation 처리까지 포함됩니다.
다섯 번째는 인앱 구조화 렌더링이었습니다. 노트 타입마다 결과물의 형태가 다르므로, 타입별로 전용 렌더러 컴포넌트가 필요했습니다. 이 부분은 TDD(테스트 주도 개발) 방식으로 진행했습니다. 먼저 각 렌더러가 받아야 할 입력과 출력을 테스트로 정의하고, 에이전트가 그 테스트를 통과하는 컴포넌트를 짰습니다.
원칙 1 — 작업을 잘게 쪼개서 던진다
에이전트에게 "노트 타입 시스템 만들어줘"라고 통째로 던지면 어떻게 될까요. 저는 초반에 그렇게 했다가 결과물을 거의 쓰지 못했습니다. 에이전트가 만들어온 결과는 방향은 맞는데 구체적인 선택들이 제가 생각한 것과 달랐습니다. 데이터 모델이 달랐고, 컴포넌트 구조가 달랐고, 타입 정의 방식이 달랐습니다. 결국 그것을 고치는 데 처음부터 짜는 것보다 더 많은 시간이 걸렸습니다.
커스텀 노트 타입 시스템을 22개 단위로 쪼갰을 때 상황이 달라졌습니다. 22개는 어떻게 나온 숫자냐 하면, 제가 화이트보드에 이 시스템에 필요한 것들을 나열하다 보니 자연스럽게 그렇게 됐습니다. 데이터 모델 정의, Supabase 테이블 생성, CRUD API 엔드포인트, 프론트엔드 리스트 화면, 프론트엔드 상세 편집 화면, 타입 선택 UI, AI 프롬프트 조립 로직, 테스트 케이스... 이런 식으로 항목이 쌓였습니다. 각 항목은 독립적으로 구현 가능하고, 구현 완료 여부를 확인할 수 있는 단위였습니다.
에이전트에게 한 번에 던지는 단위가 작을수록 결과물의 정확도가 높아집니다. 이것은 제가 직접 경험으로 확인한 사실입니다. 큰 덩어리를 주면 에이전트는 스스로 세부 결정을 내려야 하고, 그 결정들이 개발자가 의도한 것과 어긋날 확률이 높습니다. 반면 "Supabase에 note_types 테이블을 만들고, user_id·name·prompt_template·output_schema 컬럼을 가지며, RLS 정책은 본인 행만 읽기·수정·삭제 가능하게 설정해줘"라고 지시하면 에이전트는 정확히 그것을 합니다.
작업을 쪼개는 것은 사람과의 협업에서도 기본입니다. 범위가 명확한 티켓을 주면 팀원이 질문 없이 진행합니다. 범위가 흐린 티켓을 주면 중간에 계속 물어보게 됩니다. 에이전트는 중간에 물어보는 대신 스스로 결정하기 때문에, 사람보다 더 엄격하게 범위를 명확히 해야 합니다. 에이전트는 맥락을 추론하는 능력이 뛰어나지만, 그 추론이 개발자의 의도와 일치한다는 보장은 없습니다.
22개 단위로 쪼갠 후 제가 한 일은 그것을 의존성 순서대로 정렬하는 것이었습니다. 데이터 모델이 먼저, API 엔드포인트가 그다음, UI 컴포넌트가 그다음. 에이전트에게 의존성 역순으로 던지면 존재하지 않는 함수를 호출하는 코드가 생겨나고, 그것을 고치는 데 다시 한 번 에이전트를 투입해야 합니다. 순서를 지키는 것은 에이전트의 결과물이 즉시 동작하도록 만드는 가장 단순한 방법이었습니다.
원칙 2 — 테스트를 안전벨트로 쓴다
v2 재작성을 시작할 때 v1에서 넘어온 Jest 테스트가 몇 개 있었습니다. 정확히는 적지 않겠지만, v2가 끝날 무렵에는 144개가 됐습니다. 이 숫자는 목표로 정한 게 아니라, 각 작업 단위마다 테스트를 붙이다 보니 자연스럽게 쌓인 것입니다. 저는 재작성 내내 "이 작업이 끝났다"는 기준을 테스트 통과로 삼았습니다. 144개가 전부 통과하면 다음 작업 단위로 넘어갔습니다.
왜 이것이 에이전트 주도 개발에서 특히 중요하냐면, 에이전트가 만든 코드는 "그럴듯한데 틀린" 경우가 있기 때문입니다. 컴파일은 되고, 타입 체크도 통과하고, 화면에도 표시되는데, 엣지 케이스에서 잘못 동작하는 코드입니다. 이런 코드를 사람이 매번 읽고 검증하는 것은 불가능합니다. 작업 단위가 22개라면 어느 정도 가능하겠지만, 프로젝트가 커질수록 한계가 생깁니다. 테스트가 충분하면 에이전트가 만든 변경을 신뢰하고 넘어갈 수 있습니다. 테스트가 통과하지 않으면 에이전트에게 "테스트가 통과하도록 수정해줘"라고 다시 던지면 됩니다.
구체적인 예를 들겠습니다. 커스텀 노트 타입에서 AI가 음성을 구조화할 때, 사용자가 정의한 output_schema에 맞게 결과를 만들어야 합니다. 에이전트가 이 로직을 처음 구현했을 때, 정상 케이스는 잘 동작했지만 output_schema에 선택적 필드가 섞인 경우에 undefined가 누락되는 버그가 있었습니다. 이것은 테스트를 돌리기 전까지는 발견하기 어려운 문제입니다. 테스트에서 잡혔고, 에이전트에게 실패한 테스트 케이스와 함께 다시 넘겼더니 두 번째 구현에서 수정됐습니다.
TDD를 AI 에이전트와 함께 쓰는 방식은 전통적인 TDD와 약간 다릅니다. 전통 TDD는 개발자가 먼저 실패하는 테스트를 작성하고, 그것을 통과하는 구현을 씁니다. AI 에이전트와 함께 할 때는, 제가 "이런 입력이 들어오면 이런 출력이 나와야 한다"는 케이스를 먼저 설명하고, 에이전트에게 테스트와 구현을 함께 작성하게 할 수도 있습니다. 또는 제가 테스트를 먼저 쓰고 에이전트에게 구현만 맡길 수도 있습니다. 어떤 방식이든 핵심은 하나입니다. 작업 완료의 기준이 코드 작성이 아니라 테스트 통과여야 한다는 것입니다.
에이전트를 빠르게 쓰려면 역설적으로 테스트가 더 두꺼워야 합니다. 테스트가 얇으면 에이전트 결과물을 받을 때마다 사람이 직접 검증해야 합니다. 그렇게 되면 에이전트를 쓰는 속도 이점이 사라집니다. 테스트가 두꺼우면 에이전트가 만든 변경이 기존 동작을 깨뜨리지 않는다는 것을 자동으로 확인할 수 있습니다. 이것이 에이전트 주도 개발에서 테스트가 안전벨트 역할을 한다는 뜻입니다. 속도를 낼수록 안전벨트가 더 중요해집니다.
마지막으로, 테스트 커버리지만이 전부는 아닙니다. 저는 Jest 단위 테스트 외에도 Supabase 로컬 환경에서 마이그레이션 실행, Expo 에뮬레이터에서 실제 빌드 확인, 실기기(iPhone)에서 위젯 동작 확인을 별도로 거쳤습니다. 테스트가 통과했다고 실기기에서 동작한다는 보장은 없습니다. 특히 iOS 위젯처럼 네이티브 레이어가 개입하는 기능은 반드시 실기기 확인이 필요했습니다. 테스트는 코드 로직 레벨의 안전벨트이고, 실기기 확인은 그 위의 통합 검증입니다.
원칙 3 — 기존 자산을 재사용하게 설계한다
AI 파이프라인을 다시 짤 때 가장 중요하게 생각한 것이 재사용 가능한 구조입니다. 음성 처리의 핵심 로직 — 분류, 정제, 구조화, 존댓말 처리, 코드스위칭 — 은 새로 짜지 않고, v1에서 쌓인 로직을 그대로 가져오되 입력 인터페이스만 통일하는 방식으로 접근했습니다. 결과적으로 Edge Function 내부는 입력 경로와 프롬프트 조립 레이어만 바뀌었고, 핵심 처리 로직은 그대로 재사용됐습니다.
이 설계 결정이 얼마나 중요했는지는 7편에서 드러납니다. v2를 완성하고 앱스토어 심사를 거치는 과정에서, STT(음성인식) 방식을 클라우드 API에서 온디바이스 처리로 바꿔야 하는 상황이 생겼습니다. 온디바이스 STT를 붙인다는 것은 입력 경로 전체를 교체하는 작업입니다. 만약 처리 로직이 입력 방식과 강하게 결합되어 있었다면, STT 교체가 파이프라인 전체를 다시 짜는 작업이 됐을 겁니다. 입력 인터페이스를 통일해 두었기 때문에, STT 어댑터만 새로 만들고 나머지는 그대로 연결할 수 있었습니다.
에이전트에게 재사용을 명시적으로 지시하는 것이 필요했습니다. 에이전트는 지시를 받을 때 컨텍스트를 참고하지만, "기존에 이런 로직이 있으니 새로 만들지 말고 그것을 호출하라"고 명시하지 않으면 비슷한 코드를 새로 작성하는 경향이 있습니다. 저는 각 작업 지시에 "기존 processVoiceInput 함수를 재사용하고, 새 함수를 만들 필요가 있을 때는 그 이유를 먼저 알려줘"라는 제약을 붙였습니다. 이 한 줄이 코드베이스 내의 중복을 상당히 줄였습니다.
재사용 가능한 구조를 미리 잡아두는 것도 중요합니다. v2에서 AI 파이프라인을 다시 설계할 때, 저는 처리 단계를 함수 단위로 명확히 분리하고 각 함수의 입출력 타입을 TypeScript 인터페이스로 정의했습니다. 이 정의가 있으면 에이전트가 새 기능을 추가할 때 기존 인터페이스를 따르게 됩니다. 인터페이스 없이 "대략 이런 형태로 만들어줘"라고 하면 에이전트마다 결과가 달라지고, 나중에 합쳐야 할 때 타입 충돌이 납니다.
에이전트에게 코드베이스의 설계 의도를 알려주는 방법이 몇 가지 있습니다. 하나는 주석입니다. 핵심 함수나 모듈에 "이 함수는 STT 방식에 무관하게 동작해야 한다"처럼 의도를 적어두면, 에이전트가 그것을 컨텍스트로 참고합니다. 다른 하나는 지시문에 직접 포함하는 것입니다. 저는 중요한 설계 원칙은 매번 지시에 포함시켰습니다. 에이전트가 컨텍스트에서 읽어낼 수 있다고 기대하기보다는, 명시적으로 넣는 것이 결과물의 일관성을 높였습니다.
재사용 설계의 또 다른 이점은 에이전트 작업 간의 충돌을 줄인다는 것입니다. 여러 작업 단위를 순차적으로 진행할 때, 앞 작업에서 만든 코드가 뒤 작업에서도 쓰일 수 있어야 합니다. 인터페이스와 재사용 구조가 명확하면 앞 작업 결과를 기반으로 뒤 작업을 진행할 수 있습니다. 반면 각 작업에서 에이전트가 제각기 다른 유틸리티 함수를 만들면, 나중에 전체를 합칠 때 동일한 일을 하는 함수가 여러 개 공존하게 됩니다.
함정도 정직하게
에이전트 주도 개발이 좋기만 한 경험이었다면 이 편은 훨씬 짧았을 겁니다. 실제로는 발이 걸리는 지점이 분명히 있었습니다. 나중에 비슷한 상황에 처한 분들이 시행착오를 줄이도록, 제가 겪은 함정을 그대로 적겠습니다.
첫 번째는 의존성 버전 불일치였습니다. React Native Expo 프로젝트는 SDK 버전과 네이티브 모듈 버전이 엄격하게 맞아야 합니다. 에이전트는 설치 명령을 제안할 때 자연스럽게 최신 버전을 씁니다. 당시 프로젝트의 Expo SDK 버전과 에이전트가 제안한 react-native-widget-extension의 버전이 맞지 않았습니다. 빌드가 통째로 깨지는 에러였고, 에러 메시지만으로는 원인이 버전 불일치라는 것을 바로 알기 어려웠습니다. 버전 불일치 관련 구체적인 사례는 7편에서 자세히 다룹니다. 에이전트가 패키지 설치를 제안할 때는 항상 현재 SDK와의 호환성을 따로 확인하는 습관이 필요합니다.
두 번째는 공유 파일 충돌이었습니다. 저는 에이전트를 하나씩 순차적으로 쓰지 않고, 구현이 끝난 작업 단위의 결과를 다음 지시에 포함시키며 진행했습니다. 이 과정에서 한 작업 결과가 반영되기 전에 다음 작업 지시를 넣으면, 에이전트는 이전 상태의 파일을 기준으로 변경을 만들어냅니다. 이 변경을 적용하면 앞서 수정한 내용이 사라집니다. "File modified since read" 에러가 이 상황을 알려줍니다. 저는 이 에러를 처음 만났을 때 재시도했다가 두 번째로 날렸습니다. 이후로는 한 작업이 완전히 반영된 것을 확인한 뒤에 다음 지시를 넣었습니다. 한 번에 하나씩, 순서대로 진행하는 것이 가장 안전했습니다.
세 번째는 "동작한다"와 "검증됐다"의 차이였습니다. 에이전트가 만든 코드가 Jest 테스트를 통과하고 TypeScript 컴파일도 됩니다. 그런데 iPhone에서 실제로 빌드하면 동작하지 않는 경우가 있었습니다. 특히 iOS 위젯은 App Extension 빌드 구조가 본 앱 빌드와 다릅니다. config plugin이 생성하는 Xcode 설정값 중 하나가 잘못됐을 때, 에뮬레이터에서는 넘어가고 실기기에서 크래시가 났습니다. 이런 종류의 문제는 테스트로 잡히지 않습니다. 에이전트 작업이 끝났다고 해서 실기기 확인을 건너뛰는 것은 위험합니다. 코드 작업 완료와 기능 검증 완료는 다른 단계입니다.
네 번째는 컨텍스트 증발이었습니다. 에이전트와 긴 작업을 진행하다 보면, 초반에 합의한 구조적 결정들이 후반 지시에서 반영되지 않는 경우가 있었습니다. 에이전트의 컨텍스트 창이 긴 대화 중에 초반 내용을 밀어내는 현상입니다. 저는 이것을 대응하기 위해 중요한 구조 결정들을 별도의 메모 파일로 정리해 두고, 새 작업 지시를 넣을 때마다 해당 내용을 함께 붙였습니다. 또는 특정 시점에서 대화를 새로 시작하고 그때까지의 구조 결정을 요약해서 먼저 넣었습니다. 에이전트의 기억을 믿기보다는 중요한 맥락을 개발자가 직접 관리하는 것이 안전합니다.
다섯 번째는 에이전트 과신이었습니다. 초반에 여러 번 좋은 결과가 나오면, 점점 검증을 소홀히 하게 됩니다. "지난번에도 잘 됐으니까"라는 생각으로 테스트 실행을 건너뛰거나, 코드를 훑어보지 않고 바로 다음 작업으로 넘어가기 시작합니다. 그러다 어느 시점에서 테스트 전체를 돌려보면, 몇 작업 전부터 누적된 버그가 한꺼번에 터집니다. 어느 작업에서 문제가 생겼는지 역추적하는 것이 처음부터 검증했을 때보다 훨씬 많은 시간을 씁니다. 에이전트의 결과물은 결과물이 나올 때마다 확인해야 합니다. 신뢰는 쌓아가는 것이지만, 검증을 생략하는 근거는 되지 않습니다.
정리
v2 재작성을 마쳤을 때 앱의 코드베이스는 v1과 완전히 달랐습니다. 커스텀 노트 타입, 동적 프롬프트 조립, iOS 위젯, 딥링크가 모두 들어갔고 Jest 144개가 통과했습니다. 저 혼자였다면 이 규모의 재작성에 얼마나 걸렸을지 알 수 없습니다. 에이전트와 함께 했을 때의 속도가 어느 정도였는지 직접 비교할 방법이 없기 때문입니다. 다만 제가 확인한 것은, 세 가지 조건이 갖춰졌을 때 에이전트가 동료처럼 작동했다는 것입니다.
작업을 잘게 쪼개는 것, 테스트를 안전벨트로 쓰는 것, 기존 자산을 재사용하게 설계하는 것. 이 세 가지는 독립적인 원칙이 아닙니다. 작업을 잘게 쪼개야 각 작업에 대한 테스트를 쓰기 쉬워지고, 테스트가 두꺼워야 재사용 구조를 바꿀 때 안전하게 진행할 수 있습니다. 재사용 구조가 잡혀 있어야 새 작업 단위를 작게 정의할 수 있습니다. 셋은 서로 받쳐주는 구조입니다.
함정도 다섯 가지를 적었습니다. 의존성 버전 불일치, 공유 파일 충돌, 동작과 검증의 차이, 컨텍스트 증발, 에이전트 과신. 이것들은 에이전트가 나쁜 도구여서 생기는 문제가 아닙니다. 에이전트를 처음 쓰는 사람이 자연스럽게 밟게 되는 지점들입니다. 저도 전부 밟았습니다. 다음에 비슷한 재작성을 한다면 이 지점들을 미리 알고 시작할 수 있을 것입니다.
에이전트는 1인개발자에게 곱셈기가 맞습니다. 그런데 곱하는 대상이 명확해야 합니다. 명확하게 쪼개진 작업, 두꺼운 테스트, 재사용 가능한 구조 — 이 셋이 받쳐줄 때 에이전트는 동료가 됩니다. 셋 중 하나라도 없으면 에이전트는 빠른 속도로 잘못된 방향을 향합니다. 혼자 할 때보다 더 빠르게 틀린 곳으로 갑니다.
다음 편은 다시 아픈 쪽으로 돌아갑니다. 이 잘 만든 앱이 앱스토어에서 두 번 반려당했습니다. 첫 번째 반려를 받았을 때 저는 코드 문제라고 생각했습니다. 두 번째 반려를 받고 나서는 생각이 달라졌습니다. 코드 이야기가 아니었습니다. 그 이야기가 6편입니다.
- 떠오른 생각을 Notion에 박제하고 싶었다 발행
- 다 만들고도 제출조차 못 한 첫 버전 발행
- 준비 중독, 그리고 마케팅 공포 발행
- 그래서 나는 또 도망쳤다 — v2 전면 재작성 발행
- Claude Code로 앱을 다시 짓다 현재 편
- 앱스토어 2회 반려가 진짜로 말한 것 발행
- 너무 느려서 내가 안 썼다 — 온디바이스 STT 피벗 발행
- 매출 $0에서 다시 정한 우선순위 발행
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
댓글
댓글 쓰기