binlog
Projects/Page of Artist

Page of Artist

텍스트 중심 탐색의 한계를 해결하기 위해, 아티스트와 음악 데이터를 3D 인터페이스로 재구성한 뮤직 플랫폼. 데이터 구조와 인터랙션을 결합해 사용자가 콘텐츠를 탐색하는 과정을 하나의 경험 흐름으로 설계했습니다.

Role프론트엔드 개발, 3D 인터랙션Year2025
React 18TypeScriptThree.jsReact Three FiberZustandFirebaseSpotify APIVite
Page of Artist
01

Problem

캡스톤 프로젝트로 진행된 6인의 팀 프로젝트로, 기존 음악 플랫폼의 탐색 경험을 개선하기 위해 시작했습니다. 기존 음악 플랫폼의 아티스트 페이지는 앨범, 트랙, 프로필 정보를 2D 리스트 형태로 나열하는 구조가 대부분입니다. 이 방식은 정보를 빠르게 확인하기에는 적합하지만, 사용자가 아티스트의 음악 세계관이나 앨범 간 분위기 차이를 시각적으로 탐색하기에는 한계가 있었습니다. 특히 신인 아티스트나 개성 있는 음악 콘텐츠는 단순 목록 안에서 차별점이 드러나기 어렵고, 사용자는 콘텐츠를 "탐색"하기보다 이미 알고 있는 곡을 "재생"하는 흐름에 머물게 됩니다. 그래서 Page of Artist는 아티스트와 음악 데이터를 단순히 나열하는 것이 아니라, 사용자가 직접 움직이고 선택하며 탐색할 수 있는 3D 공간 기반 음악 경험으로 재구성하는 것을 목표로 했습니다.

02

Limitation

기존 3D 웹 기반 음악 콘텐츠는 시각적으로는 인상적이지만, 실제 음악 데이터 구조와 연결되지 않는 경우가 많았습니다. 즉, 3D 오브젝트는 장식적인 요소에 머물고, 아티스트 정보나 앨범, 트랙 데이터와 유기적으로 연결되지 않아 서비스 구조로 확장하기 어려웠습니다. 또한 3D 인터페이스는 사용자가 조작할 때 어색한 움직임이 발생하기 쉽습니다. 단순 위치 이동이나 회전만 적용하면 카드가 기계적으로 움직이고, 사용자가 "탐색하고 있다"는 감각보다 "효과를 보고 있다"는 느낌에 가까워집니다. 모바일 환경과 성능도 중요한 한계였습니다. React 상태 변화만으로 3D 카드의 위치, 회전, 포커싱을 처리하면 불필요한 리렌더링이 발생할 수 있고, 카드 개수가 늘어날수록 인터랙션이 끊기거나 프레임 저하가 생길 가능성이 있었습니다.

03

Solution

Page of Artist는 아티스트와 음악 데이터를 3D 카드 인터페이스로 재구성하고, 사용자의 조작이 곧 탐색 흐름이 되도록 설계했습니다. 각 카드는 단순한 이미지 요소가 아니라 아티스트, 앨범, 트랙 정보를 담는 데이터 단위로 정의했습니다. 3D 구현에는 React Three Fiber를 사용했습니다. React 기반 컴포넌트 구조 안에서 Three.js의 3D 객체를 다룰 수 있어, 카드 UI와 데이터 구조를 함께 관리하기에 적합했기 때문입니다. 이를 통해 아티스트 카드, 앨범 정보, 트랙 리스트를 각각 독립적인 컴포넌트로 분리하면서도 하나의 3D 탐색 흐름 안에 배치할 수 있었습니다. 카드의 이동과 포커싱은 단순 애니메이션이 아니라 Spring 기반 물리 연산으로 처리했습니다. 사용자가 드래그하거나 스크롤할 때 카드가 즉시 끊겨 움직이는 것이 아니라, 감쇠와 관성을 가진 움직임으로 반응하도록 설계해 자연스러운 조작감을 만들고자 했습니다. 또한 카드의 상태를 active, adjacent, background로 나누어 현재 선택된 카드와 주변 카드의 크기, 위치, 시각적 강조를 다르게 처리했습니다. 이를 통해 사용자가 어떤 콘텐츠에 집중하고 있는지 명확하게 인식할 수 있도록 했습니다. 성능 측면에서는 React의 일반적인 상태 업데이트에 모든 움직임을 맡기지 않고, useFrame과 ref 기반 계산을 활용해 렌더링 사이클과 물리 연산을 분리했습니다. 이 구조를 통해 카드의 위치, 회전, 스케일 변화가 반복적으로 발생해도 불필요한 리렌더링을 줄이고 안정적인 인터랙션을 유지하도록 설계했습니다.

