4.0 전체 로드맵
| 단계 | 작업 | 시간 |
|---|---|---|
| 4.1 | 사전 준비 체크리스트 (환경 확인) | 5분 |
| 4.2 | 앱 설계 — 화면, 데이터 모델, API 규약 | 10분 |
| 4.3 | Claude Code로 프로젝트 뼈대 구성 | 5분 |
| 4.4 | Cloudflare 백엔드 구축 (Worker + D1 + OpenRouter) | 15분 |
| 4.5 | 모바일 앱 UI 제작 (채팅, 음성, 일일 로그) | 20분 |
| 4.6 | Expo Go로 진짜 휴대폰에서 테스트 | 5분 |
| 4.7 | 배포: 백엔드는 Cloudflare에, 앱은 APK 빌드 후 휴대폰에 직접 설치 | 10분 |
| 4.8 | 마무리 & 확장 아이디어 | 5분 |
텍스트 답변은 nemotron-3.5-lightning:free, 음성 받아쓰기는 오디오 입력을 지원하는 별도 무료 모델을 사용합니다.
4.1 사전 준비 체크리스트 (워크숍 전에 공유)
모두가 준비해야 할 것:
- 노트북 (Windows, Mac, Linux)에 다음 설치:
- Node.js 24 LTS — nodejs.org (터미널에서
node -v로 확인,v24.x권장) - Git — git-scm.com
- VS Code (추천 에디터) — code.visualstudio.com
- Claude Code — 터미널에서
npm install -g @anthropic-ai/claude-code실행 후claude입력하고 로그인
- Node.js 24 LTS — nodejs.org (터미널에서
- 휴대폰 (iOS 또는 Android)에 Expo Go 설치 (App Store / Google Play)
- 계정 (모두 무료):
- Cloudflare — dash.cloudflare.com/sign-up
- OpenRouter — openrouter.ai → API 키 생성 (무료 모델
nvidia/nemotron-3.5-lightning:free사용, 별도 충전 불필요) - Expo — expo.dev/signup
- 두 기기가 같은 Wi-Fi에 연결 (Expo Go 실시간 미리보기 필수)
node -v와 npm -v로 버전을 확인하고, 만약 Node 23을 사용 중이라면 Node 24로 바꾸세요.
node -v
npm -v
# 권장 예시: v24.x / npm 11.x
nvm을 사용한다면 운영체제에 맞는 명령으로 Node 24를 설치하고 기본 버전으로 지정합니다:
# macOS / Linux (nvm)
nvm install 24
nvm use 24
nvm alias default 24
node -v
npm -v
# Windows PowerShell (nvm-windows)
nvm install 24
nvm use 24
node -v
npm -v
nvm이 없다면 macOS/Linux는 nvm, Windows는 nvm-windows를 먼저 설치하거나 Node.js 공식 사이트에서 Node 24 LTS 설치 파일을 사용하세요.
4.2 1단계 — 코드보다 설계 먼저 (10분)
화면 (3개만 — 최소한으로)
- 채팅/기록 화면 (홈) — 채팅 인터페이스. 각 메시지가 기록(텍스트 또는 음성)이며, AI의 답변이 그 아래에 표시됩니다.
- 일일 로그 화면 — 달력 날짜별 보기: 오늘의 모든 기록이 시간 순서대로 나열되고, 상단에 메타데이터(날짜, 요일, 날씨, 위치, 기록 수)가 표시됩니다.
- 히스토리 화면 — 지난 날들의 목록. 탭하면 그날의 로그를 열 수 있습니다.
데이터 모델 (워크숍 전체를 위해 테이블 하나면 충분)
CREATE TABLE entries (
id TEXT PRIMARY KEY, -- uuid
user_id TEXT NOT NULL, -- 워크숍용 간단한 기기 ID
day TEXT NOT NULL, -- '2026-08-24' (현지 날짜)
created_at TEXT NOT NULL, -- ISO 타임스탬프
type TEXT NOT NULL, -- 'text' | 'voice'
content TEXT NOT NULL, -- 기록된 생각 (음성은 받아쓰기된 텍스트)
ai_reply TEXT, -- LLM의 답변
weather TEXT, -- '{"temp_c": 24, "condition": "Clear"}'
city TEXT,
lat REAL, -- 위도
lon REAL -- 경도
);
CREATE INDEX idx_entries_day ON entries(user_id, day);
API 규약 (앱과 백엔드의 약속)
| 엔드포인트 | 메서드 | 요청 → 응답 |
|---|---|---|
/log | POST | { userId, type, content?, audioBase64?, audioFormat?, city?, lat?, lon? } → { entry, aiReply } |
/log/today | GET | ?userId=&day?= → { day, weather, city, lat, lon, entries: [...] } |
/log/history | GET | ?userId= → { days: [{ day, count }] } |
활동 (2분): 참가자가 종이에 채팅 화면을 스케치합니다. 2~3개를 공유하세요. 못생긴 스케치가 제일 좋습니다.
4.3 2단계 — Claude Code로 프로젝트 뼈대 잡기 (5분)
두 개의 프로젝트를 담은 폴더를 만듭니다 — app(Expo)와 server(Cloudflare Worker):
mkdir mindlog && cd mindlog
# 1. 모바일 앱 — 현재 Expo Go와 맞는 SDK 54 고정
npx create-expo-app@latest app --template blank-typescript@sdk-54
# 2. 백엔드 — create-cloudflare 생성기 대신 검증된 최소 구성
mkdir server && cd server
npm init -y
npm install --save-dev wrangler@latest typescript
mkdir src
cd ..
# 3. git 초기화 (Claude Code가 git을 가장 잘 활용합니다)
git init
이제 이 폴더에서 Claude Code를 실행하고 마스터 프롬프트를 입력합니다:
claude
We are building "MindLog", a mobile logging app for a beginner workshop.
Structure: ./app is an Expo (React Native, TypeScript) app. ./server is a
Cloudflare Worker (TypeScript).
The app lets a user log thoughts/ideas/facts by text or voice in a chat UI.
Each entry gets an AI reply. Entries are grouped per day and stored with
metadata (date, time, weather, city, lat, lon).
Backend endpoints: POST /log, GET /log/today, GET /log/history, GET /health.
The Worker calls OpenRouter model nvidia/nemotron-3.5-lightning:free for text
replies. For audio input it uses the separate audio-capable model
nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free. It calls Open-Meteo only
for weather; the Expo app obtains the city with expo-location reverseGeocodeAsync
and sends city to the Worker. Entries are stored in D1 with server/schema.sql.
Add MOCK_AI support: when server/.dev.vars has MOCK_AI="true", return a
deterministic fake transcript and reply so the complete local D1/API flow can
be tested without an API key or model availability.
First, only create the plan as TODO.md with small, ordered tasks. Do not
write code yet.
server 폴더를 기준으로 비교하세요. 이 스타터는 D1·CORS·텍스트·음성 mock·오늘 조회·히스토리를 로컬 통합 테스트했습니다.
✅ 체크포인트: mindlog/ 폴더에 app/, server/, 그리고 여러분이 읽고 승인한 TODO.md가 있어야 합니다.
4.4 3단계 — 백엔드 만들기 (15분)
3a. 로컬 D1 설정 및 스키마 적용
1) 아래 schema 내용을 확인합니다.
CREATE TABLE IF NOT EXISTS entries (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
day TEXT NOT NULL,
created_at TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'voice')),
content TEXT NOT NULL,
ai_reply TEXT,
weather TEXT,
city TEXT,
lat REAL,
lon REAL
);
CREATE INDEX IF NOT EXISTS idx_entries_day
ON entries(user_id, day);
2) 위 내용을 server/schema.sql 파일로 저장합니다.
3) server/wrangler.jsonc에 다음 설정을 저장합니다. 로컬 테스트에서는 원격 D1을 만들 필요가 없습니다.
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "mindlog-api",
"main": "src/index.ts",
"compatibility_date": "2026-08-25",
"d1_databases": [
{
"binding": "DB",
"database_name": "mindlog-db",
"database_id": "00000000-0000-0000-0000-000000000000",
"preview_database_id": "mindlog-db-local"
}
],
"vars": {
"OPENROUTER_MODEL": "nvidia/nemotron-3.5-lightning:free",
"OPENROUTER_AUDIO_MODEL": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free"
}
}
그다음 --local을 명시해 로컬 데이터베이스에만 스키마를 적용합니다:
cd server
npx wrangler d1 execute mindlog-db --local --file=./schema.sql
3b. 로컬 비밀 파일 추가 (절대 커밋하지 마세요!)
server/.dev.vars 파일을 만들고 먼저 mock 모드로 시작합니다:
OPENROUTER_API_KEY="replace-with-your-openrouter-key"
MOCK_AI="true"
server/.gitignore에 .dev.vars*, .env*, .wrangler/를 추가하세요. wrangler secret put은 배포된 Worker용이므로 로컬 실행에는 사용하지 않습니다.
3c. Worker 구현
아래 프롬프트 전체를 한 번에 복사해서 Claude Code에 입력하세요:
Implement task: the Worker. Requirements:
- POST /log: body { userId, type, content?, audioBase64?, audioFormat?, city?, lat?, lon? }
1. audioBase64가 있으면 OpenRouter chat/completions에 input_audio 형식으로 보내고 OPENROUTER_AUDIO_MODEL로 받아쓰기를 요청합니다. audioFormat 기본값은 m4a입니다. 텍스트 답변 모델과 음성 모델을 혼용하지 않습니다.
2. Open-Meteo로 날씨를 가져옵니다 (https://api.open-meteo.com/v1/forecast?latitude=..&longitude=..¤t=temperature_2m,weather_code). 도시명은 요청 body의 city를 저장합니다. 날씨 실패 시 null로 계속 진행합니다.
3. 모델에게 기록에 대한 짧고 따뜻하고 도움이 되는 답변(2-3문장)을 요청합니다.
4. D1에 기록을 INSERT하고 { entry, aiReply }를 반환합니다. 위치(lat, lon)도 함께 저장합니다.
- GET /log/today?userId=&day?= : 지정일 또는 오늘의 기록을 created_at 순서로 반환하고, 날짜 메타데이터(날짜, 요일, 첫 기록의 날씨, 위치)도 함께 줍니다.
- GET /log/history?userId= : 날짜별 기록 개수 목록을 반환합니다.
- GET /health : { ok: true }를 반환합니다.
- Expo 앱이 호출할 수 있도록 permissive CORS 헤더를 추가합니다.
- env.MOCK_AI === "true"이면 OpenRouter를 호출하지 않고 고정된 transcript와 aiReply를 반환합니다.
- JSON 파싱, userId/type/content 누락, OpenRouter 오류에 각각 400/502 JSON 오류를 반환합니다.
- 초보자를 위해 전체 Worker를 src/index.ts 하나에 넣고, 충분히 주석을 답니다.
Worker 내부의 핵심 LLM 호출은 이렇게 생겼습니다 (Claude Code가 전체 파일을 생성할 것입니다):
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${env.OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: env.OPENROUTER_MODEL, // nvidia/nemotron-3.5-lightning:free
messages: [
{ role: "system", content: "You are MindLog, a warm journaling companion. Reply in 2-3 sentences." },
{ role: "user", content: entryText },
],
}),
});
const data = await res.json();
const aiReply = data.choices[0].message.content;
3d. 타입·로컬 D1·API 순서로 테스트
npx wrangler types
npx tsc --noEmit
npx wrangler dev # http://localhost:8787 에서 실행
새 터미널에서:
curl http://localhost:8787/health
curl -X POST http://localhost:8787/log \
-H "Content-Type: application/json" \
-d '{"userId":"demo","type":"text","content":"아이디어: 스스로 만드는 워크숍 앱","city":"서울","lat":37.5665,"lon":126.9780}'
curl "http://localhost:8787/log/today?userId=demo"
curl "http://localhost:8787/log/history?userId=demo"
먼저 MOCK_AI="true"에서 JSON 응답과 D1 저장을 확인합니다. 이후 실제 OpenRouter 키를 넣고 MOCK_AI="false"로 바꾼 다음 Wrangler를 재시작해 실제 AI 응답을 한 번 확인하세요.
✅ 체크포인트: health 200, POST 200 + aiReply, today에 1개 이상, history에 날짜별 count가 반환되어야 합니다. 로컬 D1에는 반드시 --local, 배포 D1에는 반드시 --remote를 사용합니다.
4.5 4단계 — 모바일 앱 UI 제작 (20분)
app/ 폴더에서 작업합니다. Claude Code에게 한 번에 하나씩 다음 프롬프트를 주고, 매번 테스트하세요:
프롬프트 1 — 채팅 화면:
In ./app, build the home screen: a chat UI for logging. - An inverted FlatList of bubbles: user entries on the right, AI replies on the left, each with a small timestamp. - A bottom input bar: multiline TextInput, a Send button, and a mic button. - On send, POST to our Worker (put the base URL in a config constant API_URL) with userId, type: "text", content, city, lat, and lon; append both the entry and the AI reply to the list. Show the server error message when response.ok is false. - Use AsyncStorage to persist a random userId on first launch. - Clean, calm design: off-white background, one accent color.
프롬프트 2 — 음성 기록:
Add voice logging to the mic button using expo-audio:
- Press-and-hold (or tap to start/stop) records an m4a file.
- Read the file as base64 with expo-file-system and POST it to /log as
{ userId, type: "voice", audioBase64, audioFormat: "m4a", city, lat, lon }.
Get lat/lon from expo-location and city from Location.reverseGeocodeAsync.
- Show a red recording indicator and the transcribed text when the reply
arrives. Handle the microphone permission request politely.
프롬프트 3 — 일일 로그 + 히스토리 화면:
Add a tab navigator (@react-navigation/bottom-tabs) with three tabs: 1. "Today" — the chat screen. 2. "Daily Log" — fetches GET /log/today and renders a header card with date, weekday, weather (map WMO weather codes to emoji + label), city, lat/lon location, and entry count, then a timeline of entries. 3. "History" — fetches GET /log/history, lists past days, and tapping a day shows that day's entries (read-only).
Claude Code가 요청한 패키지를 설치합니다:
cd app
npx expo install expo-audio expo-location expo-file-system \
@react-navigation/native @react-navigation/bottom-tabs \
react-native-screens react-native-safe-area-context \
@react-native-async-storage/async-storage
✅ 체크포인트: 시뮬레이터/웹 미리보기에서 기록을 입력하면 AI 답변이 나타납니다.
4.6 5단계 — 진짜 휴대폰에서 실행 (5분)
터미널 1에서 Worker가 휴대폰에서도 보이도록 실행하고, 앱의 API_URL에는 localhost가 아닌 노트북의 LAN IP를 넣습니다:
cd server
npx wrangler dev --ip 0.0.0.0
# 예: API_URL = "http://192.168.0.23:8787"
터미널 2에서 Expo를 시작합니다:
cd app
npx expo start --tunnel
- 터미널에 QR 코드가 나타납니다
- iPhone: 카메라 앱으로 스캔 · Android: Expo Go 안에서 스캔
- 앱이 휴대폰에 로드됩니다. 텍스트 기록과 음성 기록을 남긴 뒤, 일일 로그 탭을 열어보세요.
| 증상 | 해결책 |
|---|---|
| "Network response timed out" | 휴대폰과 노트북이 같은 Wi-Fi여야 합니다. --tunnel 유지 |
| 휴대폰에서 API 호출 실패 | 휴대폰에는 localhost:8787이 없습니다. 노트북의 LAN IP를 쓰고 OS 방화벽에서 Node/Wrangler 수신을 허용하세요. 교실 Wi-Fi가 기기 간 통신을 막으면 다음 단계에서 Worker를 먼저 배포하세요. |
| 마이크가 반응 없음 | 권한 요청 대화상자를 수락하세요. 웹이 아닌 실제 기기에서 테스트해야 합니다 |
4.7 6단계 — APK 빌드해서 휴대폰에 직접 설치 (10분)
6a. 백엔드 배포 (2분)
cd server
npx wrangler login
npx wrangler d1 create mindlog-db
# 출력된 database_id를 wrangler.jsonc의 0000... 자리와 교체
npx wrangler d1 execute mindlog-db --remote --file=./schema.sql
npx wrangler secret put OPENROUTER_API_KEY
npx wrangler deploy
# ✓ Published mindlog-api https://mindlog-api.<당신의-서브도메인>.workers.dev
--remote가 빠지면 로컬 DB만 초기화되어 배포 후 no such table: entries가 납니다. 배포가 끝나면 앱의 API_URL을 이 주소로 업데이트하고 같은 curl POST를 배포 URL에도 한 번 실행하세요.
6b. EAS로 APK 빌드하기 (5분)
cd app
npm install -g eas-cli
eas login
eas build:configure
eas update:configure
eas build --platform android --profile preview # 설치 가능한 APK 생성
빌드는 클라우드에서 실행됩니다 — Android Studio도, Mac도 필요 없습니다. 빌드가 진행되는 동안(10~20분) 수업을 이어가세요. APK 링크가 이메일이나 EAS 대시보드에 도착하면, 휴대폰에서 해당 링크를 열어 APK를 다운로드하고 직접 설치하세요. Android에서는 "출처를 알 수 없는 앱" 설치를 허용해야 할 수 있습니다.
6c. 즉시 업데이트 (2분)
eas update --channel preview --message "workshop day 1"
eas update:configure 후 만든 preview APK만 preview 채널의 업데이트를 받습니다. 색상이나 텍스트 한 줄을 바꾸고 게시한 뒤 앱을 완전히 종료·재실행하면 업데이트가 다운로드되고, 한 번 더 재실행할 때 적용됩니다.
6d. 실제 스토어 출시는 이렇게 합니다 (워크숍 이후, 개념 설명용)
이 워크숍에서는 APK 직접 설치까지만 진행하지만, 실제 상용 배포를 원한다면 다음 과정을 거칩니다:
- Apple Developer 계정(연 $99) 및/또는 Google Play 계정(일회 $25) 가입
eas build --platform all --profile production으로 프로덕션 바이너리 생성- 간단한 개인정보 처리방침 작성 (Cloudflare Pages에 무료로 호스팅) — 오디오를 녹음하는 앱은 필수입니다
eas submit --platform ios/eas submit --platform android로 스토어에 제출- 스토어 심사 질문에 답변 (데이터 수집, 마이크 사용 목적 등)
- 심사 통과 후 공개 출시
4.8 마무리 & 확장 아이디어 (5분)
- 모바일 앱 = 설치형, 기기 기능 활용, 스토어 배포; 웹 앱 = 즉시 접속, 보편적, 브라우저 기반
- 크로스플랫폼 프레임워크(React Native + Expo)로 두 휴대폰 플랫폼을 하나의 코드로 커버
- 배포는 파이프라인: 빌드 → 제출 → 심사 → 출시, 거기에 OTA 업데이트로 속도를 더함
- 작은 서버리스 백엔드(Cloudflare Worker + D1)가 비밀을 안전하게 지키고 이 규모에서는 비용이 전혀 들지 않음
- LLM 게이트웨이(OpenRouter)가 AI 모델을 교체 가능한 설정값으로 만듦 — 한 번의 결정이 아닌 선택
숙제로 해볼 확장 아이디어:
- 매일 밤 9시 푸시 알림: "오늘은 어떤 생각을 했나요?" (
expo-notifications) - Cloudflare Cron Trigger로 매일 AI가 쓴 일기 요약을 이메일로 보내기
- 기록별 감정 감지, 주간 감정 차트
- 과거 기록 의미 검색 ("그때 그 아이디어가 언제였더라...?")