코딩 보조 도구로 끝내지 못할 프로젝트 되살리기

13 hours ago 3

(blog.matthewbrunelle.com)

  • 오래 묵은 개인 프로젝트는 새로 학습하기보다 구현을 끝내는 데 초점이 맞춰져 있어, 코딩 보조 도구를 시험하기에 잘 맞았음
  • YouTube Music을 OpenSubsonic API로 노출하는 shim을 다시 구현해, 서로 다른 Subsonic 클라이언트가 같은 방식으로 붙을 수 있게 만듦
  • 최소 구조와 구현 규약을 먼저 고정한 뒤 짧은 반복 주기로 작업하면서, OpenAPI 스펙 기반 stub 생성과 실제 클라이언트 연결 테스트를 함께 돌림
  • 스펙만 맞춘 초기 구현은 실제 연결에서 바로 깨졌고, 요청 로그 확인과 단위 테스트 추가를 반복하면서 검색·재생이 되는 수준까지 끌어올림
  • 학습용 stretch 프로젝트가 아니라 그냥 있었으면 하는 프로젝트라면, 코딩 보조 도구는 미뤄 둔 작업을 실제로 쓰는 서비스로 바꾸는 데 큰 도움이 됨

프로젝트 배경과 접근 방식

  • 오래전에 손댔다가 끝내지 못한 개인 프로젝트는 AI 코딩 보조 도구를 시험하기에 적합했음
    • 다시 살린 대상은 YouTube Music과 OpenSubsonic API를 잇는 shim이었음
    • OpenSubsonic은 음악 스트리밍 클라이언트와 서버를 분리할 수 있게 하는 API 계약으로 쓰였음
    • 서버는 Navidrome, 데스크톱 클라이언트는 Feishin, Android는 Symfonium을 사용했음
  • shim은 YouTube Music을 OpenSubsonic API에 맞게 노출해 어떤 클라이언트에서도 붙을 수 있게 만듦
    • 메타데이터 조회에는 ytmusicapi, 스트리밍에는 yt-dlp를 프로그램적으로 호출했음
    • 기본 스트리밍은 비교적 쉽게 붙었지만, 규격에 맞게 모든 엔드포인트를 구현하는 데 긴 꼬리 작업이 남아 있었음
  • 이 프로젝트는 새롭거나 독창적인 문제보다 명확한 스펙 구현이 중심이라 코딩 보조 도구와 잘 맞았음
    • Claude Code와 Opus 4.6으로 처음부터 다시 구현하는 실험으로 진행함

초기 세팅

  • 시작점은 구조를 미리 제한한 최소 프로젝트였음
    • uv 프로젝트를 만들고 fastapi, pydantic, ytmusicapi, yt-dlp를 의존성으로 추가함
    • main.py는 FastAPI 예제 메인 파일로 바꾸고 OpenSubsonic OpenAPI 스펙을 폴더에 넣음
    • README에는 서버 역할, 사용 라이브러리, 문서 URL, openapi.json 위치를 짧게 적어 둠
    • 빈 TODO 파일을 추가하고 /init으로 CLAUDE.md를 생성함
  • CLAUDE.md에는 구현 규약을 따로 적어 반복 지시를 줄였음
    • 메서드 인자와 반환값에 타입 애너테이션과 docstring을 요구함
    • 데이터 모델링은 Pydantic V2 규약으로 맞춤
    • docstring은 Google 스타일의 args, returns 섹션을 요구함
    • 테스트는 상위 레벨 함수와 assert, fixture를 쓰는 최신 pytest 스타일로 맞춤
  • 이 시작점은 git repository로 묶어 둠

MVP 구현 흐름

  • 작업 방식은 계획 모드와 짧은 반복 주기로 굴렸음
    • 다음 작업을 프롬프트로 던지고 초기 계획에서 빠진 점이나 문제를 확인함
    • 어긋나는 경우 관련 리소스 링크를 추가로 주거나, 여러 선택지가 있으면 검색 도구로 더 관용적인 방식을 찾게 함
    • 작업 뒤에는 "Accept and clear context"를 사용해 컨텍스트를 비우고 반복함
  • 첫 구현은 OpenAPI 스펙에서 신형 JSON 엔드포인트만 stub으로 만드는 데 집중했음
    • 예전 XML 엔드포인트와 새 JSON 엔드포인트가 함께 있었지만, 새 쪽만 처리하도록 범위를 좁힘
    • 구현 후에는 모든 메서드가 맞는지 다시 점검하는 검증 단계를 넣음
    • 스펙이 있어도 첫 시도에는 실수가 나왔고, 두 번째 점검에서 대부분 잡힘
  • 큰 변경 뒤에는 /init을 다시 돌려 CLAUDE.md를 새 상태에 맞게 갱신
  • 다음 단계는 검색과 스트리밍이 되는 최소 기능을 정의하고 붙이는 작업이었음
    • Subsonic 클라이언트를 연결해 곡을 검색하고 재생할 수 있는 최소 기능을 목표로 삼음
    • 검색은 ytmusicapi, 스트리밍은 yt-dlp를 사용함

