10일 만에 토플 단어장 앱 만들어서 App Store에 출시한 이야기
App Store에서 피카보카 다운로드
왜 만들었나
토플 공부를 하면서 두꺼운 영단어 책을 들고 다니기 귀찮았다. 길에서, 카페에서 모르는 영단어가 보이면 사진만 찍어서 바로 단어장에 저장하고 싶었다. 딱 그런 앱이 없어서 직접 만들었다.
기술 스택
| 영역 | 기술 |
|---|---|
| 프론트엔드 | React Native (Expo) + TypeScript |
| 스타일링 | NativeWind (Tailwind CSS) |
| 백엔드 | Supabase (Auth, DB, Edge Functions) |
| OCR/Vision | Claude Haiku 4.5 (Anthropic API) |
| 인앱결제 | RevenueCat (react-native-purchases) |
| 에러 모니터링 | Sentry |
| 개발 도구 | Claude Code (AI 페어 프로그래머) |
타임라인
| 날짜 | 한 일 |
|---|---|
| Day 1 (3/6) | 프로젝트 세팅, 소셜 로그인, 덱 CRUD, 스와이프 카드 학습, NativeWind 마이그레이션 |
| Day 2 (3/7) | 카메라 스캔(OCR), 알림, TTS, 테마 전환, Apple 로그인 |
| Day 3 (3/8) | 인앱결제(RevenueCat), 프리미엄 제한, 학습 모드, 틀린 단어 복습 |
| Day 4 (3/9) | Vision API 교체 삽질, EAS 빌드, 앱 아이콘, 첫 번째 빌드 제출 |
| Day 5 (3/10) | 튜토리얼 오버레이, Sentry 연동 |
| Day 6 (3/11) | 다국어 시도 → 롤백, SM-2 복습 탭, 스펠링 모드 |
| Day 7 (3/12) | 덱 코드 공유 기능, 코드 정리 |
| Day 8 (3/13) | Sentry 빌드 에러 해결, 스캔 이미지 5MB 이슈 해결 |
| Day 9 (3/14) | max_tokens 버그 수정, Apple Sign In 수정, 심사 제출 |
| Day 10~ (3/17) | Apple Sign In 재수정, 심사 승인, App Store 출시 |
트러블슈팅
Vision API 삼세번 — OCR 엔진 방황기
카메라로 찍은 사진에서 영단어를 추출하는 게 핵심 기능이었다. 처음에는 Claude Haiku로 구현했는데, "다른 모델이 더 빠르지 않을까?" 하는 생각에 OpenAI로 갈아탔다.
Claude Haiku → OpenAI GPT-4.1 Mini → GPT-5 mini/nano → Claude Haiku 복귀.
OpenAI 쪽이 느리고 부정확해서 결국 원점으로 돌아왔다.
const claudeRes = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-haiku-4-5-20251001",
max_tokens: 4096,
messages: [{
role: "user",
content: [
{
type: "image",
source: { type: "base64", media_type: "image/jpeg", data: image },
},
{
type: "text",
text: `Extract all English words from this image...`,
},
],
}],
}),
});교훈: 잔디 밟지 말고 처음부터 잘 되던 걸 쓰자.
Expo Go vs Dev Build
인앱결제를 위해 react-native-purchases를 설치했더니 Expo Go에서 앱이 아예 안 뜬다.
네이티브 모듈이라 Expo Go가 지원을 못 하는 거였다.
동적 import로 우회를 시도해봤지만 근본적인 해결이 안 돼서, 결국 Dev Build 체제로 전환했다.
npx expo start --dev-client이후로는 네이티브 모듈을 자유롭게 쓸 수 있게 됐다. Expo Go의 편리함은 잃었지만 인앱결제, 푸시 알림 등을 위해선 필수적인 선택이었다.
Sentry 연동 → 프로덕션 빌드 폭발
에러 모니터링을 위해 Sentry를 붙였는데, 개발 빌드에서는 잘 되다가 프로덕션 빌드에서 터졌다.
// App.tsx
Sentry.init({
dsn: "...",
enabled: !__DEV__,
tracesSampleRate: 1.0,
});원인은 Metro serializer 설정 충돌이었다.
해결하고 나니 또 소스맵 업로드에서 에러.
결국 SENTRY_DISABLE_AUTO_UPLOAD=true를 추가해서 해결했다.
Apple Sign In 버튼 3연속 수정
Apple 심사 가이드라인에 맞추려고 Apple 공식 Sign In 버튼을 적용했다. 그런데 다크모드에서 안 예쁘고, 크기 조절도 자유롭지 않아서 커스텀 버튼으로 변경. 그랬더니 아이콘 크기가 안 맞고... 커밋 3개를 Apple Sign In 버튼에만 썼다.
Xcode Capability 캐시 손상
Provisioning Profile에 Sign in with Apple capability가 분명히 있는데, Xcode가 "doesn't include the capability" 에러를 뱉었다. 프로파일을 다시 만들고, 다시 다운받고, Xcode를 재시작해도 안 됐다.
원인은 Xcode 26.3 업데이트 후 capability 캐시 파일이 손상된 것이었다.
# 캐시 확인 — 정상이면 80개 이상이어야 하는데 11개만 있었다
cat ~/Library/Developer/Xcode/UserData/Capabilities/*.json \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('data',[])))"
# 결과: 11
# 해결
rm -rf ~/Library/Developer/Xcode/UserData/Capabilities/삭제 후 Xcode 재시작하니 깨끗하게 해결됐다. 구글링해도 잘 안 나오는 이슈라 반나절을 날렸다.
스캔 이미지 5MB 제한
사용자가 고해상도 사진을 찍으면 base64 인코딩 후 5MB를 넘어서 API 요청이 실패했다. 클라이언트에서 자동 리사이즈 로직을 추가했다.
// expo-image-manipulator로 2048x2048 이하, JPEG 0.8 품질로 압축
const compressed = await ImageManipulator.manipulateAsync(
uri,
[{ resize: { width: 2048, height: 2048 } }],
{ compress: 0.8, format: SaveFormat.JPEG }
);서버 쪽에서도 방어를 추가했다.
const imageSizeBytes = Math.ceil(image.length * 3 / 4);
if (imageSizeBytes > 5 * 1024 * 1024) {
return new Response(JSON.stringify({
error: "이미지가 너무 커요. 더 작은 이미지로 시도해주세요."
}), { status: 400 });
}max_tokens JSON 잘림
스캔으로 단어를 많이 인식하면 Claude 응답의 JSON이 중간에 잘려서 파싱 에러가 났다.
max_tokens가 1024로 설정되어 있던 게 원인.
4096으로 올려서 해결.
스펠링 모드 UI 7연속 수정
스펠링 모드는 단어의 일부 글자를 가리고 사용자가 직접 입력하는 기능이다.
// 글자의 ~15%만 힌트로 보여주는 로직
function generateRevealedIndices(word: string): Set<number> {
const letterIndices: number[] = [];
word.split("").forEach((c, i) => {
if (/[a-zA-Z]/.test(c)) letterIndices.push(i);
});
const revealCount = Math.max(1, Math.round(letterIndices.length * 0.15));
const revealed = new Set<number>([letterIndices[0]]);
// 나머지는 랜덤 공개
}문제는 UI였다. 셀 박스 스타일 → 밑줄 스타일로 바꾸고, 버튼 배치 조정하고, 정답/오답 색상 통일하고, 문구 변경하고... 커밋 7개를 연속으로 스펠링 모드에만 쏟았다. 학습 UX는 사소한 디테일이 사용 경험을 크게 좌우하기 때문에 타협하지 않았다.
기타 삽질들
다국어 지원 시도 → 롤백. i18next + react-i18next + expo-localization으로 다국어 지원을 시도했다. DB 스키마까지 바꿨는데, 출시 일정을 생각하면 scope이 너무 커졌다. 과감하게 롤백하고 한국어 단일 버전으로 먼저 출시하기로 결정.
앱 이름 변경 (ditto → 피카보카). 개발 중간에 리브랜딩했는데, 코드 곳곳에 박혀있는 이름을 바꾸는 건 금방이지만 App Store Connect 설정, 번들 ID 등 외부 설정을 맞추는 게 은근 손이 갔다.
한글 입력 높이 버그. React Native에서 한글을 입력하면 조합 중인 글자 때문에 TextInput 높이가 들쑥날쑥 변하는 이슈. 검색 인풋과 뜻 수정 인풋 두 군데서 동일한 문제가 발생해서 커밋 2개로 잡았다.
sharp 패키지 EAS 빌드 실패. 이미지 처리를 위해 sharp를 넣었더니 EAS 빌드가 깨졌다. Node.js 네이티브 모듈이라 React Native에서 안 돌아간다. 제거하고 expo-image-manipulator로 대체.
SM-2 간격 반복 학습
단순 암기가 아닌 과학적 복습을 위해 SM-2 알고리즘을 간소화해서 적용했다.
const INTERVALS = [1, 3, 7]; // 1일 → 3일 → 7일 → 졸업
export function computeSrs(state: SrsState, quality: number): SrsResult {
let { easeFactor, intervalDays, repetitions } = state;
if (quality >= 3) {
// 정답: 다음 간격으로
intervalDays = INTERVALS[repetitions] ?? INTERVALS[INTERVALS.length - 1];
repetitions++;
} else {
// 오답: 1일차부터 다시
repetitions = 0;
intervalDays = 1;
}
const nextReviewAt = new Date();
nextReviewAt.setDate(nextReviewAt.getDate() + intervalDays);
return { easeFactor, intervalDays, repetitions, nextReviewAt };
}스와이프 오른쪽 = 정답, 왼쪽 = 오답. 3회 연속 정답이면 졸업 처리되어 복습 큐에서 빠진다.
Claude Code를 페어 프로그래머로 쓴 후기
이 앱은 처음부터 끝까지 Claude Code와 함께 만들었다.
자연어로 기능 요청. "카메라로 찍은 사진에서 영단어를 추출하는 Edge Function 만들어줘" 같은 요청을 하면 코드를 생성하고, 파일을 만들고, 배포 명령어까지 알려줬다.
디버깅 파트너. "Sentry 붙였더니 프로덕션 빌드가 안 돼" → Metro serializer 충돌 원인 분석 → 수정 → 빌드 확인까지 같이 진행.
아키텍처 논의. "인앱결제 어떤 구조로 짜면 좋을까?" 같은 질문에 Context + Provider 패턴을 제안하고, 무료/프리미엄 제한을 어디서 체크할지까지 설계.
CLAUDE.md로 컨텍스트 유지. 프로젝트 규칙을 CLAUDE.md 파일에 적어두면 매 대화마다 자동으로 읽어온다. "Edge Function 배포할 때 --no-verify-jwt 필수" 같은 규칙을 한 번 적어두면 다음부터는 알아서 지켜준다.
메모리 기능으로 히스토리 축적. Vision API를 여러 번 바꾼 이력, 트러블슈팅 해결법 등을 메모리에 저장해두면 다음 대화에서도 맥락을 이어갈 수 있었다.
혼자 개발하면서도 "같이 하는 느낌"이 있었다. 특히 삽질할 때 빛을 발했다 — 에러 로그 붙여넣으면 원인 분석부터 수정까지. 단순 코드 생성이 아니라, 의도를 이해하고 구조를 제안하는 수준이다. 퇴근 후 1~2시간 작업으로 10일 만에 출시가 가능했던 건 이 도구 덕분이 크다.
회고
잘한 점.
- scope를 과감하게 줄였다 (다국어 롤백, 한국어 단일 버전으로 출시)
- "일단 출시하자"는 마인드로 완성도보다 속도를 우선했다
- AI 도구를 적극적으로 활용해서 1인 개발의 한계를 넘었다
아쉬운 점.
- Vision API 갈아엎는 데 시간을 낭비했다 (처음부터 벤치마크를 했으면...)
- 스펠링 모드 UI를 7번이나 고친 건 기획이 부족했던 것
- 테스트 코드를 작성하지 못했다
다음에 할 것.
- 다국어 지원 재도전
- Android 출시
- 다음 앱 프로젝트 시작
댓글
불러오는 중...