1. 서론
Claude Code를 이용해서 웹 + 데스크톱 앱 개발한 일지를 기록합니다.
아래 링크에서 관련 코드를 확인할 수 있습니다.
https://github.com/sngmng6506/caffeine-Flow
순서가 바뀐 느낌이지만 개발을 회고하면서 구체적인 구현과정에서 다소 생략하고 넘어간 설계 의미를 찾으려고 하는 목적으로 작성합니다. ㅎㅎ
일단 돌아가도록 구현해놓고 리버스 엔지니어링하는 것처럼 코드와 구조를 파악해나가는 모습이 다소 웃기기도 하면서 앞으로는 흔히 목격되는 모습이지 않을까 하는 생각도 듭니다.
2. 개발 목적
사실 사용자가 QR로 카페에 희망 음악을 신청할수있도록 도움을 주는 앱을 처음 구상한 시점은 한참 거슬러 올라가야한다. 2021년 간단한 프로젝트가 코딩 공부에 도움이 된다는 말에 무엇을 구현할까 생각하던 때가 있었다. 그 무렵은 항상 카페에 상주하면서 진로에 대해 고민하곤 했기 때문에 항상 듣는 카페 음악을 다른 음악으로 바꾸고 싶다는 생각을 자연스럽게 하게되었다. 그리고 해당 기능을 구현하기 위해 인터넷을 찾아보며 공부하기 시작했다.
그 당시의 삽질이 내 깃헙의 첫 프로젝트이며 아직도 비공개 레포로 숨겨져있다.
그때는 포기로 끝났지만 클로드가 있으면 무서울게 없기 때문에 2026년 3월 29일 첫 커밋을 시작으로 다시금 구현해보기 시작했다.
3. 개발 현황
3.1 전체 아키텍처

-
HTTP 프로토콜
- 손님 -> 서버
곡 신청 / 댓글 / 투표
- 사장님 -> 서버
신청곡 수락(또는 거절)/공지 변경 / 설정 변경/통계 조회
-
Websocket 프로토콜
- 손님 -> 서버
접속/접속 해제
- 서버 -> 손님
사장님의 액션에 따라 손님 화면의 "지금 재생 중" 및 대기열, 신청곡 상태가 갱신
투표수 변동
TOP 차트 갱신
사장님 설정이 변경되면 손님 화면에 반영
- 사장님 -> 서버
접속/접속 해제
- 서버 -> 사장님
사장님 기능의 대시보드 업데이트 (신규 신청, 신청 취소, 투표수 변동, 새 댓글)
3.2 손님 화면


3.3 손님 기능
핵심 컨셉: 가입·로그인 없이 QR 한 번 스캔으로 신청·투표·댓글까지 끝나야 한다.
- 링크 입력만으로 신청 — YouTube · SoundCloud · Spotify URL을 받으면 서버가 oembed로 제목·썸네일·채널을 가져온다.
- localStorage UUID로 익명 식별 (x-visitor-id) — 회원가입 없는 환경에서 "같은 사람이 또 투표/신청했는지"를 IP만으로 판별하면 가족·일행 NAT
IP가 묶여서 부당하게 막힘. 그래서 브라우저 UUID + IP 두 차원을 모두 사용.
- 실시간 큐 반영 — Socket.IO /cafe namespace, slug 기준 room. 사장님이 수락하거나 곡이 끝나면 즉시 손님 화면 갱신.
- TOP 차트 2종 — 매장별 TOP10 / 전체 카페 통합 TOP10. 매장 분위기 맞춤 인기곡 + 전체 트렌드 동시 제공.
- 레이트 리미트 이중 키 스택 — visitor_id 헤더는 클라이언트가 매 요청 새로 생성해 우회 가능하므로 visitor(3/min) + IP(10/min) 둘 다 통과해야만 신청 허용.
3.4 사장님 화면