04

System Architecture

User Input (Mouse / Keyboard / Touch / Gyroscope)
                      |
                      v
       DOM Event Handlers (CircularCarousel)
                      |
          springTarget ref <- drag / scroll
                      |
       useFrame Physics Loop (60fps, no re-render)
    force = dx x TENSION - vel x FRICTION
                      |
   Card Group Positions (imperative update)
                      |
          Three.js Renderer -> Canvas

  Spotify API -> Express Proxy (Token Cache)
                      |
          Artist Data + Track Info
                      |
  Firebase Firestore (realtime) -> Zustand Store
                      |
            Card Data -> 3D Scene
05

Key Implementation

카드 인터랙션은 마우스 위치 기반 3D 틸팅과 Spring 물리 연산을 결합해, 사용자의 입력에 따라 자연스럽게 반응하는 인터랙션 구조를 구현했습니다. 렌더링 사이클과 분리된 ref 기반 물리 계산을 적용해, React 리렌더 없이도 60fps 환경에서 안정적인 애니메이션을 유지했습니다. 외부 음악 데이터를 API 기반으로 연동하고, 응답 지연이나 실패 상황에서도 UI 흐름이 유지되도록 데이터 처리 구조를 설계했습니다. Firebase Firestore 실시간 구독을 통해 아티스트 데이터 변경이 즉시 반영되도록 구성해, 데이터와 UI 상태가 실시간으로 동기화되는 구조를 구현했습니다. 장르 필터 선택 시 카드 재배치 애니메이션을 물리 기반으로 처리해, 데이터 변화가 자연스러운 시각적 흐름으로 이어지도록 설계했습니다.

06

Key Code

01CircularCarousel — Spring Physics Looptypescript
1const SPRING_TENSION = 170
2const SPRING_FRICTION = 26
3const RADIUS = 3.8
4
5const springPos = useRef(0)
6const springVel = useRef(0)
7const springTarget = useRef(0)
8
9useFrame((_, dt) => {
10 if (N === 0) return
11 const safe = Math.min(dt, 0.033)
12
13 // Damped spring: force = tension * displacement - friction * velocity
14 const dx = springTarget.current - springPos.current
15 const force = dx * SPRING_TENSION - springVel.current * SPRING_FRICTION
16 springVel.current += force * safe
17 springPos.current += springVel.current * safe
18
19 // Imperatively update each card's 3D position (no React re-render)
20 for (let i = 0; i < N; i++) {
21 const group = cardGroupRefs.current[i]
22 if (!group) continue
23 const angle = i * angleStep + springPos.current
24 group.position.set(RADIUS * Math.sin(angle), 0, RADIUS * Math.cos(angle))
25 group.rotation.y = -angle
26 }
27
28 const computed = wrap(Math.round(-springPos.current / angleStep), N)
29 if (computed !== activeIndex) setActiveIndex(computed)
30})
31
32const onPointerUp = () => {
33 if (isDragging.current) {
34 // Snap to nearest card after drag release
35 const nearest = -Math.round(springPos.current / angleStep) * angleStep
36 springTarget.current = nearest
37 }
38 isDragging.current = false
39}

useFrame 안에서 감쇠 스프링(tension=170, friction=26)을 직접 계산하고, ref로 각 Three.js 카드 그룹을 명령형으로 업데이트합니다. React 리렌더 없이 60fps 인터랙션을 유지하는 핵심 구조입니다.

07

Result & Learnings

캡스톤 프로젝트를 통해 인터랙션 중심 UI 설계와 팀 기반 개발 경험을 쌓을 수 있었고, 최종 A+ 학점과 함께 "기획과 기술적 도전성이 뛰어나며 협업 기반 문제 해결 능력이 우수하다"는 평가를 받았습니다. 이후 리빌딩 과정에서 렌더링과 물리 연산을 분리한 구조를 적용해, 60fps 환경에서도 안정적인 인터랙션을 구현했습니다. 외부 API의 불안정성을 고려해 정적 데이터 폴백 구조를 적용함으로써, 서비스 환경에서도 사용자 경험이 끊기지 않도록 개선했습니다. 이 과정을 통해 인터랙션과 데이터 구조를 함께 설계하는 것이 사용자 경험의 완성도를 결정한다는 것을 확인했습니다.

Links