Vercel 롤백의 숨겨진 로직: 2시간의 삽질로 배운 교훈
문제의 시작
로컬에서 완벽하게 작동하는 코드를 Vercel에 배포했다. 몇 분 후 "Deployed Successfully" 메시지를 확인했다.
프로덕션 사이트를 열었다. 변경사항이 반영되지 않았다.
"캐시인가?" 하드 리프레시를 했다. 여전히 이전 버전이다.
"뭐지?"
디버깅의 시작
1차 시도: 코드 재확인
로컬과 프로덕션이 다르다면 코드 문제일 것이다. 코드를 다시 확인했다.
문제없다. 로컬에서는 완벽하게 작동한다.
2차 시도: 환경 변수
프로덕션 환경 변수가 잘못되었을 수도 있다. Vercel 대시보드에서 환경 변수를 확인했다.
모두 정상이다.
3차 시도: 재배포
혹시 배포 과정에서 뭔가 잘못되었을까? 재배포를 했다.
"Building... Deployed Successfully!"
프로덕션 확인. 여전히 이전 버전.
4-8차 시도: 계속된 재배포
"이번엔 뭔가 다를 거야."
코드를 약간 수정하고 재배포. 안 됨. 다른 부분을 수정하고 재배포. 안 됨. 콘솔 로그를 추가하고 재배포. 안 됨. 완전히 다른 접근으로 수정하고 재배포. 안 됨.
2시간이 지났다.
깨달음
Vercel 대시보드를 다시 자세히 봤다.
Deployments:
✅ v13 - Deployed 2m ago
✅ v12 - Deployed 15m ago
✅ v11 - Deployed 30m ago
✅ v10 - Deployed 1h ago
→ v9 - Production (Active)
잠깐.
v13까지 배포했는데 왜 v9가 Production인가?
각 배포를 클릭해서 상태를 확인했다:
- v13: Ready (but not Production)
- v12: Ready (but not Production)
- v11: Ready (but not Production)
- v9: Production
모든 최신 배포가 프로덕션이 아니었다.
진짜 원인: 롤백의 숨겨진 로직
타임라인을 재구성했다:
어제:
- v10을 프로덕션에 배포
- 버그 발견
- v9로 롤백 (Promote to Production)
오늘:
- v11 배포 → 자동으로 staging
- v12 배포 → 자동으로 staging
- v13 배포 → 자동으로 staging
Vercel의 로직:
과거 버전으로 수동 롤백을 하면, 시스템은 "의도적인 프로덕션 고정"으로 인식한다. 그 이후의 모든 배포는 자동으로 프로덕션에 적용되지 않고 staging 상태로 남는다. 프로덕션에 적용하려면 명시적으로 "Promote to Production"을 해야 한다.
이것을 몰랐다.
해결
v13 배포를 찾아서 "Promote to Production" 버튼을 클릭했다.
몇 초 후, 모든 변경사항이 프로덕션에 반영되었다.
2시간의 삽질이 끝났다.
왜 이런 로직인가?
처음에는 이해가 안 갔다. "왜 최신 배포가 자동으로 프로덕션이 안 되지?"
하지만 생각해보니 합리적이다.
시나리오:
- 프로덕션에 치명적 버그 발생
- 급하게 과거 버전으로 롤백
- 버그를 수정하고 새 버전 배포
- 근데 수정이 완벽하지 않아서 또 배포
- 여러 번 시도...
이 과정에서 모든 배포가 자동으로 프로덕션에 적용된다면?
각 배포마다 프로덕션이 요동친다. 사용자들은 혼란스럽다. 버그가 반복된다.
Vercel의 접근: 롤백 = "지금은 프로덕션을 안정화하고 싶다"는 신호 → 이후 배포는 staging으로 → 충분히 검증하면 수동으로 Promote
안전장치였다.
배운 점
1. 롤백은 단순한 과거 회귀가 아니다
롤백은 시간을 되돌리는 것이 아니라, 배포 파이프라인의 상태를 변경하는 행위다.
많은 배포 시스템에서 롤백은 이후 동작에 영향을 미친다:
- Vercel: 롤백 후 모든 배포가 staging
- AWS CodeDeploy: 롤백 후 auto-deployment 비활성화
- Kubernetes: rollback 후 새 deployment는 manual approval
- Heroku: 롤백 후 pipeline promotion 필요
2. "Deployed Successfully" ≠ "Production에 적용됨"
배포 시스템은 여러 단계로 구성된다:
- Build
- Deploy
- Promote to Production
"Deployed Successfully"는 Deploy 단계까지만 의미한다.
3. 상태를 제대로 봐야 한다
Vercel 대시보드에서 각 배포의 상태가 표시된다:
- ✅ Production
- 🔵 Preview
- 🟡 Ready (staging, 롤백 후)
나는 "Deployed Successfully"만 보고 안심했다. 상태를 자세히 확인했어야 했다.
4. 로컬-프로덕션 차이의 진짜 원인
"로컬에서는 되는데 프로덕션에서는 안 돼"라는 문제의 원인은 다양하다:
흔한 원인:
- 환경 변수 차이
- 빌드 환경 차이
- 의존성 버전 차이
내 경우:
- 배포 상태 차이 (staging vs production)
코드는 전혀 문제가 없었다.
5. 문서를 읽자
Vercel 문서에는 이 동작이 명확히 설명되어 있다:
"When you rollback to a previous deployment, subsequent deployments will not automatically be promoted to production. You must manually promote them."
하지만 나는 문서를 자세히 읽지 않았다. 2시간을 낭비했다.
체크리스트 업데이트
이제 배포 후 체크리스트에 이것을 추가했다:
배포 후 확인사항:
- Build 성공 확인
- Deploy 완료 확인
- 배포 상태 확인 (Production? Staging?)
- 최근 롤백 이력 확인
- 필요시 "Promote to Production" 실행
- 프로덕션 사이트에서 직접 확인
- 주요 기능 동작 테스트
특히 롤백 이후에는 더 주의깊게 확인해야 한다.
비슷한 경험들
이 글을 쓰면서 다른 개발자들에게 물어봤다. 비슷한 경험이 많았다:
A 개발자: "AWS CodeDeploy에서 롤백했는데, 다음 배포가 자동으로 안 돼서 한참 헤맸어요. Auto-deployment가 비활성화된 걸 몰랐죠."
B 개발자: "Kubernetes에서 rollback 하고 나서 새 deployment가 pending 상태로만 있더라고요. Manual approval이 필요한 걸 나중에 알았어요."
C 개발자: "Heroku pipeline에서 롤백 후에 새 빌드가 production에 자동으로 안 올라갔어요. Promote 버튼을 직접 눌러야 했죠."
패턴이 보인다. 롤백은 배포 파이프라인에 "수동 모드"를 활성화한다.
결론
2시간의 삽질은 아까운 시간처럼 보였다. 하지만 값진 투자였다.
이제 나는:
- Vercel의 롤백 로직을 이해한다
- 배포 상태를 제대로 확인한다
- "Deployed Successfully"를 맹신하지 않는다
- 롤백 후 더 주의깊게 배포한다
그리고 이 글을 읽는 누군가의 2시간을 절약할 수 있다면, 내 2시간은 충분히 가치있다.
서버 드리븐 UI 도입과 롤백: 완벽한 아키텍처가 아닌 필요한 아키텍처
문제의 발견
KnotNet의 대시보드는 사용자가 작성한 노트 수에 따라 다른 인사이트 섹션을 보여준다.
// 프론트엔드에 하드코딩된 비즈니스 로직
{notes.length >= 2 && <Summary />}
{notes.length >= 4 && <PatternInsights />}
{notes.length >= 10 && <Discovery />}
각 섹션은 개별 API를 호출한다:
/api/insights
- Summary용/api/pattern-insights
- Pattern Insights용/api/insights-discovery
- Discovery용
코드를 보다가 문득 생각이 들었다.
"모바일 앱 만들면 이 로직 또 구현해야 하잖아?"
iOS에서 한 번, Android에서 한 번. 그리고 변경사항이 생기면? 세 곳 모두 수정.
이것은 좋지 않다.
아이디어: 서버 드리븐 UI
해결책이 떠올랐다. 서버 드리븐 UI (Server-Driven UI).
기존 방식:
- 프론트엔드: "노트가 몇 개지? 4개네. 그럼 Pattern Insights 보여줘야겠다."
- 프론트엔드가 비즈니스 로직을 알고 있음
서버 드리븐 방식:
- 프론트엔드: "서버야, 뭘 보여줄지 알려줘"
- 서버: "이거랑 저거 보여줘. 데이터도 함께 줄게"
- 프론트엔드: "알았어, 그냥 그대로 렌더링할게"
장점이 명확해 보였다:
- Single Source of Truth: 비즈니스 로직이 서버 한 곳에만
- 플랫폼 독립적: Web, iOS, Android 모두 같은 API
- 유연한 제어: A/B 테스트, 기능 플래그를 서버에서만 관리
- 구독 플랜 대응: 유료/무료에 따른 기능 제어가 쉬움
완벽해 보였다.
설계의 진화
하지만 어떻게 구현할 것인가? 여러 버전을 고민했다.
v1: 단순 플래그 방식
{
"showSummary": true,
"showPatternInsights": false,
"showDiscovery": false
}
문제: 여전히 프론트엔드가 어떤 컴포넌트를 어떻게 렌더링할지 알아야 한다.
v2: 컴포넌트 이름 전달
{
"sections": ["Summary", "PatternInsights"]
}
문제: 프론트엔드가 컴포넌트 이름을 알아야 하고, 각 섹션의 props는?
v3: 완전한 서버 드리븐 (최종 선택)
{
"sections": [
{
"type": "text-card",
"title": "요즘의 나는...",
"content": "목표와 도전과제를 중심으로 생각하고 있어요",
"color": "orange"
},
{
"type": "pattern-list",
"title": "반복되는 패턴",
"patterns": [
{ "text": "주말마다 골프", "noteId": "123" }
]
}
]
}
프론트엔드는 type
만 보고 적절한 컴포넌트를 렌더링하면 된다. 데이터도 모두 포함되어 있다.
완벽하다!
구현
Step 1: 통합 API 엔드포인트
기존의 여러 API를 하나로 통합했다.
// /api/dashboard/route.ts
export async function GET(request: NextRequest) {
const userId = request.searchParams.get('userId')
const noteCount = await getNoteCount(userId)
const sections: DashboardSection[] = []
// 서버에서 모든 비즈니스 로직 처리
if (noteCount >= 2) {
const summary = await fetchSummary(userId)
sections.push({
type: 'text-card',
title: '요즘의 나는...',
content: summary.content,
color: 'orange'
})
}
if (noteCount >= 4) {
const patterns = await fetchPatterns(userId)
sections.push({
type: 'pattern-list',
title: '반복되는 패턴',
patterns: patterns
})
}
if (noteCount >= 10) {
const discovery = await fetchDiscovery(userId)
sections.push({
type: 'discovery-card',
discoveries: discovery
})
}
return NextResponse.json({ sections })
}
Step 2: 프론트엔드 단순화
프론트엔드 코드가 극적으로 단순해졌다.
// Before: 복잡한 조건부 렌더링
{notes.length >= 2 && insights && (
<Summary data={insights.summary} />
)}
{notes.length >= 4 && patternInsights && (
<PatternInsights data={patternInsights} />
)}
{notes.length >= 10 && discovery && (
<Discovery data={discovery} />
)}
// After: 단순 반복 렌더링
{sections.map((section, index) => {
switch(section.type) {
case 'text-card':
return <TextCard key={index} {...section} />
case 'pattern-list':
return <PatternList key={index} {...section} />
case 'discovery-card':
return <DiscoveryCard key={index} {...section} />
}
})}
조건부 로직이 사라지고, 단순한 매핑만 남았다.
Step 3: 타입 안전성
TypeScript Union Types를 사용해 타입 안전성을 확보했다.
type TextCardSection = {
type: 'text-card'
title: string
content: string
color: string
}
type PatternListSection = {
type: 'pattern-list'
title: string
patterns: Pattern[]
}
type DiscoveryCardSection = {
type: 'discovery-card'
discoveries: Discovery[]
}
type DashboardSection =
| TextCardSection
| PatternListSection
| DiscoveryCardSection
interface DashboardResponse {
sections: DashboardSection[]
expectedSections: string[] // 스켈레톤 로딩용
}
any
타입 없이 완벽한 타입 추론이 가능했다.
모바일 대응 완료
이제 iOS나 Android 앱을 만들 때 동일한 API를 사용하면 된다.
// iOS
for section in response.sections {
switch section.type {
case "text-card":
renderTextCard(section)
case "pattern-list":
renderPatternList(section)
case "discovery-card":
renderDiscoveryCard(section)
}
}
// Android
for (section in response.sections) {
when (section.type) {
"text-card" -> renderTextCard(section)
"pattern-list" -> renderPatternList(section)
"discovery-card" -> renderDiscoveryCard(section)
}
}
비즈니스 로직은 한 곳에만. 플랫폼은 렌더링만.
완벽하다!
그런데...
사용하다 보니 문제가 보이기 시작했다.
문제 1: 캐시 동기화
각 섹션이 독립적인 데이터를 가지고 있어, 캐싱 전략이 복잡해졌다.
- Summary는 매일 갱신
- Pattern은 주 단위로 갱신
- Discovery는 실시간
하나의 API에서 모두 처리하니, 캐시 invalidation 전략이 복잡해졌다.
문제 2: 개발 속도
프론트엔드 수정을 하려면 서버도 수정해야 했다.
"Summary 카드 디자인 바꾸고 싶은데... 서버 API 응답 구조도 바꿔야 하네?"
작은 UI 수정에도 서버 배포가 필요했다.
문제 3: 복잡도
아키텍처가 복잡해졌다. 새로운 팀원이 코드를 이해하는 데 시간이 걸렸다.
"섹션 추가하려면 어떻게 해요?" "서버 API 먼저 수정하고, 타입 정의하고, 프론트 렌더러 추가하고..."
문제 4: 현실
그리고 결정적으로: 지금 모바일 앱이 없다.
웹만 있는 상황에서 플랫폼 독립성을 위한 추상화는... 오버엔지니어링이었다.
롤백 결정
고민 끝에 롤백을 결정했다.
좋은 아키텍처다. 맞다. 하지만 지금 필요한가?
- 모바일 앱: 아직 없음
- 팀 크기: 1명 (나)
- 개발 속도: 중요함
- 복잡도: 증가함
트레이드오프 분석:
항목 | 서버 드리븐 | 기존 방식 |
---|---|---|
플랫폼 독립성 | ✅ 우수 | ❌ 각각 구현 |
개발 속도 | ❌ 느림 | ✅ 빠름 |
복잡도 | ❌ 높음 | ✅ 낮음 |
캐싱 | ❌ 복잡 | ✅ 단순 |
현재 필요성 | ❌ 낮음 | ✅ 높음 |
현재 단계에서는 기존 방식이 더 적합했다.
롤백 과정
롤백은 생각보다 쉬웠다.
- 통합 API를 다시 개별 API로 분리
- 프론트엔드 조건부 렌더링 복원
- 타입 정의 단순화
한나절 만에 완료했다.
배운 점
1. 좋은 아키텍처 ≠ 지금 필요한 아키텍처
서버 드리븐 UI는 훌륭한 패턴이다. Netflix, Airbnb 같은 대기업들이 사용한다.
하지만 그들은:
- 여러 플랫폼을 가지고 있고
- 큰 팀이 있고
- A/B 테스트를 자주 하고
- 자주 변경되는 비즈니스 로직이 있다
나는:
- 웹만 있고
- 혼자 개발하고
- A/B 테스트는 아직이고
- 빠른 개발이 중요하다
컨텍스트가 다르다.
2. 점진적 발전
완벽한 아키텍처를 처음부터 만들 필요는 없다.
필요할 때 리팩토링하면 된다.
지금:
- 웹만 있음
- 조건부 렌더링으로 충분
나중에 (모바일 앱 출시):
- 서버 드리븐 UI 재도입
- 이번 경험이 도움될 것
3. 실용주의
이론적으로 우수한 것과 실무적으로 적합한 것은 다르다.
- 책에서 배운 패턴
- 대기업 사례
- 컨퍼런스 발표
모두 좋다. 하지만 내 상황에 맞는가?
4. 롤백은 실패가 아니다
롤백을 결정하면서 실패한 것 같은 기분이 들었다.
"시간 낭비한 거 아냐?"
하지만 아니었다.
배운 것:
- 서버 드리븐 UI 패턴을 깊이 이해
- 트레이드오프를 체험적으로 학습
- 모바일 앱 출시 시 더 나은 설계 가능
- 언제 사용하고 언제 사용하지 말아야 하는지 앎
이것은 학습이었다. 투자였다.
다음 계획
모바일 앱 출시 시점에 서버 드리븐 UI를 다시 도입할 것이다.
하지만 이번에는 다르게:
개선점:
- 섹션별 독립 캐싱: 각 섹션이 독립적인 TTL을 가짐
- 개발 워크플로우: 프론트 수정이 잦은 부분은 클라이언트에 유연성 제공
- 더 나은 타입 시스템: Zod나 io-ts로 런타임 검증 추가
- 점진적 도입: 한 번에 모든 화면이 아니라 필요한 부분부터
결론
완벽한 아키텍처를 추구하는 것은 좋다. 하지만 지금 필요한 아키텍처가 더 중요하다.
서버 드리븐 UI는 훌륭한 패턴이다. 하지만:
- 팀 규모
- 제품 단계
- 플랫폼 수
- 개발 속도 요구사항
을 고려해야 한다.
나는 이번에 도입했다가 롤백했다. 실패가 아니라 현명한 선택이었다.
그리고 모바일 앱이 나올 때, 나는 더 나은 서버 드리븐 UI를 구현할 준비가 되어 있을 것이다.
좋은 엔지니어는 최신 기술을 쓰는 사람이 아니라, 적절한 기술을 선택하는 사람이다.
그리고 때로는, 가장 적절한 선택은 "아직 아니야"일 수 있다.
제품 철학과 사용자 니즈의 간극: KnotNet 첫 인터뷰에서 배운 것
첫 대화
KnotNet을 만든 지 2주. 유튜브를 통해 알게 되어 실제로 사용하고 있다는 사람과 인터뷰를 했다.
전직 기획자이자 현재 교육 콘텐츠 관련 일을 하는 분.
갤럭시 노트부터 Notion, Google Docs, Type, Pocket까지 다양한 기록 도구를 사용해 본 경험이 있는 "파워 유저"였다.
완벽한 인터뷰 대상이었다. 그리고 나는 많은 것을 배웠다.
가장 큰 깨달음은 이것이었다: 내가 만든 것과 사용자가 보는 것은 달랐다.
두 개의 KnotNet
내가 만든 KnotNet: "기록을 통해 생각의 연결고리를 발견하고, 자신을 이해하는 여정"
성찰, 자기 이해, 감정 분석, 패턴 발견. 깊이 있는 도구. 나를 알아가는 과정.
사용자가 보는 KnotNet: "생각을 자유롭게 던질 수 있는 공간"
수집, 스크랩, 빠른 메모, 링크 저장. 가볍게 던지는 도구. 정리는 나중에.
같은 제품, 완전히 다른 인식.
사용자가 원한 것
인터뷰에서 사용자가 요청한 것들:
1. 모바일 공유 기능
"인스타그램이나 유튜브에서 영감 받는 콘텐츠를 보면, 바로 KnotNet에 던지고 싶어요. 지금은 복사-붙여넣기 해야 하는데 번거로워요."
Share Intent 연동. 기술적으로 복잡하지 않지만, 사용자에게는 핵심 니즈였다.
2. 자동 정리
"Pocket처럼 링크를 저장하면 자동으로 요약해주고, AI가 태그를 달아주면 좋겠어요. 제가 수동으로 정리하는 건 싫거든요."
자동화. 수동 작업 최소화. "던지면 알아서" 정리되는 구조.
3. 모바일 중심 사용성
"산책하다가 생각이 떠오르면 바로 기록하고 싶어요. 집에 가서 컴퓨터 켜는 건... 그때는 이미 잊어버려요."
모바일 퍼스트가 아니라 모바일 온리. 즉각성이 핵심.
4. 결과물 생성
"제가 던진 자료들로 AI가 보고서나 계획서 초안을 만들어주면... 그땐 돈 낼 의향 있어요."
단순 저장을 넘어선 가치. 인풋에서 아웃풋으로.
사용자가 모르는 것
더 충격적인 발견이 있었다.
KnotNet의 핵심 기능들:
- 자연어 검색: "기분 좋았던 날" 같은 문장으로 검색 가능
- 유사노트 연결: AI가 관련된 과거 기록을 자동으로 찾아줌
- 인사이트 탭: 기록 패턴과 감정 분석
사용자는 이 기능들의 존재를 전혀 몰랐다.
"어? 그런 기능이 있었어요? 어떻게 쓰는 거예요?"
아무리 좋은 기능도 사용자가 발견하지 못하면 없는 것과 같다.
"메모계의 핀터레스트"
인터뷰 중 사용자가 한 표현:
"이거 메모계의 핀터레스트 같아요."
핀터레스트. 시각적 수집 도구. 영감 저장소. 나중에 보기 위한 공간.
내가 생각한 것과 완전히 달랐다. 나는 몰스킨 노트를 생각했는데, 사용자는 핀터레스트를 봤다.
유료 전환의 조건
"지금 무료로 쓰고 있는데, 유료로 전환할 의향 있으세요?"
"지금처럼 던지기만 하는 거면 무료로 충분해요. 근데..."
"근데?"
"AI가 제가 던진 자료로 가치 있는 결과물을 만들어준다면? 예를 들어 제 노트들을 바탕으로 보고서 초안을 작성해준다거나, 프로젝트 계획서를 만들어준다면? 그땐 기꺼이 지불할 의향 있어요."
명확한 가치 전환 지점이 보였다:
단계 | 기능 | 가치 | 과금 |
---|---|---|---|
1 | 수집 | "던지는 공간" | Free |
2 | 자동 정리 | "정리해주는 도구" | Free/Freemium |
3 | 인사이트 | "패턴 발견" | Premium |
4 | 결과물 생성 | "아웃풋 창출" | Premium |
사용자는 1단계에 있다. 유료 전환은 4단계에서 일어난다.
철학 vs 니즈
제품 철학: "기록을 통한 자기 성찰" 사용자 니즈: "생각을 던지는 수집 공간"
이 간극을 어떻게 해석해야 할까?
옵션 1: 철학을 버린다
사용자가 원하는 대로 만든다. 수집 도구에 집중한다. Pocket이나 Notion의 클론이 된다.
옵션 2: 니즈를 무시한다
원래 비전을 고수한다. "사용자가 이해하지 못하는 것뿐"이라고 생각한다. 설득하려 한다.
옵션 3: 점진적 진화
초입은 낮춘다. "던지는 공간"으로 시작한다. 하지만 점진적으로 "성찰 도구"로 유도한다.
나는 3번을 선택한다.
사용자 여정의 재설계
Phase 1: 수집 (현재)
- 가볍게 던질 수 있는 공간
- 모바일 공유 연동
- 마찰 없는 입력
목표: "여기에 모든 걸 던지게 만들기"
Phase 2: 자동화
- 링크 자동 요약
- AI 자동 태그
- 수동 작업 최소화
목표: "정리는 AI가 알아서"
Phase 3: 발견
- 유사노트 자동 연결 (이미 있지만 숨겨짐)
- 패턴 분석
- 감정 추적
목표: "내 기록에서 패턴 발견하기"
Phase 4: 인사이트
- 기록 기반 리포트 생성
- 아이디어 연결
- 창의적 아웃풋
목표: "던진 자료가 결과물이 되다"
Phase 5: 성찰 (궁극적 목표)
- 나를 이해하는 도구
- 성장 추적
- 자기 발견
목표: "기록이 삶이 되다"
사용자는 1단계에서 시작한다. 5단계까지 자연스럽게 이동시키는 것이 나의 역할이다.
UX의 실패
핵심 기능을 사용자가 모른다는 것은 UX의 실패다.
문제 1: 검색창의 숨은 힘 자연어 검색이 가능한데, 사용자는 키워드 검색만 가능한 줄 알았다.
해결: 플레이스홀더 텍스트 개선
- 현재: "검색..."
- 개선: "말하듯 검색해보세요 (예: '기분 좋았던 날')"
문제 2: 유사노트의 숨겨진 가치 AI가 관련 노트를 찾아주는데, 사용자는 그 탭을 클릭하지 않았다.
해결: 첫 노트 작성 후 자동 알림 "이 노트와 비슷한 생각을 2개 찾았어요 👀"
문제 3: 인사이트의 부재한 존재 탭이 있지만 가본 적이 없다.
해결: 주간 요약 푸시 알림 "이번 주 당신의 기록 패턴을 분석했어요"
기능이 아무리 좋아도 발견되지 않으면 의미가 없다.
Pocket × Notion × ChatGPT
사용자의 기대를 한 문장으로 정리하면:
"Pocket의 수집 편의성 + Notion의 정리 구조 + ChatGPT의 인사이트 생성"
이것은 실현 가능한가? 기술적으로는 가능하다.
문제는 우선순위다. 어디서 시작할 것인가?
단기 (1개월):
- 모바일 공유 연동 (Share Intent)
- 검색 UX 개선
- 기능 튜토리얼 강화
중기 (3개월):
- 링크 자동 요약
- AI 태그 자동화
- 유사노트 발견성 개선
장기 (6개월+):
- 노트 기반 리포트 생성
- 아이디어 연결 시각화
- 창의적 아웃풋 도구
린 스타트업의 교훈
에릭 리스는 말했다: "고객과 대화하라."
나는 했다. 그리고 배웠다.
배운 것 1: 제품 철학을 버릴 필요는 없다 다만 사용자 여정의 시작점을 낮춰야 한다.
배운 것 2: 사용자는 내가 생각한 대로 제품을 보지 않는다 그들의 렌즈로 봐야 한다.
배운 것 3: 기능보다 발견성이 중요하다 있는 기능을 모르면 없는 것과 같다.
배운 것 4: 가치 전환 지점은 명확하다 "결과물을 만들어주면 돈 낼게" - 이보다 명확할 수 없다.
배운 것 5: 한 번의 인터뷰가 몇 주의 추측을 대체한다 데이터가 아니라 대화. 숫자가 아니라 이야기.
다음 단계
이제 할 일이 명확하다:
- 단기: 사용자가 원하는 것을 준다 모바일 공유, 자동 정리, 발견성 개선
- 중기: 사용자가 필요한 것을 보여준다 패턴 발견, 인사이트 생성
- 장기: 사용자가 몰랐던 것을 깨닫게 한다 자기 이해, 성장, 성찰
제품 철학과 사용자 니즈. 둘은 모순이 아니라 여정의 양 끝이다.
내 역할은 사용자를 한쪽 끝에서 다른 쪽 끝으로 안내하는 것이다.
결론: 간극을 인정하고 다리를 놓다
첫 인터뷰는 충격이었다. 내가 만든 것과 사용자가 보는 것이 달랐다.
하지만 이것은 실패가 아니다. 발견이다.
제품이 시장과 맞지 않는 것이 아니라, 아직 다리를 놓지 못한 것이다.
"메모계의 핀터레스트"에서 시작해서 "생각의 연결고리를 발견하는 여정"으로.
수집에서 성찰로.
인풋에서 인사이트로.
그것이 KnotNet의 진짜 여정이다. 그리고 나는 이제 그 지도를 가지고 있다.
사용자가 그려준 지도를.
구독 기능 하나의 무게: 기술 통합의 보이지 않는 복잡성
단순해 보이는 기능
"안드로이드 앱에 구독 기능을 추가하자."
계획은 단순했다. 사용자들이 프리미엄 기능을 사용할 수 있도록 월 구독 플랜을 만드는 것. 많은 앱들이 하고 있는 일이고, Google Play도 이를 위한 시스템을 제공한다.
얼마나 복잡할까?
펼쳐진 체크리스트
실제로 시작하니 필요한 작업들이 보이기 시작했다:
- 판매 상품의 ID를 앱에 연결 Google Play Console에서 상품을 생성하고, 그 ID를 앱 코드에 연결해야 한다.
- 백엔드에 Google Play Developer API 설정 구독 상태를 서버에서 확인하려면 API 연동이 필요하다.
- Receipt 검증 엔드포인트 구현 사용자가 정말로 결제했는지 서버에서 검증해야 한다. 클라이언트만 믿으면 안 된다.
- 사용자 플랜 관리 로직 누가 어떤 플랜을 쓰고 있는지, 언제 만료되는지, 갱신은 되었는지 관리해야 한다.
여기까지는 예상 범위 내였다. 복잡하지만 필요한 것들이다.
미로의 시작
문제는 환경 변수 설정부터 시작되었다.
Google Play Developer API를 사용하려면 Service Account가 필요하다. 그래서 Service Account Key를 생성하려고 했다.
Google Cloud Console로 이동.
"Service Account Key를 생성할 권한이 없습니다."
뭐?
권한의 미로
권한을 얻으려면 어떻게 해야 하나?
구글링(아니, 이제는 AI에게 물어보기)을 하니 답이 나왔다.
Google Admin으로 가서 권한을 설정해야 한다.
Google Cloud Console이 아니라 Google Admin. 또 다른 시스템이다.
Google Admin에 로그인했다. 권한 설정 메뉴를 찾았다. 필요한 권한을 추가했다.
다시 Google Cloud Console로 돌아왔다.
이제 되나?
똥개 훈련
이 과정을 반복하면서 든 생각:
"이거 똥개 훈련시키는 거 같은데?"
시스템 A가 말한다: "이거 해" → 권한 없다 → 시스템 B로 가라 → 시스템 B: "여기서 이거 설정해" → 다시 시스템 A로 돌아옴 → 또 다른 권한 없다 → 시스템 C로 가라
끝없는 루프.
AI 시대의 개발
예전 같았으면 어땠을까?
각 단계마다 구글링을 했을 것이다. 스택 오버플로우를 뒤졌을 것이다. 공식 문서를 읽고 또 읽었을 것이다. 하루가 다 갔을 것이다.
지금은 Claude에게 물어본다:
"Google Play Developer API를 사용하려면 어떤 권한이 필요해?" "Service Account Key를 생성할 수 없는데 어떻게 해?" "Google Admin에서 어떤 설정을 해야 해?"
답은 빠르게 나온다. 정확하고 구체적이다.
정보 접근성은 확실히 좋아졌다.
하지만 여전히 피곤하다.
피곤함의 본질
왜 피곤할까?
정보가 없어서가 아니다. AI가 답을 알려주니까.
시스템의 복잡성 자체가 문제다.
Google Play 구독 시스템은:
- Google Play Console
- Google Cloud Console
- Google Admin
- 백엔드 서버
- 안드로이드 앱
최소 5개의 서로 다른 시스템을 오가며 설정해야 한다.
각 시스템은 자체 권한 체계를 가지고 있다. 각 시스템은 다른 UI를 가지고 있다. 각 시스템은 다른 용어를 사용한다.
인지 부하가 크다.
기술 부채가 아닌 생태계 부채
이것은 내 코드가 나쁘거나 설계가 잘못된 것이 아니다.
이것은 생태계의 복잡성이다.
Google이 나쁜 시스템을 만든 것도 아니다. 각 시스템은 나름의 이유로 존재한다:
- Play Console: 앱 배포 관리
- Cloud Console: 클라우드 리소스 관리
- Admin: 조직 권한 관리
문제는 이들이 서로 다른 시대에, 다른 팀에 의해, 다른 목적으로 만들어졌다는 것이다.
그리고 개발자는 이 모든 것을 이해하고 연결해야 한다.
기능 하나의 무게
"구독 기능 하나 추가하면 되지"라고 생각했다.
하지만 그 하나 뒤에는:
- 5개 이상의 시스템
- 10개 이상의 설정
- 20개 이상의 문서
- 수십 번의 시행착오
가 숨어있다.
번아웃의 순간
오늘은 여기서 멈췄다.
진이 다 빠졌다.
코드를 한 줄도 작성하지 못했다. 그냥 권한 설정만 했다.
생산적인 날이 아니었다.
하지만 이것도 개발의 현실이다.
AI는 만능이 아니다
AI 도구들은 정말 도움이 된다. Claude Code 없이 어떻게 개발했을까 싶다.
하지만 AI가 해결할 수 없는 것들이 있다:
- 시스템의 근본적 복잡성 여러 시스템을 오가는 것 자체는 여전히 인간이 해야 한다.
- 권한과 인증의 벽 AI는 권한을 대신 받아줄 수 없다.
- 인지 부하 여러 시스템의 멘탈 모델을 동시에 유지하는 것은 여전히 힘들다.
- 기다림 권한 설정, API 활성화, 전파 시간 등은 단축할 수 없다.
지속 가능한 개발
이런 날이 있다는 것을 인정해야 한다.
생산적이지 않은 날. 진이 빠지는 날. 똥개 훈련하는 느낌의 날.
이런 날은 쉬어야 한다.
억지로 밀어붙이면 번아웃이 온다. 그리고 번아웃은 회복하는 데 훨씬 오래 걸린다.
교훈
1. 기술 선택 시 통합 복잡도를 고려하라
"이 기능이 있네"가 아니라 "이 기능을 통합하는 데 얼마나 걸릴까"를 물어야 한다.
2. 문서상 단순함에 속지 말라
공식 문서는 항상 해피 패스만 보여준다. 실제로는 권한 문제, 설정 문제, 버전 문제 등이 존재한다.
3. 버퍼를 두어라
"하루면 되겠지"가 "사흘 걸렸네"가 되는 것이 당연하다. 특히 외부 플랫폼 통합은.
4. 쉬는 것도 일이다
진이 빠졌을 때 억지로 하는 것보다, 쉬었다가 맑은 정신으로 하는 것이 더 빠르다.
내일
내일 다시 시작할 것이다.
권한 설정은 끝났다. 이제 진짜 구현을 할 수 있다.
그리고 아마 또 다른 문제를 만날 것이다.
하지만 괜찮다. 이것이 개발이니까.
단순해 보이는 기능 하나에도 이렇게 많은 것들이 숨어있다.
그리고 그것을 해내는 것이 개발자다.
오늘은 쉰다. 내일은 다시 싸운다.
"예뻐지는 데이터": 숫자가 아닌 모양이 말해주는 것
데이터가 예쁘다는 건 무슨 뜻일까
KnotNet 개발 10일차, 동료가 데이터를 보더니 이렇게 말했다.
"데이터가 예뻐지고 있어."
처음엔 무슨 말인지 이해하지 못했다. 사용자 수가 폭발적으로 늘어난 것도 아니고, 특별히 큰 숫자가 나온 것도 아니었다. 하지만 그가 보고 있던 것은 숫자가 아니라 모양이었다.
그리고 나도 보기 시작했다. 데이터의 형태가 변하고 있다는 것을.
숫자의 변화
먼저 객관적인 지표부터 보자.
10일차 초반 vs 후반:
지표 | 초반 | 후반 | 변화 |
---|---|---|---|
파워유저 수 | 2명 | 4명 | +100% |
평균 활동일수(상위 6명) | 5.8일 | 6.0+일 | +3.4% |
노트/일 평균(상위 6명) | 6.3개 | 8.5개 | +35% |
숫자만 보면 그럭저럭 괜찮은 성장이다. 하지만 "예쁘다"는 표현을 쓸 정도는 아니다. 진짜 의미는 숫자 뒤에 숨어 있었다.
패턴의 변화: 진짜 이야기
1. 폭발형에서 리텐션형으로
초기 패턴: 한두 명의 사용자가 엄청난 양의 노트를 작성했다. 84개, 67개, 33개. 인상적인 숫자들이다. 하지만 이건 "스프린터"형 사용 패턴이다. 강렬하지만 지속 가능성이 불확실하다.
현재 패턴: 노트 수의 증가보다 활동일수의 증가가 두드러진다. 사용자들이 "한 번 많이 쓰기"에서 "여러 날 계속 쓰기"로 이동하고 있다.
이것이 의미하는 바:
- "그냥 해본다" → "계속 한다"
- 호기심 → 습관
- 실험 → 루틴
2. 의존에서 생태계로
초기 구조: 2명의 파워유저가 전체 활동의 대부분을 차지했다. 그들이 멈추면 데이터도 멈춘다. 위험한 구조다.
현재 구조:
지속형 코어층: 13일 연속 활동
중기 습관층: 6일 활동
단기 집중층: 4일간 84개 노트
습관 진입층: 5일 활동으로 안정화 진행
더 이상 한두 명에게 의존하지 않는다. 다양한 사용 패턴을 가진 유저들이 동시에 존재한다. 이것은 1:1 관계에서 1:N 커뮤니티로 전환되는 초입 신호다.
3. 자연적 리듬의 발견
가장 놀라운 발견은 이것이었다: 사용자들이 스스로 하루 5~9개의 노트를 작성하는 리듬을 찾았다는 것.
내가 이 숫자를 유도하지 않았다. 가이드를 주지도, 권장하지도 않았다. 그냥 자연스럽게 그렇게 되었다.
이것은 제품이 사용자의 실제 니즈와 리듬에 맞아떨어지고 있다는 신호다. 강제된 행동이 아니라 자발적인 습관이 형성되고 있다.
"예쁜 데이터"의 정의
이제 이해가 된다. "예쁜 데이터"란:
- 지속 가능한 성장 폭발적이지 않지만, 꾸준하고 안정적이다
- 다양성 한 타입이 아니라 여러 사용 패턴이 공존한다
- 자발성 강제나 인센티브가 아니라 자연스러운 행동이다
- 균형 극단적인 값들이 아니라 분포가 건강하다
- 방향성 숫자가 크지 않아도 올바른 방향으로 움직인다
성장곡선의 첫 변곡점
이것이 의미하는 바는 명확하다: 성장곡선의 첫 변곡점에 도달했다.
변곡점이란 곡선의 기울기가 바뀌는 지점이다. 물리적으로는 가속도가 바뀌는 순간이다.
이전: 한두 명의 파워유저에 의존하는 선형 성장 지금: 여러 유저층이 형성되며 자생적 생태계로 전환
숫자는 아직 작다. 하지만 형태는 근본적으로 달라졌다.
데이터가 말하는 것
이 패턴이 말해주는 것은:
1. 프로덕트가 작동한다
사람들이 한 번 쓰고 마는 것이 아니라 돌아온다. 이것은 가치를 발견했다는 의미다.
2. 타겟이 맞다
다양한 사용 패턴이 나타나는 것은 다양한 니즈를 충족시키고 있다는 뜻이다.
3. 습관화가 가능하다
연속 활동일수가 증가하는 것은 일상의 루틴으로 자리잡고 있다는 신호다.
4. 커뮤니티 잠재력이 있다
중간층이 형성되는 것은 파워유저와 일반 유저 사이의 다리가 생기고 있다는 뜻이다.
다음 단계: 리텐션 튜닝
이제 전략이 명확해진다.
하지 말아야 할 것:
- 성급한 유저 확장
- 새로운 기능 추가 경쟁
- 바이럴 마케팅 시도
해야 할 것:
- 리텐션 루프 강화
- 습관화 메커니즘 설계
- 자연적 리듬 지원
구체적 액션:
- "최근 작성 요약 카드" 자동 생성 사용자가 작성한 노트들에서 패턴과 인사이트를 발견하게 해준다. 반복 사용의 보상 구조를 만든다.
- "연속 작성일 배지" 추가 활동일수 증가를 가시화하고 보상한다. 하지만 부담스럽지 않게.
- "나와 비슷한 하루 찾기" 기능 실험 파워유저들의 리듬을 패턴화하여 중간층에게 영감을 준다.
- 유입 확장 보류 이 곡선이 고착될 때까지는 신규 유입보다 기존 유저 리텐션에 집중한다.
린 스타트업의 본질
에릭 리스의 린 스타트업은 "빠른 실패"가 아니다. 빠른 학습이다.
KnotNet의 10일은 다음을 가르쳐줬다:
- 큰 숫자보다 올바른 패턴이 중요하다
- 폭발적 성장보다 지속 가능한 성장이 가치있다
- 많은 기능보다 핵심 가치가 명확해야 한다
- 마케팅보다 프로덕트가 먼저다
결론: 모양이 말해준다
데이터 분석에서 가장 중요한 것은 숫자를 읽는 것이 아니라 이야기를 읽는 것이다.
140명의 사용자는 인상적인 숫자가 아니다. 하지만 그 중 4명이 매일 돌아오고, 6명이 일주일째 사용하며, 자연스러운 리듬이 형성되고 있다면?
그것은 살아있는 프로덕트의 신호다.
"예뻐지는 데이터"를 본다는 것은 이런 의미다. 숫자의 크기가 아니라 숫자의 모양을, 양의 증가가 아니라 질의 변화를, 지표의 달성이 아니라 방향의 정합성을 보는 것.
KnotNet의 데이터는 이제 예뻐지고 있다. 숫자는 아직 작지만, 형태는 완전히 살아났다.
그리고 이것이 스타트업의 첫 번째 진짜 승리다.
가장 가까운 세 사람의 거절: 타겟 유저의 명확성이 주는 교훈
세 번의 거절
KnotNet의 첫 버전을 만들고, 가장 가까운 사람들에게 테스트를 요청했다. 아내, 그리고 함께 일하는 두 명의 동료. 피드백을 받고 싶었고, 초기 사용자로 만들고 싶었다.
결과: 세 명 모두 사용하지 않았다.
처음에는 당황스러웠다. '내가 만든 게 그렇게 안 좋나?' '설명을 잘못한 건가?' 여러 생각이 스쳤다. 하지만 곧 깨달았다. 이것은 나쁜 신호가 아니라 오히려 좋은 신호라는 것을.
과거의 실수: 유튜브 채널
몇 년 전, 유튜브 채널을 시작했을 때의 일이다.
채널 초기에는 구독자 수가 중요하게 느껴졌다. 그래서 지인들에게 하나하나 연락했다. "구독 좀 해줘", "영상 한 번 봐줘". 친구들, 가족들, 동료들. 의리로, 인정으로, 그들은 구독 버튼을 눌러줬다.
초반 100명의 구독자를 모으는 것은 생각보다 쉬웠다.
하지만 그 다음이 문제였다.
타겟 유저가 명확하지 않았다.
채널이 누구를 위한 것인지, 어떤 가치를 제공하는지, 어떤 문제를 해결하는지 모호했다. 지인들은 '나'를 지지하기 위해 구독했지, '콘텐츠'에 관심이 있어서가 아니었다.
결과적으로:
- 콘텐츠 방향성을 잡기 어려웠다
- 피드백이 도움이 되지 않았다 (그들은 타겟이 아니니까)
- 성장이 멈췄다 (100명 이후 늘지 않았다)
- 채널의 정체성이 흐릿했다
그 경험에서 배운 교훈: 초기 숫자는 함정이다.
KnotNet은 다르다
KnotNet을 만들면서, 그 교훈을 기억했다.
KnotNet은 명확한 타겟을 위한 제품이다:
KnotNet의 타겟 유저:
- 일상을 기록하는 것에 가치를 느끼는 사람들
- 자신의 기억을 체계적으로 관리하고 싶은 사람들
- 기록을 통해 패턴을 발견하고 성장하고 싶은 사람들
- 과거의 자신과 대화하고 싶은 사람들
KnotNet의 타겟이 아닌 사람들:
- 기록에 관심이 없는 사람들
- 일기를 써본 적이 없고 쓸 생각도 없는 사람들
- 과거를 돌아보는 것에 가치를 느끼지 못하는 사람들
아내와 동료들은 후자에 속했다. 그들이 KnotNet을 사용하지 않는 것은 지극히 자연스러운 일이다. 그리고 이것은 제품의 명확성을 보여주는 좋은 신호다.
거절이 주는 명확성
세 명의 거절은 실패가 아니라 다음을 확인시켜줬다:
1. 제품의 정체성
KnotNet이 무엇이고, 누구를 위한 것인지가 명확하다. 모호한 제품이라면 누구나 "음... 뭔가 좋긴 한데?"라고 했을 것이다. 하지만 명확한 제품은 "이건 나한테 필요 없어" 혹은 "이거 정말 필요했어!"라는 확실한 반응을 만든다.
2. 타겟팅의 중요성
마케팅 메시지를 어떻게 만들어야 할지 더 명확해졌다. "모든 사람을 위한 기록 앱"이 아니라 "성장하고 싶은 사람을 위한 기억 관리 도구"로 포지셔닝해야 한다.
3. 피드백의 질
앞으로 받을 피드백이 더 가치있을 것이다. 타겟 유저로부터 받는 피드백은 제품 개선으로 직결된다. 비타겟 유저의 피드백은 오히려 방향을 흐릴 수 있다.
4. 채널 전략
어디서 사용자를 찾아야 할지 명확해졌다. 기록, 생산성, 자기계발에 관심 있는 커뮤니티에 집중하면 된다.
Early Adopters vs 지인의 지지
스타트업 생태계에서 흔히 하는 조언 중 하나는 "지인부터 시작하라"는 것이다. 하지만 이것에는 함정이 있다.
지인의 지지는:
- 따뜻하지만 피상적일 수 있다
- 의례적인 응원일 수 있다
- 진정한 니즈를 반영하지 않을 수 있다
진짜 Early Adopters는:
- 제품의 가치를 스스로 발견한다
- 능동적으로 피드백을 준다
- 다른 사람에게 자발적으로 추천한다
- 돈을 낼 의향이 있다
지인 100명보다 진짜 타겟 유저 10명이 훨씬 가치있다.
거절도 데이터다
프로덕트 개발에서 모든 반응은 데이터다:
"사용한다" = 긍정적 신호 제품이 니즈를 충족시킨다
"사용하지 않는다" = 중립적 신호 타겟이 아니거나, 제품이 문제를 해결하지 못한다
"사용해보고 싶다" = 잠재적 신호 메시징이나 온보딩 개선 필요
"이런 게 필요했어!" = 강력한 긍정 신호 진짜 타겟을 찾았다
세 명의 거절은 두 번째 카테고리다. 그들은 타겟이 아니다. 이것을 알게 된 것 자체가 진전이다.
다음 단계: 진짜 타겟 찾기
이제 할 일은 명확하다:
- 타겟 유저가 있는 곳 찾기
- 생산성 커뮤니티
- 자기계발 포럼
- 일기/저널링 관련 그룹
- 가치 제안 명확히 하기
- "기록 앱"이 아니라
- "성장을 위한 기억 관리 도구"
- 진짜 피드백 받기
- 타겟 유저의 반응 관찰
- 사용 패턴 분석
- 진정한 니즈 파악
- 반복적 개선
- 타겟 유저가 원하는 방향으로
- 명확한 가치 제공에 집중
결론: 명확성의 가치
세 명의 가까운 사람들이 KnotNet을 사용하지 않은 것은 실망스러운 일이 아니다. 오히려 제품이 명확한 타겟을 가지고 있다는 증거다.
모호한 제품은 모든 사람에게 애매하게 어필한다. 명확한 제품은 특정 사람들에게 강렬하게 어필하고, 나머지에게는 전혀 어필하지 않는다.
나는 후자를 선택한다.
유튜브 채널의 실수를 KnotNet에서 반복하지 않을 것이다. 초기 숫자에 집착하지 않고, 진짜 타겟 유저를 찾는 데 집중할 것이다.
그들이 누구인지는 이미 안다. 이제 그들을 찾아가면 된다.
거절도 데이터다. 그리고 이 데이터는 내가 올바른 방향으로 가고 있다는 것을 말해준다.
타임라인
- 1일차: 개발 시작
- 7일차: 웹 버전 완성 및 런칭
- 7-10일차: 140명의 초기 사용자 확보
- 10일차: 안드로이드 & iOS 앱 개발 완료, Google Play Store 테스트 모드 런칭
겨우 열흘 만에 웹과 모바일 앱을 모두 런칭했다. 이 속도에 대해 많은 사람들이 놀라워한다. 어떻게 가능했을까? 그리고 왜 이렇게 서둘렀을까?
가설: 모바일 접근성이 핵심이다
웹 버전을 만들면서 확인한 것은 사람들이 기록이라는 행위 자체에 관심이 있다는 것이었다. 140명이 가입했고, 실제로 사용했다. 프로덕트 마켓 핏의 초기 신호를 확인한 셈이다.
하지만 나에게는 더 중요한 질문이 있었다:
"사람들은 데스크탑 앞에 앉았을 때만 기록하는가, 아니면 일상의 순간순간에 기록하고 싶어 하는가?"
이 질문에 답하려면 웹 버전만으로는 부족했다. 웹은 의도적으로 접속해야 한다. 브라우저를 열고, 북마크를 찾거나 URL을 입력해야 한다. 이건 이미 하나의 장벽이다.
반면 모바일 앱은 다르다:
- 항상 주머니 속에 있다
- 홈 화면에서 한 번의 탭으로 접근 가능하다
- 푸시 알림으로 리마인드할 수 있다
- 버스, 카페, 침대에서도 자연스럽게 사용할 수 있다
내 가설은 이것이었다: 모바일의 접근성이 사용 빈도를 극적으로 높일 것이다.
빠른 실행이 가능했던 이유
1. 명확한 검증 목표
웹 버전을 만들 때부터 다음 단계가 명확했다. "웹에서 기본적인 프로덕트 마켓 핏을 확인하면, 즉시 모바일로 이동한다." 이 명확함이 우선순위 결정을 쉽게 만들었다.
2. MVP 원칙의 철저한 준수
웹 버전에서 핵심 기능을 검증했기 때문에, 앱 버전에서는 같은 기능을 모바일 UX에 맞게 구현하는 것에 집중할 수 있었다.
포함한 것:
- 기록 작성 및 조회
- 기본적인 검색 기능
- 구글 로그인
포함하지 않은 것:
- 고급 필터링
- 소셜 기능
- 복잡한 데이터 시각화
- 완벽한 UI 폴리싱
3. AI 도구의 전략적 활용
Claude Code는 이번 프로젝트에서 게임 체인저였다. 특히 모바일 앱 개발에서:
- React Native 보일러플레이트 빠른 설정
- 네이티브 기능 통합 (푸시 알림, 딥링킹 등)
- 플랫폼별 빌드 설정 자동화
- 디버깅 및 에러 해결
완벽하지 않았지만, 빠른 반복을 통해 작동하는 앱을 만들 수 있었다.
4. 완벽함보다 학습
이 시점에서 완벽한 앱이 목표가 아니었다. 목표는:
- 사용자 행동 데이터 수집
- 모바일 vs 웹 사용 패턴 비교
- 푸시 알림의 효과 측정
- 실제 모바일 환경에서의 UX 문제 발견
이런 것들은 실제 사용자가 실제 기기에서 사용해봐야만 알 수 있다.
테스트 모드로 시작한 이유
Google Play Store와 App Store 모두 테스트 트랙으로 먼저 런칭했다. 이유는:
1. 빠른 피드백 루프
프로덕션 리뷰는 시간이 걸린다. 테스트 트랙은 훨씬 빠르다.
2. 리스크 관리
초기 사용자들과 함께 문제를 찾고 고칠 수 있다.
3. 점진적 확장
작은 그룹에서 검증하고, 확신이 서면 공개 런칭으로 전환할 수 있다.
이제 시작되는 진짜 실험
앱이 스토어에 올라갔다. 이제 진짜 궁금한 질문들에 답할 시간이다:
데이터로 답할 질문들:
- 웹 vs 모바일 사용 빈도는 얼마나 차이나는가?
- 사람들은 하루 중 언제 가장 많이 기록하는가?
- 푸시 알림이 사용률을 높이는가?
- 세션 길이는 플랫폼별로 어떻게 다른가?
- 모바일 전용 기능(예: 카메라 통합)이 필요한가?
UX 개선을 위한 질문들:
- 어떤 화면에서 사용자들이 막히는가?
- 어떤 기능을 가장 자주 사용하는가?
- 어떤 에러가 가장 자주 발생하는가?
이 모든 질문들은 웹 버전만으로는 답할 수 없었던 것들이다.
속도의 진짜 의미
많은 사람들이 "열흘 만에 웹과 앱을 다 만들었다"는 점에 주목한다. 하지만 진짜 중요한 것은 속도 그 자체가 아니다.
진짜 중요한 것은:
- 명확한 가설: 무엇을 검증하고 싶은지 알고 있었다
- 빠른 학습: 실제 데이터로 배우는 것을 우선시했다
- 집중: 검증에 필요한 것만 만들었다
- 반복: 한 번에 완벽하게가 아니라, 빠르게 개선
속도는 결과가 아니라 이런 원칙들의 부산물이다.
다음 단계
앱 스토어에서 테스트가 승인되면:
- 기존 웹 사용자들에게 앱 다운로드 안내
- 사용 패턴 데이터 수집 시작
- 주간 단위로 데이터 분석 및 가설 검증
- 모바일 특화 기능 우선순위 결정
- 공개 런칭 준비
결론: 만드는 것보다 배우는 것
일주일 만에 웹을 만들고, 열흘 만에 앱을 만든 것이 자랑스럽다. 하지만 더 자랑스러운 것은 이제 진짜 질문들에 답할 수 있는 도구를 손에 쥐었다는 것이다.
스타트업은 빠르게 만드는 것이 아니라 빠르게 배우는 것이다. 그리고 배우려면 실제 사용자의 손에 프로덕트를 쥐어줘야 한다.
이제 사용자들이 매일 주머니 속 KnotNet을 열어볼지 지켜볼 차례다. 데이터가 다음 방향을 알려줄 것이다.
그게 린 스타트업의 본질 아닐까.
실수하지 않는 것이 미덕일까: AI와의 협업이 가르쳐준 개발자의 본질
완벽함의 신화
개발자 커뮤니티에는 오랫동안 하나의 이상향이 존재해왔다. 버그 없는 코드를 작성하는 것, 한 번에 완벽한 설계를 하는 것, 실수하지 않는 것. 10x 엔지니어의 이미지는 항상 완벽에 가까운 모습으로 그려졌다.
나 역시 그런 관점을 가지고 있었다. 실수는 부끄러운 것이고, 코드 리뷰에서 지적받는 것은 실력 부족의 증거라고 생각했다. 그래서 코드를 작성할 때 지나치게 신중해지고, 때로는 완벽하지 않으면 시도조차 하지 않는 경향이 있었다.
Claude Code와의 협업에서 발견한 것
KnotNet을 개발하면서 Claude Code를 집중적으로 사용하고 있다. AI 페어 프로그래머와 함께 일하면서 흥미로운 패턴을 발견했다.
Claude Code는 실수를 한다. 때로는 잘못된 API를 사용하고, 때로는 엣지 케이스를 놓치고, 때로는 비효율적인 알고리즘을 제안한다. 하지만 여기서 중요한 차이가 나타난다.
Claude Code의 대응 방식:
- 즉각적인 수용: 지적을 받으면 즉시 인정한다. "아, 맞습니다"라는 반응이 나온다.
- 감정적 반응 없음: 방어하거나, 변명하거나, 자존심 상해하지 않는다.
- 빠른 수정: 문제를 파악하면 즉시 수정 작업에 들어간다.
- 반복적 개선: 한 번에 완벽하지 않아도, 피드백 루프를 통해 결국 제대로 된 결과를 만든다.
이 과정을 관찰하면서 깨달았다. 실수 자체가 문제가 아니라, 실수 이후의 태도와 대응이 관건이라는 것을.
인간 개발자의 실수 대응 패턴
반면 인간 개발자들(나를 포함해서)은 종종 다르게 반응한다:
- 방어적 태도: "그건 제가 의도한 거예요", "이 경우엔 괜찮아요"
- 감정적 반응: 지적받는 것을 개인적인 공격으로 받아들임
- 과도한 설명: 실수의 맥락을 장황하게 설명하며 정당화
- 느린 수정: 자존심이 상해 수정을 미루거나 회피
물론 이런 반응들은 충분히 인간적이다. 우리는 감정을 가진 존재이고, 자신의 작업물에 애착을 느끼며, 타인의 평가에 민감하다. 하지만 이런 감정적 반응이 실제로 우리의 성장과 생산성을 저해하는 것도 사실이다.
새로운 개발자상: 회복력 중심의 역량
AI와의 협업 경험은 개발자의 역량에 대한 새로운 관점을 제시한다.
중요한 것은:
- 실수하지 않는 능력이 아니라
- 실수를 빠르게 인지하고 수정하는 능력
핵심은:
- 완벽한 첫 시도가 아니라
- 빠른 피드백 루프와 반복적 개선
필요한 것은:
- 방어적 태도가 아니라
- 수용적 자세와 학습 마인드셋
실무적 시사점
이런 관점은 실제 개발 문화에도 영향을 미칠 수 있다:
1. 코드 리뷰 문화 지적받는 것을 공격으로 받아들이지 않고, 개선의 기회로 보는 문화
2. 빠른 반복 완벽을 추구하며 시간을 쓰기보다, 빠르게 시도하고 피드백받고 개선하는 사이클
3. 심리적 안전감 실수해도 괜찮다는 환경, 중요한 것은 실수 이후의 대응이라는 인식
4. 학습 중심 마인드셋 실력의 증명보다 지속적인 학습과 성장을 중시하는 태도
결론: 완벽함보다 회복력
아이러니하게도, AI와 함께 일하면서 더 인간적인 개발자가 되는 법을 배우고 있다.
완벽하지 않아도 괜찮다. 실수해도 괜찮다. 중요한 것은 그 이후다.
피드백을 열린 마음으로 받아들이고, 빠르게 수정하고, 계속 개선해 나가는 것. 이것이 AI 시대에 개발자가 가져야 할 진짜 미덕이 아닐까.
Claude Code는 완벽한 코드를 작성하지 않는다. 하지만 완벽한 태도를 보여준다. 그리고 그 태도가 결국 더 나은 결과를 만들어낸다.
우리 인간 개발자들도 그럴 수 있지 않을까? 실수를 두려워하지 않고, 피드백을 환영하며, 빠르게 배우고 성장하는 개발자. 그것이 진짜 10x 엔지니어의 모습일지도 모른다.
인앱 브라우저가 만든 보이지 않는 장벽: KnotNet의 사용자 유입 문제 해결기
데이터에서 발견한 이상 신호
KnotNet을 운영하면서 Google Analytics를 정기적으로 확인하고 있다. 오늘도 여느 때처럼 트래픽 소스별 데이터를 살펴보던 중, 흥미로운 패턴을 발견했다.
YouTube에서 유입된 사용자들은 returning user 비율이 상당히 높았다. 사람들이 한 번 방문한 후 다시 돌아온다는 의미다. 제품에 대한 긍정적인 신호였다.
그런데 LinkedIn과 Threads에서는 상황이 완전히 달랐다. Returning user가 0이었다. 단 한 명도 재방문하지 않았다는 뜻이다.
문제의 원인: 인앱 브라우저의 제약
처음에는 콘텐츠나 타겟팅의 문제일 수도 있다고 생각했다. 하지만 곰곰이 생각해보니 기술적인 문제일 가능성이 높아 보였다.
조사 결과, LinkedIn과 Threads는 링크를 클릭하면 자체 인앱 브라우저를 띄운다는 것을 알게 되었다. 그리고 이 인앱 브라우저들은 보안상의 이유로 Google 로그인을 허용하지 않는다.
KnotNet은 사용자 인증에 Google OAuth를 사용한다. 즉, LinkedIn과 Threads의 인앱 브라우저에서는 사용자들이 아예 로그인할 수 없었던 것이다. 사용자 입장에서는 로그인 버튼을 눌러도 아무 반응이 없거나 에러가 나는 좌절스러운 경험을 했을 것이다.
해결 방법: User Agent 기반 감지와 유도
문제를 파악했으니 이제 해결책이 필요했다. 가장 직접적인 방법은 사용자가 인앱 브라우저를 사용하고 있을 때 이를 감지하고, 외부 브라우저로 열도록 안내하는 것이었다.
구현 방법은 다음과 같다:
- User agent 문자열을 확인하여 LinkedIn, Instagram, Threads 등의 인앱 브라우저인지 판단
- 인앱 브라우저로 판단되면 페이지 상단에 배너를 표시
- 배너에서 외부 브라우저(Safari, Chrome 등)로 열 것을 안내
이 방식의 장점은 사용자에게 명확한 가이드를 제공하면서도, 일반 브라우저 사용자에게는 전혀 영향을 주지 않는다는 것이다.
결과와 인사이트
배너를 구현하고 배포한 후, LinkedIn과 Threads에서의 유입이 시작되었다. 데이터가 다시 정상적으로 기록되기 시작한 것이다.
이번 경험을 통해 몇 가지 중요한 교훈을 얻었다:
1. 데이터는 항상 이유가 있다 숫자의 이상 패턴을 발견했을 때, 단순히 넘어가지 않고 원인을 추적하는 것이 중요하다.
2. 플랫폼의 제약을 이해해야 한다 각 소셜 미디어 플랫폼은 자체적인 기술적 특성과 제약을 가지고 있다. 멀티 플랫폼 전략을 가져갈 때는 이를 반드시 고려해야 한다.
3. 작은 마찰도 큰 장벽이 된다 사용자 경험에서 작은 불편함도 전환율에 치명적인 영향을 미칠 수 있다. 보이지 않는 장벽을 제거하는 것이 중요하다.
4. 기술적 해결책은 간단할 수 있다 복잡한 문제처럼 보여도, 해결책은 의외로 간단할 수 있다. User agent 확인과 배너 하나로 문제를 해결할 수 있었다.
프로덕트를 만들다 보면 이렇게 예상치 못한 문제들을 마주하게 된다. 중요한 것은 데이터를 주의 깊게 관찰하고, 문제의 본질을 파악하며, 실용적인 해결책을 찾는 것이다.
KnotNet의 여정은 계속된다. 다음에는 또 어떤 흥미로운 문제와 마주하게 될까?
AI 도입 100%가 어려운 이유
AI를 통해 프로젝트를 100% 자동화하고 싶다는 목표는 기술적으로 가능한 시대에 접어들었지만, 인간의 감정과 이해관계라는 본질적인 장애물 때문에 여전히 현실화되기 어렵다. 인간은 논리적으로 최선인 선택에 직면했을 때도 자신의 감정적 반응과 본능에 따라 행동하기 때문이다. 이 점은 감정이 단순히 비효율을 초래하는 요소가 아니라, 의사결정 과정에서 중요한 역할을 한다는 점 때문이다.
실제로 안토니오 다마지오가 그의 저서 ‘데카르트의 오류’에서 소개한 사례는 이를 잘 보여준다. 전두엽 손상으로 감정적 신호를 처리하지 못하게 된 환자는 논리적 사고와 인지 능력에는 문제가 없었지만, 작은 선택조차 하지 못하는 상황에 빠졌다. 그는 점심 메뉴를 고르는 것처럼 단순한 결정에서도 다양한 옵션의 장단점을 분석하는 데 몰두하며 결정을 내리지 못했다. 이 사례는 감정이 없으면 인간이 결과의 가치를 평가하거나 우선순위를 정하기 어려워진다는 것을 명확히 보여준다.
그러나 프로젝트와 같은 업무 환경에서는 이러한 감정적 판단이 긍정적인 역할보다는 장애물로 작용할 가능성이 크다. 상위 목표가 이미 명확히 정의된 상황에서 인간의 감정적 개입은 불필요한 갈등이나 비효율성을 초래하기 때문이다. AI는 감정을 가지지 않기 때문에 논리적이고 객관적인 판단을 통해 생산성을 극대화할 수 있다. 반면, 인간은 같은 문제를 두고도 이해관계와 감정적 반응으로 인해 갈등을 빚고 비효율적인 결정을 내릴 수 있다.
이러한 맥락에서 자율주행 기술의 도입 과정이 떠오른다. 기술적으로 완벽에 가까운 시스템이 마련되었지만, 도로 위 다른 운전자가 인간이라는 점 때문에 완전한 구현이 지연되고 있다. 이처럼 프로젝트 자동화 역시 인간의 감정적 특성이 개입되면서 AI의 전면적 도입을 방해하고 있는 것이다.
따라서 AI를 통해 생산성을 극대화하려면 감정적 판단을 최소화할 수 있는 환경을 조성하는 것이 필수적이다. AI는 모든 판단과 실행을 담당하고, 인간은 상위 목표를 설정하거나 가치를 기반으로 방향을 제시하는 데만 집중하는 구조가 필요할 수도 있다. 감정은 가치를 정의하고 목표를 설정하는 데 필수적일 수 있지만, 구체적인 업무 과정에서는 오히려 방해물이 될 가능성이 크기 때문이다.
안토니오 다마지오의 사례는 인간의 감정이 얼마나 중요한 역할을 하는지 잘 보여주지만, 동시에 그것이 잘못된 순간에 개입되었을 때 얼마나 비효율적인 결과를 초래할 수 있는지도 경고한다. 궁극적으로 AI와 인간의 조화는 감정의 적절한 분리와 통합을 통해 이루어질 것이다. 인간은 가치를 정하고, AI는 그것을 실행한다는 역할 분담이 생산성의 극대화를 실현할 수 있는 열쇠가 될 것이다.
https://craft.morethanair.com/YG4hgJJJI45mg3