3.5 사장님 기능
핵심 컨셉: BGM은 끊김 없이 흘러가고 신청곡만 위에 덮어쓴다.
- Electron 데스크톱 앱 — 최대한 카페에서 쓰는 환경 그대로 + 신청곡 덮어쓰기 정도만 지원하고싶어서 Electron 을 통해 한켠에 웹을 그대로 구현하는 방식으로 진행함.
- CastLabs wvcus 빌드 (Widevine CDM 내장) — Spotify·일부 SoundCloud DRM 콘텐츠 재생이 필수라 기본 Electron으로는 license 발급 거부.
CastLabs가 빌드해주는 Widevine 포함 Electron + EVS 서명을 받으면 합법적으로 재생 가능.
- BrowserView 2중 구조 (overlay / takeover) — bgmView(매장 BGM, 항상 살아있음) + recView(신청곡, 필요할 때만 attach·destroy). 신청곡 재생
중에도 BGM은 음소거로 백그라운드에서 계속 흐르고, 끝나면 음소거만 해제하면 끊김 없이 이어진다.
- 예외: BGM=Spotify + 신청=Spotify일 때는 Spotify Connect가 한 세션만 허용하므로 bgmView 안에서 신청곡 재생 후 원래 트랙으로 복귀하는 takeover 모드 사용.
- Google + Naver OAuth 회원가입 — 국내 카페 사장님 타깃이라 둘 다 필수.
- 자동 업데이트 — electron-updater + GitHub Releases. 사장님이 직접 인스톨러 다시 받을 일 없음.
- 드래그앤드롭 큐 관리 — 대기곡 순서 변경, 신청 → 대기 이동을 직관적으로.
- 자동수락 토글 — ON이면 추천곡이 자동으로 대기열로 진입.
3.6 서버 및 DB
Node.js + Express 4 — 손님·사장님 클라이언트 모두 JS 기반이라 백엔드도 JS면 컨텍스트 스위칭 비용 ↓. Express는 라우트 구조가 단순하고 미들웨어 생태계가 넓어 OAuth·rate-limit·CORS 같은 보일러플레이트가 다 패키지로 있다.
Postgres (Supabase) — RDB가 필요한 이유: 카페 ↔ 신청곡 ↔ 투표·댓글이 명확한 1:N 관계이고 조인·집계(TOP10, 시간대별 통계)가 핵심 기능. NoSQL이면 집계 쿼리에서 손해. Supabase는 무료 티어 + 매니지드 PG라 인프라 관리 부담 없음.
Knex 3 — SQL ORM 대신 query builder를 선택. ORM(예: Sequelize, Prisma)은 추상화가 깊어서 성능 튜닝·복잡 쿼리 작성 시 결국 raw SQL로 빠지는 경우가 많다. Knex는 SQL을 그대로 쓰면서 마이그레이션·파라미터 바인딩만 받아주는 가벼운 레이어라 학습 비용 낮고 디버깅 쉬움.
Socket.IO 4 — WebSocket 위에 자동 재연결·room·namespace를 얹어줘서 카페별 broadcast가 한 줄. 순수 WS로 직접 구현하면 reconnection·heartbeat·room 다 직접 짜야 함.
JWT (jsonwebtoken) — 세션 저장소 불필요, stateless. Electron 데스크톱 + 모바일 브라우저 둘 다 같은 토큰으로 처리. 단 secret 미설정 시 startup throw로 강제(change-me-in-production 같은 약한 기본값 방지).
Railway 배포 (NIXPACKS) — git push → 자동 빌드·재배포. Heroku 후속격이지만 더 저렴하고 빠름. Postgres는 Supabase에 있고 Railway는 API 호스팅용으로만 쓰니 부담 적음.
스키마 설계 포인트 (마이그레이션 16개):
- recommendations.status 6단계 (pending → accepted → playing → played / rejected / skipped) — 손님 신청 → 사장님 큐 관리 → 재생
라이프사이클을 한 컬럼으로 추적
- cafe_visits에 visit_date UNIQUE — 같은 손님(같은 IP)이 하루에 여러 번 들어와도 1방문으로 dedupe (KST 기준)
- daily_stats.peak_concurrent — 사장님 소켓 제외하고 실제 손님 동시접속 피크만 기록
- recommendations.platform + allowed_platforms — 카페마다 YouTube만 받거나 Spotify는 제외 같은 설정 가능
3.7 트러블 슈팅
Spotify Widevine DRM 재생 불가 → CastLabs Electron 통째 교체
증상: 일반 Electron으로 Spotify 웹플레이어 띄우면 license 발급 거부 → 음악 안 들림.
여정 (v2.1.2 → v2.2.2, 약 3주):
- cd4cd45 : Chrome의 Widevine CDM 자동 로드 시도 → 부분 작동
- 53aed33 : 결국 CastLabs electron-41.5.0+wvcus 빌드로 전환 (Widevine 내장)
- f15ce6b : autoplay-policy 플래그 제거 — Spotify SDK 비동기 play() 차단 해제
- f171832 : components.whenReady() 대기 — CDM 초기화 후 창 생성
- 8da4bb9 : DRM 권한 핸들러 추가 — Widevine 라이선스 요청 자동 허용
- a84469f : CastLabs EVS 서명 통합 — afterPack 훅으로 매 빌드 검증
- 브라우저에서 잘 되는 게 Electron에서는 안 될 수 있다. 특히 DRM은 별도 빌드/서명 인프라가 필요.
Dual BrowserView — BGM과 신청곡 분리 아키텍처
증상: 신청곡 재생할 때마다 BGM이 처음부터 다시 시작되거나 끊김.
핵심 커밋: 689be13 feat: Dual BrowserView — 매장 BGM이 신청곡 사이에도 끊김 없이 유지 (v2.0.3)
설계 결정:
- bgmView: 매장 BGM, 항상 살아있음. 신청곡 재생 동안 음소거(setAudioMuted(true))만 토글
- recView: 신청곡 전용, 필요할 때만 attach·destroy
- 신청곡 끝나면 setAudioMuted(false)로 BGM 그대로 이어 재생
파생 트러블:
- e5031ab : backgroundThrottling: false — BrowserView가 뒤에 가려져도 JS 실행 유지 (Spotify 플레이어 일시정지 방지)
- da98120 : setAudioMuted 후 Spotify play 클릭으로 무음 재생 유지 — mute가 pause를 유발하는 버그 우회
4. 향후 계획
5. 피드백.
가게 A ) 과거에 신청곡들을 받았었는데, 가게 일이 가중되어서 현재는 안받고 있음.
-> 자동 수락 기능도 있긴 하지만 그렇게되면 가게 분위기에 안맞는 곡이 들어올 수도 있음
-> LLM을 통해 한번 필터링 하는건 어떨까?