책에서 음악으로, 기준을 정하는 일
“책과 함께 읽을 플레이리스트가 있으면 좋겠다”는 아이디어를 들었다. 책과 음악을 매칭하는 것. 뭔가 만들어볼 만한 느낌이 들어서 바로 시작했다.
API는 문서 대신 직접 테스트
책 데이터 후보가 여럿 있었다. 카카오, 네이버, 알라딘, Google Books. 문서만 보면 다 비슷해 보이는데, 직접 뽑아보니까 차이가 명확했다.
기준은 하나였다. LLM이 분위기를 뽑기에 충분한 소개글을 주는가. 카카오는 250자에서 잘렸다. 알라딘은 카테고리까지 줬지만 수동 승인이라 즉시 시작이 안 됐다. 네이버는 773~921자 — 줄거리와 사회적 맥락까지 들어있었다. 네이버로 결정.
음악 쪽은 Spotify Recommendations API를 먼저 봤는데, 2024년 11월에 deprecated됐다는 걸 직접 호출해서 확인했다. 문서 공지는 있었지만 정말 막히는지는 테스트해봐야 알 수 있었다.
대안으로 Deezer, Last.fm, Spotify 플레이리스트 검색을 각각 테스트했다. Deezer는 복합 쿼리가 안 됐다. Last.fm은 태그 정확도가 높았다. “dark ambient” 태그로 검색하면 진짜 dark ambient가 나왔다. Spotify 플레이리스트는 큐레이션 품질이 좋았다.
Spotify 플레이리스트 + Last.fm 트랙 조합으로 결정했다. 플레이리스트(분위기 단위)와 개별 트랙(곡 단위) 두 층으로 제공하면 더 풍부해지기도 하고, 각자의 강점이 보완된다.
Claude 프롬프트는 출력 형식부터
책 소개글 → 음악 검색 쿼리 변환이 핵심 레이어였다. Claude API를 쓰기로 했는데, 프롬프트 설계에서 한 번 틀렸다.
처음에 “책의 분위기를 표현해줘”라고 했더니 “어둡고 철학적, 내면적” 같은 형용사가 나왔다. 그걸 Spotify에 던졌더니 결과가 엉망이었다. 자연어 형용사와 음악 검색 쿼리는 다른 영역이다.
역으로 생각했다. 다음 단계(Spotify, Last.fm)에서 어떤 형식의 입력을 받아야 제대로 동작하는지를 먼저 정의하고, 거기에 맞게 프롬프트를 바꿨다. “음악 장르 키워드로 표현해줘”로 명시하니까 “dark ambient”, “melancholic indie” 같은 게 나왔고, 검색 결과가 바뀌었다.
Last.fm도 비슷한 문제가 있었다. 여러 단어를 하나의 태그로 인식해서 결과가 0개였다. 단어별로 분리해서 개별 태그로 검색하도록 수정했다.
재추천과 공유의 상충
재추천할 때마다 다른 플레이리스트가 나와야 한다. 근데 공유 URL로 들어온 사람은 내가 본 것과 같은 걸 봐야 한다. 두 조건이 충돌한다.
variation 파라미터로 해결했다. 재추천할 때마다 ?v=0, ?v=1, ?v=2처럼 URL이 바뀐다. ISBN + variation 조합을 PostgreSQL에 캐싱해두면, 같은 URL에 들어오는 사람은 항상 같은 결과를 본다.
재추천은 새로운 variation을 생성하는 것이고, 공유는 현재 variation을 URL에 고정하는 것이다. 두 기능이 같은 메커니즘 위에서 자연스럽게 분리됐다.
단계의 축소
ZXing.js로 바코드 스캐너를 붙였는데, 처음엔 “스캔 시작” 버튼을 뒀다. 버튼 누르고 → 카메라 허용하고 → 스캔하는 흐름.
생각해보니 버튼 자체가 불필요한 마찰이었다. 홈에서 카메라를 항상 켜두는 방식으로 바꿨다. 모바일에서 앱을 열면 바로 카메라가 떠있고, 책 바코드를 갖다 대면 된다. 진입장벽을 줄이는 데 이게 더 맞는 구조였다.
책과 음악을 잇는 일은 “어떤 기준으로 이을 것인가”를 먼저 정하는 일이었다. 소개글 길이, 장르 키워드 형식, 태그 분리 방식 — 각 결정은 다음 단계가 쓸 수 있는 형식을 기준으로 내렸다. 기준이 있으면 테스트는 빠르다.