모차르트 작품 카탈로그의 작품 상세 페이지에 "편곡버전 들어보기" 섹션을 붙였습니다. 원곡 연주가 아니라 피아노 연탄, 오르간 4손, 재즈 편곡처럼 같은 곡을 다르게 옮긴 버전을 소개하는 자리입니다. 요구는 간단해 보였습니다. 제목과 설명과 유튜브 URL을 받아, 기존 곡에 연결한다. 그런데 "기존 곡"이 작품 전체일 수도 있고 그 작품의 한 악장일 수도 있다는 조건이 붙었습니다. 이 조건 하나가 설계의 축이 됐습니다.
발단"작품일 수도, 악장일 수도 있다"
편곡 하나는 반드시 어떤 원곡에 매달립니다. 그런데 그 원곡이 작품 단위일 때가 있고("교향곡 40번 전체의 피아노 편곡"), 악장 단위일 때가 있습니다("피가로의 결혼 서곡만 따로 편곡"). 두 경우를 한 테이블로 받아야 했습니다.
새 개념처럼 보이지만, 사실 이 프로젝트엔 이미 똑같은 모양의 관계가 있었습니다. 사용자가 남기는 감상
글(살롱 스토리)이 정확히 "작품에 매달리되 선택적으로 악장까지 좁혀지는" 구조였습니다. 그래서 새 패턴을
만들지 않고 그걸 그대로 빌렸습니다. workId는 필수, movementId는 선택.
model Arrangement {
id String @id @default(cuid())
workId String // 노출될 원곡 작품 (필수)
movementId String? // 특정 악장의 편곡이면 지정, 없으면 작품 전체
title String
description String? @db.Text
youtubeUrl String?
arranger String? // 편곡자
instrumentation String? // 편성
order Int @default(0)
isVisible Boolean @default(true)
work Work @relation(fields: [workId], references: [id], onDelete: Cascade)
movement Movement? @relation(fields: [movementId], references: [id], onDelete: SetNull)
}
원곡 작품이 지워지면 편곡도 함께 지워지고(Cascade), 연결된 악장만 지워지면 편곡은 "작품 전체 편곡"으로
강등되어 살아남습니다(SetNull). 삭제의 결이 요구와 맞아떨어졌습니다.
설계새 패널을 만들지 않는다
두 번째 결정이 더 중요했습니다. 편곡을 클릭하면 어떻게 재생할 것인가.
이 사이트에는 이미 작품/악장을 슬라이드로 띄우는 패널이 있습니다. 유튜브 iframe이 한 번만 마운트되도록, 이중 재생이 나지 않도록 공들여 다듬어 둔 컴포넌트입니다. 편곡 전용 패널을 새로 만들면 그 공든 로직을 전부 다시 짜야 합니다.
그래서 만들지 않기로 했습니다. 대신 살롱 스토리 카드가 쓰던 방법을 그대로 가져왔습니다. 재생 버튼은 원곡의 패널을 열되, 그 안의 유튜브 URL만 편곡 것으로 갈아끼웁니다.
// 편곡 카드의 재생 핸들러 — 원곡 패널을 열고 youtubeUrl만 교체
if (arrangement.movement) {
setSelectedMovement(
{ ...원곡_악장_메타, youtubeUrl: arrangement.youtubeUrl },
원곡_작품_메타,
);
} else {
setSelectedWork({ ...원곡_작품_메타, youtubeUrl: arrangement.youtubeUrl });
}
편곡 고유의 정보(제목, 편곡자, 편성, 설명)는 상세 페이지 카드에만 두고, 재생은 원곡 패널에 맡깁니다. 덕분에 이중 재생 방지, 모바일 바텀시트 같은 기존 UX가 공짜로 따라왔습니다. 편곡을 위해 새로 짠 재생 코드는 사실상 없습니다.
관리자 매핑 화면도 같은 원칙을 지켰습니다. 공연 정보를 편집할 때 곡목에 악장을 연결하던 방식이 이미 있어서, 편곡의 악장 연결도 그 UI를 본떴습니다. "작품 전체" 아니면 드롭다운에서 악장 하나를 고르는 식입니다.
실제 콘텐츠로 끝까지 굴려 보기
기능을 만들었으면 진짜 데이터로 굴려 봐야 합니다. 피가로의 결혼 서곡에 편곡 하나를 붙이기로 했습니다.
여기서 규칙이 하나 있습니다. 이 프로젝트는 음원으로 정식 발매된 공식 앨범만 씁니다. 누군가 올린 개인 연주 영상이나 라이브는 URL이 사라지거나 저작권으로 막힐 위험이 커서 배제합니다. 그래서 유튜브에서 "Topic" 채널(발매 음원이 자동 등록되는 채널)만 골라 검증했습니다.
yt-dlp --no-warnings --flat-playlist \
--print "%(id)s | %(uploader)s | %(title)s" \
"ytsearch40:Mozart Figaro overture piano four hands" | grep -iE " Topic \|"
여기서 재미있는 사실을 만났습니다. "피아노 연탄(두 사람 네 손)"이면서 동시에 "공식 발매 음원"인 피가로 서곡 편곡이 없었습니다. 4손 연주는 전부 개인 영상이었고, 공식 발매된 편곡은 편성이 조금씩 달랐습니다. 피아노 6손, 오르간 4손, 피아노 독주 환상곡. 요구 두 개가 동시에 성립하지 않는 상황이라, 어느 쪽을 우선할지는 사람이 정해야 했습니다. 정확한 4손 편성을 우선하기로 하고, 오르간 4손 편곡을 골랐습니다. 어느 앨범의 두 연주자가 오르간 앞에 나란히 앉아 친 녹음입니다.
편곡자가 자료에 "작자 미상"으로만 남아 있어서 그 필드는 지어내지 않고 그대로 비웠습니다. 없는 정보를 그럴듯하게 채우지 않는 것도 규칙입니다.
검증은 세 겹으로
데이터를 넣었으면 노출까지 확인해야 끝입니다. 세 단계로 봤습니다.
- DB 조회. 공개 페이지가 실제로 읽는 것과 똑같은 조건으로 편곡을 다시 꺼내, 악장 연결과 필드가 맞는지 확인했습니다.
- 서버 렌더. dev 서버의 상세 페이지 HTML을 받아 "편곡버전 들어보기" 섹션과 편곡 제목, 음원 식별자가 들어 있는지 봤습니다.
- 브라우저 재생. 헤드리스 브라우저로 편곡 카드의 재생 버튼을 눌러, 열린 패널의 iframe에 편곡 음원이 실리고 원곡 음원은 실리지 않는지 확인했습니다.
세 번째가 이 기능의 핵심이었습니다. "원곡 패널을 열되 음원만 편곡으로 바꾼다"가 실제로 되는지는
브라우저에서 눌러 봐야 알 수 있으니까요. 결과는 { 편곡음원: true, 원곡음원: false }. 의도대로였습니다.
곁다리로 밟은 함정들
깔끔하게 끝난 것 같지만, 중간에 몇 번 헛짚었습니다.
서버 렌더 확인이 처음엔 실패했습니다. HTML을 받아 편곡 제목을 찾는데 아무것도 안 잡혔습니다. 페이지가 캐시된 줄 알고 한참 헤맸는데, 원인은 단순했습니다. 응답이 gzip으로 압축돼 있었고, 압축을 풀지 않은 채로 텍스트를 뒤지고 있었습니다. 요청에 압축 해제 옵션 하나를 붙이니 편곡 섹션이 멀쩡히 들어 있었습니다. "안 보인다"와 "없다"는 다른 문제였습니다.
DB 연결이 어디를 보는지도 함정이었습니다. 개발용 로컬 DB에는 예전 마이그레이션 하나가 실패한 채 남아 있어서 정식 마이그레이션 명령이 막혔습니다. 그래서 테이블을 "없으면 만든다" 방식으로 조건부 생성한 뒤 데이터를 넣는 우회로를 썼습니다. 실제 서비스 반영은 별도 단계로 분리했고, 어느 DB에 쓰고 있는지는 명령이 찍는 접속 정보로 매번 확인하는 게 안전했습니다.
같은 과정을 다시 밟을 수 있게
편곡은 앞으로도 계속 추가될 콘텐츠입니다. 이번에 한 번 손으로 다 해봤으니, 다음부터는 같은 길을 헤매지 않도록 과정을 스킬로 굳혔습니다. 작품 정보를 주면서 "편곡 버전 추가"라고 하면, 대상 조회 → 공식 음원 선정 → 데이터 구성 → DB 삽입 → 세 겹 검증 → 보고까지 같은 순서로 밟습니다.
이번에 밟은 함정들도 그대로 스킬에 적어 넣었습니다. 공식 음원만 쓴다는 규칙, 편성이 안 맞으면 사람에게 물어본다는 규칙, 압축 해제를 빠뜨리지 말 것, 어느 DB인지 접속 주소로 확인할 것, 편곡자를 지어내지 말 것. 한 번 겪은 실수를 문서에 박아 두면, 다음엔 그 자리에서 미끄러지지 않습니다.
그리고 곧바로 또 쓰인 패턴
편곡 기능을 넣고 스킬까지 굳힌 지 얼마 안 돼, 같은 구조가 또 필요해졌습니다. 공연 정보 페이지에는 그 공연에서 연주하는 곡목이 나오고, 모차르트 곡은 눌러서 미리들을 수 있습니다. 그런데 공연에서 원곡이 아니라 편곡 버전을 연주하는 경우가 있습니다. 이때 미리듣기도 그 편곡 음원으로 나와야 자연스럽습니다.
여기서 새로 고민할 게 거의 없었습니다. 재생 방식은 편곡 기능에서 이미 정해 뒀으니까요. 원곡 패널을 열되 음원만 편곡으로 바꾼다. 공연 곡목도 똑같이 하면 됩니다. 곡목 항목에 편곡을 가리키는 연결 하나를 더하고, 재생할 때 그 편곡이 있으면 음원을 편곡 것으로 교체했습니다.
관리자 화면도 마찬가지였습니다. 공연 곡목에 작품이나 악장을 연결하면, 그 대상에 편곡이 있을 때만 "편곡 버전" 드롭다운이 나타납니다. 편곡이 없으면 아무것도 보이지 않아 화면이 지저분해지지 않습니다. 곡목 옆에는 편곡으로 연주된다는 작은 표시가 붙습니다.
편곡 기능 하나를 제대로 만들어 두니, 그 다음 요구는 새 코드를 거의 짜지 않고 연결만으로 끝났습니다. 스키마에 컬럼 하나, 재생 함수에 조건 하나, 관리자에 드롭다운 하나. 처음에 "새 패널을 만들지 않는다"고 정한 결정이 여기까지 이자를 냈습니다.
마무리
기능 하나를 만드는 일은 코드를 짜는 데서 끝나지 않았습니다. 이미 있던 패턴을 알아보고 빌리는 눈, 실제 데이터로 끝까지 굴려 보는 검증, 그리고 그 과정을 다음 사람(또는 다음의 나)에게 넘길 수 있게 굳히는 일까지가 한 묶음이었습니다.
