우리 가족 AI 비서 '재찬키' 만든 이야기 — 아이 이름을 딴 가족 전용 AI 비서
첫째 재희, 둘째 찬희의 이름을 합쳐서 “재찬키”. 두 아이 이름을 딴 우리 가족 전용 AI 비서를 하루 만에 웹앱으로 만들어봤다. Claude Code와 함께 기획부터 캐릭터 디자인, 배포, 원격 접속 문제 해결까지 겪은 과정을 기록해둔다.
무엇을 만들었나
- 이름: 재찬키 (재희 + 찬희)
- 형태: 설치 가능한 웹앱(PWA) — 브라우저 주소만 알면 폰 홈 화면에 아이콘으로 설치
- 두뇌: DeepSeek API (메인), 나중에 Anthropic API 키를 발급받으면 Claude로 자동 폴백되는 구조
- 사용자: 가족 구성원별 로그인 + 대화 기록 분리, 동시 접속 가능
- 캐릭터: 로컬 GPU로 직접 생성한 3D 로봇 마스코트, 대화 중엔 화면 가운데서 춤을 춤
- 부가 기능: 음성으로 말하기(STT) / 음성으로 듣기(TTS), 어디서든 접속(Tailscale)
기술 스택
| 영역 | 선택 | 이유 |
|---|---|---|
| 백엔드 | FastAPI (Python) | 기존 프로젝트가 이미 Python 기반, DeepSeek 호출 패턴 재사용 가능 |
| 프론트엔드 | 순수 HTML/CSS/JS (PWA) | Streamlit은 설치형 PWA(매니페스트·서비스워커)에 적합하지 않아서 제외 |
| DB | SQLite | 가족 단위 소규모 사용에 충분, 별도 서버 불필요 |
| 인증 | 서명된 세션 쿠키 | 가족 계정 몇 개 수준이라 가볍게 |
| 캐릭터 이미지 | 로컬 Stable Diffusion 1.5 | 이미 GPU에 세팅되어 있던 로컬 이미지 생성 파이프라인 재사용, 외부 이미지 생성 API 비용 없음 |
| 원격 접속 | Tailscale | 집 와이파이가 아니어도 어디서나 접속 가능하게 |
제작 과정
1. 메인 두뇌 결정 — Claude Pro와 API 키는 별개였다
처음엔 “Claude 메인 + DeepSeek 폴백”으로 기획했는데, Claude Pro 구독이 있으면 API 키도 자동으로 되는 줄 알았던 게 착각이었다. Claude Pro 구독과 Anthropic API 키는 완전히 별도 시스템이라, API를 쓰려면 콘솔에서 별도로 결제수단을 등록해야 한다는 걸 확인. 일단 이미 쓰고 있던 DeepSeek 키를 메인으로 쓰고, 나중에 Claude API 키를 발급받으면 자동으로 폴백에 얹히도록 코드를 설계해뒀다.
# llm.py — provider 순서를 리스트로 관리
PROVIDERS = ["deepseek"] + (["claude"] if ANTHROPIC_API_KEY else [])
키 하나만 .env에 채워 넣으면 이후 코드 수정 없이 폴백이 활성화되는 구조.
2. 캐릭터 이미지 — SD1.5가 몰래 넣은 유령 워터마크
로컬에 이미 세팅되어 있던 Stable Diffusion 1.5 파이프라인으로 마스코트를 생성했다. 처음 뽑은 후보 중 마음에 드는 걸 골랐는데, 자세히 보니 얼굴에 흐릿한 스톡사진 워터마크(Dreamstime류) 유령이 찍혀 있었다.
원인을 찾아보니 SD1.5가 학습한 LAION 데이터셋에 워터마크 박힌 스톡 일러스트가 워낙 많아서, “플랫 벡터 일러스트 / 동화책 스타일” 같은 프롬프트를 쓰면 그 학습 데이터 클러스터를 끌어와 워터마크가 딸려 나오는 고질적인 현상이었다. img2img로 지워보려 했지만 완전히 사라지지 않았고, 결국 프롬프트 스타일을 “3D 픽사 렌더” 계열로 통째로 바꿔서 그 오염된 클러스터를 피하는 방식으로 해결했다.
# Before (워터마크 유발)
"clean vector-ish digital painting, children's book illustration style"
# After (문제 없음)
"3D pixar-style render, glossy plastic toy look, octane render"
이후 rembg로 배경을 투명하게 제거하고 앱 아이콘 세트로 변환.
3. 화면 속 살아있는 펫 — 상태 기반 모션
“에이전트가 작업할 때 캐릭터가 Hermes 펫처럼 움직였으면 좋겠다”는 요청으로 상태 기반 애니메이션을 붙였다.
- 평소(idle): 화면 가운데서 크게(최대 260px) 둥실둥실 부유
- 응답 생성 중(thinking): 4가지 랜덤 댄스 모션 중 하나로 전환 — 매번 같은 동작이 반복되지 않도록
- 심심할 때: 9~17초마다 한 번씩 짧은 “기지개 점프” 모션이 자동 재생
const DANCE_CLASSES = ["dance-1", "dance-2", "dance-3", "dance-4"];
function startDancing() {
const pick = DANCE_CLASSES[Math.floor(Math.random() * DANCE_CLASSES.length)];
centerPet.classList.add(pick);
}
4. 서비스워커가 내 발목을 잡다
PWA용 서비스워커를 “캐시 우선(cache-first)” 전략으로 짜뒀더니, 코드를 수정해도 브라우저가 예전 화면을 계속 보여주는 문제가 반복됐다. 개발 중인 앱에는 캐시 우선 전략이 오히려 독이라는 걸 체감하고, “온라인이면 항상 최신 파일을 받아오고, 오프라인일 때만 캐시로 대체”하는 네트워크 우선 전략으로 바꿨다. 이후로는 새로고침 한 번이면 항상 최신 버전이 반영됐다.
5. 채팅창 투명도 — 캐릭터를 가리지 않게
가운데서 춤추는 마스코트가 잘 보이도록 채팅 말풍선을 여러 단계로 다듬었다.
- 반투명 배경 + 블러 (
rgba(.., 0.82)) - 더 투명하게 (
0.55→ 0.32) - 결국 말풍선 배경을 아예 없애고, 텍스트에 halo 그림자만 남기는 방식으로 정착
.bubble {
text-shadow:
0 0 6px var(--card-bg),
0 0 6px var(--card-bg),
0 0 12px var(--card-bg);
}
배경이 없어도 어떤 색 위에 있든 글자가 또렷하게 읽히면서, 캐릭터는 거의 그대로 다 보이는 균형을 찾았다.
6. 음성 기능 — 별도 API 없이 브라우저 내장 기능으로
“음성으로 말하고 듣기”는 별도 유료 TTS/STT API 없이 브라우저 내장 Web Speech API로 해결했다.
- 🎤 버튼 →
SpeechRecognition으로 음성 인식 → 자동 전송 - 🔊 버튼 → 응답 완료 시
speechSynthesis로 자동 읽어주기 (기기별 on/off 상태 저장)
무료이고, 기기에 내장된 음성이라 저작권 문제도 없다. 다만 iOS Safari는 음성 인식을 지원하지 않아서, 아이폰에서는 🎤 버튼이 자동으로 숨겨지도록 기능 감지 처리를 해뒀다 (듣기는 아이폰에서도 정상 작동).
7. 집 밖에서도 쓰기 — 방화벽과 Tailscale
같은 와이파이에서는 잘 되는데 막상 폰에서 접속이 안 되는 문제가 있었다. 원인은 두 가지였다.
- Windows 방화벽이 외부 기기의 접속을 막고 있었음 → 인바운드 규칙 추가로 해결
New-NetFirewallRule -DisplayName "Jaechanki 8000" -Direction Inbound -Protocol TCP -LocalPort 8000 -Action Allow - 애초에 “같은 와이파이에서만 되는 건 의미가 없다”는 지적에 Tailscale을 도입 — 폰에 Tailscale 앱만 깔면 어느 네트워크에 있든 개인 IP로 접속 가능해졌다.
추가로 폰에서 화면 위아래가 잘리는 문제도 있었는데, 모바일 브라우저 주소창 때문에 100vh가 실제 화면 크기와 어긋나는 게 원인이었다. 100dvh(동적 뷰포트 단위)로 바꾸고 노치/제스처바 영역에 env(safe-area-inset-*) 여백을 줘서 해결.
배운 점
- AI 이미지 생성은 스타일 선택이 품질뿐 아니라 “오염도”에도 영향을 준다. 같은 모델이라도 프롬프트 스타일에 따라 워터마크가 박힌 학습 데이터 클러스터를 피할 수 있다는 걸 배웠다.
- 개발 중인 PWA에는 캐시 우선 전략이 독이다. 네트워크 우선 전략으로 바꾸자 모든 수정이 즉시 반영됐다.
- “가족용”이라도 인증은 필요하다. 아이들이 각자 다른 대화 기록을 가지니 로그인 구분이 생각보다 중요했다.
- Tailscale 같은 도구 하나면 집 밖에서도 내 서버에 안전하게 접속할 수 있다. 공인 IP나 포트포워딩 설정 없이도.
- 하루 만에 만든다고 해서 대충 만든 게 아니다. 작은 스코프 + 잘 갖춰진 도구(Claude Code, FastAPI, 로컬 SD)면 하루로도 충분히 쓸 만한 가족 서비스를 만들 수 있다.
아이들이 아침에 “재찬키야” 부르는 모습을 보니, 만든 보람이 확실히 느껴진다. 다음엔 Claude API 키를 받아서 폴백을 실제로 활성화하고, 아이들 대화 데이터로 캐릭터 성격을 더 다듬어볼 계획이다.