우리 가족 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. 채팅창 투명도 — 캐릭터를 가리지 않게

가운데서 춤추는 마스코트가 잘 보이도록 채팅 말풍선을 여러 단계로 다듬었다.

  1. 반투명 배경 + 블러 (rgba(.., 0.82))
  2. 더 투명하게 (0.55 → 0.32)
  3. 결국 말풍선 배경을 아예 없애고, 텍스트에 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

같은 와이파이에서는 잘 되는데 막상 폰에서 접속이 안 되는 문제가 있었다. 원인은 두 가지였다.

  1. Windows 방화벽이 외부 기기의 접속을 막고 있었음 → 인바운드 규칙 추가로 해결
    New-NetFirewallRule -DisplayName "Jaechanki 8000" -Direction Inbound -Protocol TCP -LocalPort 8000 -Action Allow
    
  2. 애초에 “같은 와이파이에서만 되는 건 의미가 없다”는 지적에 Tailscale을 도입 — 폰에 Tailscale 앱만 깔면 어느 네트워크에 있든 개인 IP로 접속 가능해졌다.

추가로 폰에서 화면 위아래가 잘리는 문제도 있었는데, 모바일 브라우저 주소창 때문에 100vh가 실제 화면 크기와 어긋나는 게 원인이었다. 100dvh(동적 뷰포트 단위)로 바꾸고 노치/제스처바 영역에 env(safe-area-inset-*) 여백을 줘서 해결.

배운 점

  • AI 이미지 생성은 스타일 선택이 품질뿐 아니라 “오염도”에도 영향을 준다. 같은 모델이라도 프롬프트 스타일에 따라 워터마크가 박힌 학습 데이터 클러스터를 피할 수 있다는 걸 배웠다.
  • 개발 중인 PWA에는 캐시 우선 전략이 독이다. 네트워크 우선 전략으로 바꾸자 모든 수정이 즉시 반영됐다.
  • “가족용”이라도 인증은 필요하다. 아이들이 각자 다른 대화 기록을 가지니 로그인 구분이 생각보다 중요했다.
  • Tailscale 같은 도구 하나면 집 밖에서도 내 서버에 안전하게 접속할 수 있다. 공인 IP나 포트포워딩 설정 없이도.
  • 하루 만에 만든다고 해서 대충 만든 게 아니다. 작은 스코프 + 잘 갖춰진 도구(Claude Code, FastAPI, 로컬 SD)면 하루로도 충분히 쓸 만한 가족 서비스를 만들 수 있다.

아이들이 아침에 “재찬키야” 부르는 모습을 보니, 만든 보람이 확실히 느껴진다. 다음엔 Claude API 키를 받아서 폴백을 실제로 활성화하고, 아이들 대화 데이터로 캐릭터 성격을 더 다듬어볼 계획이다.