실제 연결에서 드러난 문제와 보정

  • 겉보기에는 그럴듯한 구현이 빨리 나왔지만, 실제로 Feishin에 붙이면 바로 무너졌음
    • 클라이언트를 직접 테스트하고 서버 요청 로그를 Claude Code에 넘기며 반복 수정함
    • 스펙만으로는 드러나지 않는 세부 차이도 있었고, 예를 들어 엔드포인트의 .view 접미사는 벗겨내야 했음
  • 오류가 날 때마다 단위 테스트를 새로 만들어 회귀를 막음
  • 몇 번 반복하지 않아도 Feishin에서 실제로 오디오가 재생되기 시작했음
    • 핵심 문제는 stub 엔드포인트가 아무것도 반환하지 않는 데 있었음
    • 대부분의 엔드포인트는 비어 있더라도 구조가 맞는 응답을 돌려주도록 바꿔야 했음
  • 다만 이런 수준의 MVP는 예전 POC와 크게 다르지 않았고, 실제 사용성은 이후의 긴 꼬리 작업에 달려 있었음

긴 꼬리 작업과 전체 기능 확장

  • OpenSubsonic 문서 기준으로 API는 약 80개 엔드포인트가 15개 카테고리에 퍼져 있었음
  • MVP에 필요한 범위는 비교적 작았음
    • getLicense, getUser, getGenres, getMusicDirectories는 비어 있지만 유효한 컬렉션을 반환함
    • getSong은 쿼리 파라미터의 ID를 그대로 돌리고 기본값을 채우는 pass-through로 처리함
    • search3는 간단한 ytmusicapi 호출로 구현함
    • stream은 asyncio.to_thread로 감싼 yt-dlp 호출로 "bestaudio" 포맷 URL을 추출함
    • getCoverArt는 yt-dlp로 커버 이미지 URL을 뽑음
  • 실제 Subsonic 클라이언트 전체 기능을 지원하려면 추가 작업이 필요했음
    • ytmusicapi 호출에는 단순한 메모리 캐시를 넣어 사용량 제한을 피함
    • 음악 메타데이터 저장에는 sqlite를 쓰고 browsing 카테고리의 모든 엔드포인트를 구현함
    • getTopSongs도 top songs 목록을 질의해 처리함
  • 스트리밍 중에는 곡을 디스크에 저장해 재다운로드를 피했음
    • 클라이언트가 다운로드가 끝나기 전에 stream 엔드포인트 연결을 끊으면, 미완성 파일을 정리하는 추가 처리가 필요함
  • 이런 작업들은 원래 손으로도 할 수 있었지만 끝내 하지 못했던 부분이었고, 배포 계획이 없어서 인증처럼 어려운 부분은 여전히 건너뜀
  • 최종적으로는 짧은 저녁 시간 안에 Subsonic 클라이언트가 연결되는 동작하는 서비스를 만들었고, 프로젝트 이름은 Sub-standard로 붙임

어디에 쓰는 것이 맞는가

  • 코딩 보조 도구를 무조건 밀어붙이기보다는 의존으로 인한 역량 저하에 대한 우려도 함께 두고 있었음
  • 개인 프로젝트는 크게 두 갈래로 나뉘었음
    • 배우고 성장하기 위한 stretch 프로젝트
    • 그냥 존재했으면 좋겠다고 느끼는 프로젝트
  • 이번 프로젝트는 두 번째 갈래에 속했고, 코딩 보조 도구는 그동안 끝내지 못했던 바람을 실체화하는 데 잘 맞았음
    • 원래라면 손대지 못했을 프로젝트를 실제로 가지게 됐고, 읽지 못한 책 더미를 하나 줄이는 감각에 가까웠음
  • 중요한 기준은 두 번째 갈래 프로젝트를 하느냐보다, 첫 번째 갈래의 학습용 프로젝트도 계속 함께 하고 있느냐에 있었음
Read Entire Article