사용자도 없는데 앱을 다시 짰다 — SwiftUI에서 React Native 전면 재작성
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
- 1편 떠오른 생각을 Notion에 박제하고 싶었다 발행
- 2편 다 만들고도 제출조차 못 한 첫 버전 발행
- 3편 준비 중독, 그리고 마케팅 공포 발행
- 4편 그래서 나는 또 도망쳤다 — v2 전면 재작성 현재 편
- 5편 Claude Code로 앱을 다시 짓다 발행
- 6편 앱스토어 2회 반려가 진짜로 말한 것 발행
- 7편 너무 느려서 내가 안 썼다 — 온디바이스 STT 피벗 발행
- 8편 매출 $0에서 다시 정한 우선순위 발행
앱 전면 재작성은 발전일 때도 있고 도망일 때도 있습니다. 3편에서 저는 코딩 뒤에 숨는 제 패턴을 자세히 진단했습니다. 노트 앱에 써둔 자가진단의 결론도 한 줄로 요약되어 있었습니다. "제품은 끝났다. 사람을 데려와라." 몇 주에 걸친 자기관찰 끝에 내린 결론이었고, 스스로도 옳다고 생각했습니다.
그리고 저는 정확히 그 반대로 했습니다. 사람을 데려오는 대신, 앱을 처음부터 다시 짜기로 결정했습니다. 그것도 언어와 플랫폼까지 갈아엎는 전면 재작성이었습니다. SwiftUI로 만든 iOS 네이티브 앱 JustBlurt v1을 버리고, Expo와 React Native 기반의 v2를 새로 시작했습니다.
이 글은 그 결정을 변명하지 않고 기록합니다. 이 시리즈의 다른 편들처럼 실제 겪은 일을 그대로 옮기되, 서비스명과 세부 식별 정보는 바꿔서 씁니다. 도망이라는 사실을 부정하지 않겠습니다. 하지만 도망 안에도 합리적 판단이 섞여 있었고, 그 둘이 어떻게 한 결정 안에서 뒤엉켜 있었는지를 가능한 한 정확하게 적으려 합니다. 재작성이 발전인가 도망인가를 구분하는 기준에 대해 생각해온 것도 함께 정리합니다.
진단과 정반대로 간 결정
기록을 다시 읽으면 부끄러울 정도로 명백합니다. 한쪽에는 "사용자 0, 매출 $0, 이제 사람을 모아라"라는 제 손글씨가 있고, 바로 그다음 행보는 전면 재작성이었습니다. 파일 생성 타임스탬프가 거짓말을 하지 않습니다. 자가진단을 마무리한 날로부터 사흘이 채 지나지 않아 Xcode 프로젝트를 닫고 터미널에 npx create-expo-app을 입력하고 있었습니다.
왜 그게 매력적이었는지는 압니다. 재작성은 100% 코딩입니다. 거절이 없습니다. 새 프로젝트는 깨끗하고, 커밋 그래프는 다시 초록색으로 채워지기 시작하고, "이번엔 제대로 만들겠다"는 서사는 그 자체로 달콤합니다. 블로그 게시물을 쓰거나 소셜미디어에 앱을 홍보하거나 낯선 사람에게 피드백을 부탁하는 일과 달리, 코드 작성은 거절당할 가능성이 없습니다. 화면이 원하는 대로 움직이지 않으면 그건 버그이고, 버그는 고칠 수 있습니다. 사람의 무관심은 고칠 수가 없습니다.
마케팅 공포를 정면으로 마주하는 대신, 저는 가장 익숙한 도피처로 돌아간 것입니다. 3편에서 스스로 "코딩 뒤에 숨는다"고 진단했는데, 그 진단문을 읽고 나서도 다시 코딩 뒤로 숨었습니다. 이 패턴의 무서운 점은 자각과 행동이 완전히 분리될 수 있다는 것입니다. 알고 있다는 것이 멈출 수 있다는 것을 의미하지 않습니다. 저는 자신이 도망치고 있다는 사실을 알면서도 도망쳤습니다.
그 일주일을 돌이켜보면 이상하게 생산적인 기분이 들었습니다. 매일 커밋이 쌓였고, 컴포넌트 구조가 정리됐고, 타입 정의가 완성됐습니다. 무언가를 이루고 있다는 느낌이 있었습니다. 하지만 그 일주일 동안 단 한 명의 잠재 사용자에게도 연락하지 않았고, 데모 영상 한 컷도 찍지 않았고, 피드백을 구한 적도 없었습니다. 생산적 도망은 일반 도망보다 오히려 더 오래 지속됩니다. 멈춰야 할 이유를 스스로 찾기가 어렵기 때문입니다.
그럼에도 저는 재작성을 멈추지 않았습니다. 왜냐하면 그 결정 안에는 진짜 명분도 있었기 때문입니다.
그런데 재작성 자체엔 합리적 이유도 있었다
여기가 이 이야기의 가장 복잡한 지점입니다. 도망에는 늘 그럴듯한 명분이 붙습니다. 그리고 JustBlurt v1에서 v2로의 전환에는 실제로 기술적 근거가 있었습니다. 그것이 재작성의 진짜 이유였는지, 아니면 재작성을 정당화하기 위해 나중에 붙인 이유인지, 솔직히 말하면 저 자신도 100% 확신하지 못합니다. 아마 둘 다였을 것입니다.
v1은 SwiftUI 네이티브로 작성된 iOS 전용 앱이었습니다. Swift 5.9, SwiftUI 5.0, iOS 17+ 타깃. Xcode 15 환경에서 로컬 Whisper STT와 Notion API를 연결하는 구조였습니다. 기능적으로는 동작했습니다. 버튼을 누르면 녹음이 시작됐고, 음성이 텍스트로 전환됐고, Notion 페이지가 생성됐습니다. 문제는 그것이 iOS에서만 동작한다는 점이었습니다. 안드로이드 사용자는 처음부터 타깃이 아니었습니다.
1인개발자가 iOS 전용으로 출발하는 것은 드문 일이 아닙니다. 하지만 제가 잠재 사용자를 떠올릴 때 실제로 주변에서 음성 메모를 자주 쓰는 사람들을 관찰해보면, iOS와 안드로이드가 섞여 있었습니다. 특히 업무 환경에서 안드로이드 비율이 결코 낮지 않았습니다. "iOS 전용"은 처음부터 시장의 절반을 잘라내는 선택이었습니다. v1 개발 당시에도 이 점이 마음에 걸렸지만, Swift를 쓰는 것이 더 자연스러웠기 때문에 미뤘습니다.
크로스플랫폼으로 전환하면 사용자 풀이 이론적으로 두 배가 됩니다. 이건 사실입니다. 그래서 스택 전환을 이렇게 결정했습니다.
- SwiftUI 네이티브 → Expo SDK 54 + React Native 0.81 + React 19 + TypeScript strict
- 백엔드(Supabase) → 그대로 재활용
- Notion API 연동 로직 → 일부 재작성, 일부 이식
- 로컬 STT 모듈 → Whisper.cpp에서 Expo-compatible 방식으로 교체
React Native를 선택한 데는 또 다른 이유가 있었습니다. 저는 웹 프론트엔드를 주로 TypeScript와 React로 다루어왔습니다. Swift는 개인 프로젝트용으로 배운 언어였고, SwiftUI의 선언형 패러다임은 익숙했지만 고급 기능으로 갈수록 학습 비용이 가팔랐습니다. 특히 v1을 만들면서 SwiftUI의 몇 가지 제약을 실감했습니다. 복잡한 커스텀 애니메이션은 UIKit과의 혼용이 불가피했고, 배경 오디오 세션 처리는 생각보다 까다로웠으며, 특정 UI 컴포넌트는 동작이 예측 불가능했습니다.
React Native는 적어도 제가 이미 능숙한 언어와 생태계를 사용합니다. TypeScript, React 컴포넌트 모델, npm 패키지 생태계. 코드 공유율이 iOS와 안드로이드 사이에서 80% 이상으로 유지되고, Expo가 네이티브 모듈 설정의 많은 부분을 추상화해주었습니다. 크로스플랫폼 전환이면서 동시에 제가 더 빠르게 구현할 수 있는 스택으로의 이동이기도 했습니다.
명분은 진짜였습니다. 문제는 타이밍이었습니다. 사용자가 단 한 명이라도 있었다면, 그 사람이 안드로이드를 쓰는지 아닌지가 데이터가 됐을 것입니다. 크로스플랫폼 전환의 우선순위도 실제 피드백으로 결정할 수 있었을 것입니다. 그러나 사용자가 0인 상태에서의 크로스플랫폼 재작성은, 존재하지 않는 수요를 위해 존재하지 않는 사용자를 위한 제품을 다시 만드는 일이었습니다. 명분이 진짜여도 타이밍이 틀리면, 그 행동은 여전히 도망입니다.
SwiftUI의 한계가 실제로 걸림돌이었나
재작성 결정을 내리기 전, 저는 v1의 기술 부채를 정리한 메모를 남겼습니다. 그 메모를 다시 꺼내보면 몇 가지는 실제 문제였고, 몇 가지는 지나치게 부풀려진 불만이었습니다.
실제 걸림돌로 기록해둔 것 중 하나는 배경 오디오 세션 처리였습니다. JustBlurt의 핵심 UX는 화면을 잠근 상태에서도 음성 메모를 녹음할 수 있어야 한다는 것이었습니다. 생각이 떠올랐을 때 폰을 꺼내 잠금을 해제하고 앱을 열고 버튼을 누르는 과정이 너무 길면, 그냥 생각을 흘려보내게 됩니다. 위젯이나 잠금 화면 단축키로 바로 녹음을 시작할 수 있어야 합니다. SwiftUI에서 이것을 구현하는 과정은 AVAudioSession의 카테고리와 옵션 조합을 정확히 맞춰야 했고, 배경 모드 entitlement 설정, 인터럽트 처리(전화 수신, 다른 앱의 오디오 재생), 그리고 iOS 버전별로 동작이 달랐던 .playAndRecord 세션 전환 이슈가 있었습니다. 해결은 됐지만 코드 복잡도가 예상보다 높았습니다.
또 다른 걸림돌은 iOS 잠금 화면 위젯과 Dynamic Island 지원이었습니다. iOS 16부터 지원된 Live Activities와 잠금 화면 위젯은 WidgetKit 프레임워크를 사용합니다. 메인 앱과 익스텐션이 App Group을 통해 데이터를 공유하는 구조였는데, 이 부분의 설정과 디버깅이 생각보다 번거로웠습니다. Xcode Simulator에서는 잘 동작하다가 실기기에서 간헐적으로 상태 동기화가 어긋나는 현상이 있었습니다.
SwiftUI 자체의 한계라기보다는, 네이티브 iOS 개발의 복잡도가 예상보다 높았다고 표현하는 것이 더 정확합니다. SwiftUI는 선언형 프레임워크이고, 간단한 화면 구성에는 뛰어납니다. 그러나 오디오, 백그라운드 처리, 위젯, 딥링크 같은 시스템 수준 기능으로 가면 SwiftUI만으로는 부족하고 UIKit 브릿지가 필요해집니다. 그 브릿지 코드는 대체로 지저분합니다. Swift를 주력으로 쓰는 iOS 전문 개발자에게는 자연스러운 영역이지만, 저처럼 웹 프론트엔드가 주 배경인 1인 개발자에게는 학습 비용이 가파르게 올라가는 구간이었습니다.
그래서 "SwiftUI가 나빴다"는 결론은 틀렸습니다. 정확한 진단은 "내가 iOS 네이티브 개발의 깊은 층에서 효율적으로 움직이기 어려웠다"였습니다. 그 비효율이 개발 속도를 늦추었고, 자꾸 어렵게 느껴지는 시스템 레이어 앞에서 동력이 약해졌습니다. React Native는 그 레이어를 상당 부분 추상화해주었고, Expo는 그 위에 다시 한 겹을 더 덧씌워 주었습니다. 제 생산성 기준에서는 현실적인 선택이었습니다.
다만 React Native가 공짜는 아닙니다. 네이티브 모듈이 필요한 기능은 Expo managed workflow를 벗어나 bare workflow로 내려가야 합니다. 그 지점에서는 다시 Objective-C나 Java 브릿지 코드를 다루어야 하고, 복잡도가 올라갑니다. Expo의 추상화가 편리한 만큼, 그 경계를 넘어서면 디버깅이 더 어려워질 수도 있습니다. 이 트레이드오프를 v2 개발 중에 실제로 경험하게 됩니다. 하지만 그것은 이 편이 아니라 다음 편의 이야기입니다.
도망과 합리를 구분하는 법
재작성을 마친 뒤, 저는 이 결정이 도망이었는지 합리적 선택이었는지를 계속 생각했습니다. 결론은, 그 질문 자체가 잘못 설정됐다는 쪽으로 기울었습니다. 도망과 합리적 선택은 양자택일이 아닙니다. 한 결정 안에 둘 다 존재할 수 있고, JustBlurt의 재작성이 바로 그런 경우였습니다.
제가 지금 쓸모 있다고 생각하는 구분 기준은 다음 질문입니다.
이 작업은 다음 사용자를 데려오는가, 아니면 사용자 없이도 할 수 있는 일인가?
이 질문에 각 행동을 대입해보면 답이 나옵니다. 재작성은 사용자 없이도 할 수 있는 일입니다. 아무도 보지 않아도 코드를 작성할 수 있습니다. 반면 데모 영상을 만들어 올리는 것, 인디 해커 커뮤니티에 "이런 앱 쓸 사람 있나요?" 하고 글을 쓰는 것, 주변 지인 한 명에게 앱을 써봐달라고 부탁하는 것은 거절 가능성이 있습니다. 무관심이라는 피드백을 받을 수 있습니다. 그 가능성을 포함하는 행동이 "사용자를 데려올 수 있는 행동"입니다.
이 기준으로 당시 일주일을 돌아보면 분명해집니다. 저는 사용자 없이도 할 수 있는 일만 했습니다. 코드를 작성했고, 아키텍처를 고민했고, 타입 정의를 다듬었습니다. 이것들은 모두 의미 있는 작업이지만, 어떤 것도 잠재 사용자에게 앱의 존재를 알리지 못했습니다. 거절당할 가능성이 0인 일주일을 보냈다는 것은, 사용자를 데려오기 위한 시도를 전혀 하지 않은 일주일이었다는 뜻입니다.
명분이 진짜인지 가짜인지는 사실 그 자체로는 중요하지 않습니다. 크로스플랫폼 전환의 명분이 진짜였다고 해도, 그것이 마케팅 공포를 회피하는 타이밍에 실행됐다면 도망의 기능을 수행한 것입니다. 재작성이 완료된 뒤 v2로 더 많은 사람에게 접근하게 됐는지, 아니면 v2 개발 동안에도 여전히 코딩 뒤에 숨어 있었는지가 실제로 중요한 질문입니다. 그 답은 솔직히 말하면 후자에 가까웠습니다. v2가 완성된 뒤에도 저는 사람을 데려오는 대신 기능을 추가했습니다.
이 패턴을 스스로 인식한 것이 이 개발기의 핵심 자산 중 하나입니다. 아는 것과 멈추는 것은 다르지만, 아는 것에서 출발하지 않으면 멈출 수조차 없습니다. 그래서 이 글을 씁니다. 저와 비슷한 패턴을 가진 1인 개발자가 한 번이라도 빨리 이 질문을 스스로에게 던질 수 있다면, 이 기록이 쓸모가 있을 것입니다.
추가로 유용했던 자기점검 질문을 하나 더 적어둡니다. "이 작업을 지금 해야 하는 이유가 데이터에 있는가, 아니면 내 두려움에 있는가?" 안드로이드 수요가 데이터로 존재했다면 크로스플랫폼 전환은 타이밍이 맞는 결정이었습니다. 하지만 데이터가 없었습니다. 이유는 두려움 쪽에 있었습니다. 그것이 재작성을 도망으로 분류하게 만드는 결정적 요소였습니다.
v2 스택 선택의 구체적 논리 — Expo를 선택하기까지
크로스플랫폼으로 가겠다는 결정 이후에는 어떤 스택을 선택할지를 정해야 했습니다. 선택지는 몇 가지가 있었습니다.
Flutter는 첫 번째 대안이었습니다. Google이 개발하고 Dart 언어를 사용하며, 렌더링 엔진이 네이티브 UI 컴포넌트에 의존하지 않아 일관성이 높습니다. 커뮤니티가 크고, 퍼포먼스도 React Native보다 일반적으로 높게 평가됩니다. 그러나 Dart는 처음부터 새로 배워야 하는 언어였습니다. v1 개발에서도 배움의 부담이 컸는데, 다시 새 언어를 익히면서 앱을 만드는 것은 생산성 측면에서 역행이라고 판단했습니다. Flutter를 제외했습니다.
Kotlin Multiplatform Mobile(KMM)은 비즈니스 로직 공유에는 강점이 있지만, UI 레이어는 여전히 플랫폼별로 따로 작성해야 합니다. 이것은 크로스플랫폼의 핵심 이점인 코드 공유를 UI 영역에서 포기하는 구조입니다. 제 경우 비즈니스 로직보다 UI 반복 작업이 병목이었기 때문에 이 선택지도 제외했습니다.
React Native with bare workflow와 React Native with Expo managed workflow 사이에서는 Expo를 선택했습니다. 차이는 네이티브 코드에 대한 접근 수준입니다. Bare workflow는 Android Studio와 Xcode 프로젝트를 직접 열고 수정할 수 있지만, 그만큼 관리 부담이 커집니다. Expo managed workflow는 app.json과 eas.json 설정만으로 빌드와 배포를 처리해주고, 네이티브 모듈도 Expo SDK가 제공하는 범위 안에서는 config plugin만으로 통합됩니다. 1인 개발자가 iOS와 Android 빌드 파이프라인을 모두 유지하는 오버헤드를 최소화하려면 managed workflow가 현실적이었습니다.
Expo SDK 버전은 당시 최신이었던 SDK 54를 선택했습니다. React Native 0.81, React 19가 기반이었습니다. TypeScript는 strict 모드로 설정했습니다. v1에서 타입 관련 런타임 오류를 몇 차례 경험했기 때문에, v2는 처음부터 엄격하게 가기로 했습니다. 컴파일 타임에 잡힐 수 있는 오류는 최대한 컴파일 타임에 잡겠다는 원칙이었습니다.
상태 관리는 Zustand를 사용하기로 했습니다. Redux는 boilerplate가 너무 많았고, Context API는 복잡한 상태 트리에서 리렌더링 최적화가 번거롭습니다. Zustand는 API가 단순하고, TypeScript 지원이 자연스러우며, 선택적으로 immer를 통한 불변성 관리도 가능했습니다. Supabase 클라이언트는 v1에서 이미 통합했던 경험이 있었기 때문에 v2에서도 그대로 재활용했습니다. 인증, 실시간 구독, Row Level Security 설정을 처음부터 다시 하지 않아도 됐습니다. 이것이 재작성에서 유일하게 그대로 가져온 큰 덩어리였습니다.
초기 폴더 구조도 v1보다 명시적으로 설계했습니다. Feature 기반으로 디렉토리를 나누되, 공통 컴포넌트와 유틸리티는 shared/ 하위에 모았습니다. 테스트 파일은 소스 파일과 같은 디렉토리에 두는 co-location 방식을 택했습니다. v1에서는 테스트를 거의 작성하지 않았는데, v2는 처음부터 테스트를 포함하는 구조로 시작하겠다는 의도였습니다. 실제로 얼마나 잘 지켜졌는지는 다음 편에서 솔직하게 적겠습니다.
재작성 첫 주 — 새 프로젝트의 달콤함과 첫 번째 현실 충격
새 프로젝트를 시작하는 첫 주는 항상 빠릅니다. 아직 부채가 없고, 아직 예외 케이스가 없고, 아직 레거시 코드가 없습니다. npx create-expo-app JustBlurt --template expo-template-blank-typescript를 실행하고 나면 깨끗한 초록빛 출발점이 생깁니다. 그 초록빛은 오래 가지 않지만, 처음 며칠은 진짜 빠릅니다.
기본 화면 구조는 이틀 만에 잡혔습니다. 홈 화면, 녹음 화면, 노트 목록 화면, 설정 화면. Expo Router를 파일 기반 라우팅으로 사용했는데, Next.js의 App Router와 유사한 구조여서 적응이 빨랐습니다. 기본 네비게이션 스택은 반나절이면 됐습니다.
첫 번째 현실 충격은 오디오 녹음 모듈에서 왔습니다. Expo에는 expo-av라는 오디오 패키지가 있습니다. 문서에는 iOS와 Android 모두를 지원한다고 되어 있었고, 기본 녹음 기능은 실제로 작동했습니다. 문제는 배경 오디오였습니다. iOS에서 앱이 백그라운드로 내려가거나 화면이 잠기면 기본 설정으로는 녹음이 중단됩니다. v1에서도 다루었던 그 문제가 그대로 재현됐습니다.
Expo managed workflow에서 배경 오디오를 활성화하려면 app.json에 ios.infoPlist.UIBackgroundModes에 audio를 추가하고, expo-av의 Audio.setAudioModeAsync로 세션을 적절히 설정해야 합니다. 그런데 이것이 Expo Go(개발 중 빠른 프리뷰 앱) 환경에서는 완전히 테스트되지 않습니다. Expo Go는 일부 네이티브 기능을 지원하지 않고, 배경 오디오도 그 중 하나였습니다. 실제 기기에서 테스트하려면 Development Build를 만들어야 했고, 이를 위해 eas build --profile development를 실행해야 했습니다.
EAS(Expo Application Services) Build는 처음 설정이 까다로웠습니다. Apple Developer 계정 연결, 프로비저닝 프로파일 설정, 인증서 관리. 이 과정이 v1의 Xcode 설정과 크게 다르지 않았습니다. Expo가 추상화해주는 것이 많지만, 실제 기기 빌드까지 가면 결국 Apple의 인프라를 거쳐야 합니다. 첫 번째 Development Build를 실기기에 올리는 데 반나절이 걸렸습니다.
배경 녹음 문제를 해결한 뒤에는 Whisper STT 통합이 기다리고 있었습니다. v1에서는 Core ML로 변환된 Whisper 모델을 로컬에서 실행했습니다. React Native 환경에서는 이 접근이 직접 안 됩니다. whisper.rn이라는 커뮤니티 패키지가 있어서 테스트해봤는데, managed workflow에서는 config plugin으로 통합이 가능했지만 빌드 시간이 크게 늘어났고, 첫 번째 시도에서 Android 빌드가 실패했습니다. 이 문제를 우선 회피하기 위해 v2 초기에는 Whisper를 서버 측에서 처리하는 방식으로 임시 전환하고 로컬 STT는 나중으로 미뤘습니다.
이것이 재작성의 패턴입니다. 해결한 문제가 다른 형태로 다시 나타납니다. SwiftUI에서 네이티브 레이어와 씨름했던 것처럼, React Native에서도 결국 네이티브 레이어와 씨름하게 됩니다. 플랫폼이 바뀌어도 어렵다는 사실은 바뀌지 않습니다. 다만 어느 쪽이 내가 더 효율적으로 움직일 수 있는 어려움인지가 선택의 기준이 됩니다.
v2에서 실제로 달라진 것들
도망이었다고 해서 결과물까지 쓸모없는 것은 아닙니다. 재작성은 도망의 기능을 수행했지만, 그 과정에서 v1보다 실질적으로 나아진 것들도 생겼습니다. 솔직하게 구분해서 적겠습니다.
첫 번째로 달라진 것은 플랫폼 범위입니다. v1은 iOS 전용이었고, v2는 iOS와 Android 모두를 커버합니다. 이것은 사실입니다. 다만 앞에서 말한 것처럼, 이것이 실제 사용자 증가로 이어졌는지는 당시에는 증명할 수 없었습니다. 사용자가 0이었기 때문입니다. 그러나 App Store 심사와 Google Play 심사를 동시에 대비할 수 있는 구조가 됐고, 나중에 실제 마케팅을 시작할 때 안드로이드 사용자를 배제하지 않아도 됐습니다.
두 번째로 달라진 것은 커스텀 노트 타입 지원입니다. v1에서는 음성으로 녹음한 내용을 Notion의 단일 형식으로만 저장했습니다. v2에서는 노트 타입을 사용자가 선택할 수 있게 했습니다. 빠른 메모, 할 일 목록, 아이디어 스케치 등 Notion 데이터베이스의 서로 다른 템플릿과 매핑할 수 있는 구조로 설계했습니다. 이것은 v1 개발 중에도 필요하다고 생각했지만 구현하지 못했던 기능이었고, 재작성 덕분에 구조 단계에서부터 반영됐습니다.
세 번째는 위젯과 딥링크입니다. iOS 잠금 화면 위젯에서 버튼 하나로 녹음을 시작하는 것이 JustBlurt의 핵심 UX였습니다. v1에서는 WidgetKit 구현이 있었지만 안정적이지 않았습니다. v2에서는 Expo의 expo-widgets config plugin(당시 실험적)과 딥링크를 결합하는 방식을 택했고, 이 방식이 더 예측 가능하게 동작했습니다. Android에서는 앱 위젯 대신 잠금 화면 단축키를 사용했습니다.
네 번째는 테스트 커버리지입니다. v1에는 유닛 테스트가 없었습니다. v2에서는 핵심 비즈니스 로직, 특히 Notion API 직렬화/역직렬화, 오디오 세션 상태 관리, Supabase 인증 흐름에 대해 테스트를 작성했습니다. 커버리지가 높지는 않았지만, 주요 경로에 최소한의 회귀 방어선이 생겼습니다. 이것은 v1에서 자주 경험했던 "고쳤더니 다른 것이 망가짐" 현상을 줄이는 데 실제로 도움이 됐습니다.
그러나 달라지지 않은 것도 있습니다. 가장 중요한 것은, v2가 완성된 뒤에도 사용자 획득 전략이 없었다는 점입니다. 더 좋은 앱이 됐습니다. 하지만 "더 좋은 앱을 만들면 사용자가 알아서 찾아온다"는 믿음은 v2에서도 깨지지 않았습니다. 이 믿음이 깨지는 것은 6편, 앱스토어 2회 반려를 경험하고 나서였습니다.
그럼에도 v2는 더 나은 물건이 됐다
도망이었다고 해서 그 시간이 완전히 낭비였다는 뜻은 아닙니다. 재작성의 결과물인 v2는 v1보다 분명히 더 나은 앱이었습니다. 기능 범위, 코드 품질, 플랫폼 커버리지, 테스트 구조 모두에서 개선이 있었습니다. 그리고 그 과정의 많은 부분은 Claude Code 에이전트와 함께 진행됐습니다.
v2 개발에서 저는 처음으로 AI 코딩 에이전트를 단순한 자동완성 도구가 아니라 실질적인 공동 개발자로 사용했습니다. 화면 컴포넌트의 초안을 생성하게 하고, TypeScript 타입 정의를 함께 다듬고, 테스트 케이스를 작성하게 했습니다. 보일러플레이트가 많은 Expo/React Native 세계에서 에이전트는 특히 유용했습니다. 같은 패턴을 반복하는 코드를 빠르게 뽑아주고, 제가 패턴의 예외 케이스와 아키텍처 결정에 집중할 수 있게 해주었습니다.
그 구체적인 과정 — 1인 개발자가 에이전트를 어떻게 부려서 앱을 짜는지, 어떤 프롬프트가 효과적이었고 어떤 방식이 오히려 시간을 잡아먹었는지, 코드 리뷰를 에이전트에게 맡기는 것이 어떤 의미가 있는지 — 이 다음 편의 주제입니다. 이 시리즈에서 가장 실용적인 편입니다. v2를 만들면서 실제로 사용한 워크플로우를 구체적으로 공유합니다.
4편을 마무리하면서 정리하면, 이 결정은 도망이었습니다. 그러나 더 나은 제품을 향한 도망이었습니다. 그 구분이 의미 있는가는 결과가 결정합니다. 더 나은 제품이 더 많은 사람에게 닿았을 때, 그 재작성은 정당화됩니다. 사용자 없는 더 나은 제품은 도망한 채로 남습니다.
저의 경우는 어땠을까요. v2는 실제로 App Store에 제출됐습니다. 반려됐고, 수정됐고, 다시 반려됐습니다. 그 과정에서 제품에 대한 피드백을 처음으로 받게 됩니다. 피드백이 가르쳐준 것은 기능의 문제가 아니었습니다. 6편에서 그 이야기를 합니다.
- 떠오른 생각을 Notion에 박제하고 싶었다 발행
- 다 만들고도 제출조차 못 한 첫 버전 발행
- 준비 중독, 그리고 마케팅 공포 발행
- 그래서 나는 또 도망쳤다 — v2 전면 재작성 현재 편
- Claude Code로 앱을 다시 짓다 발행
- 앱스토어 2회 반려가 진짜로 말한 것 발행
- 너무 느려서 내가 안 썼다 — 온디바이스 STT 피벗 발행
- 매출 $0에서 다시 정한 우선순위 발행
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
댓글
댓글 쓰기