Skip to content

🎧 Next.js둜 λ§Œλ“œλŠ” 랜덀 μŒμ•… μΆ”μ²œ μ„œλΉ„μŠ€ with Apple Music RSS

Notifications You must be signed in to change notification settings

mynolog/one-song

Repository files navigation

🎡 OneSong

πŸ“„ μ†Œκ°œ

이 ν”„λ‘œμ νŠΈλŠ” 3λ…„ 전에 개인적으둜 μ§„ν–‰ν–ˆλ˜ 랜덀 μŒμ•… μΆ”μ²œ 아이디어λ₯Ό 기반으둜 μƒˆλ‘­κ²Œ λ¦¬λΉŒλ“œν•œ λ²„μ „μž…λ‹ˆλ‹€.

초기 ν”„λ‘œμ νŠΈλͺ…은 'Music Roulette'μ΄μ—ˆμœΌλ‚˜, λ¦¬λ‰΄μ–Όν•˜λ©΄μ„œ ν”„λ‘œμ νŠΈμ˜ 컨셉을 λ‚˜νƒ€λ‚Ό 수 μžˆλŠ” 'OneSong'으둜 λ³€κ²½ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

초기 버전은 React, CSS Module, Shazam APIλ₯Ό 기반으둜 κ°œλ°œλ˜μ—ˆμœΌλ‚˜,
ν˜„μž¬λŠ” Next.js와 TailwindCSSλ₯Ό ν™œμš©ν•˜μ—¬ 전체 ꡬ쑰와 UXλ₯Ό λŒ€λŒ€μ μœΌλ‘œ κ°œμ„ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

λ˜ν•œ 기쑴에 μ‚¬μš©ν–ˆλ˜ Shazam API의 μ£Όμš” κΈ°λŠ₯이 λŒ€λΆ€λΆ„ deprecatedλ˜μ–΄,
μƒˆλ‘œμš΄ 데이터 μ†ŒμŠ€λ₯Ό 기반으둜 ν”„λ‘œμ νŠΈλ₯Ό μž¬κ΅¬μ„±ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

πŸ›  기술 μŠ€νƒ

  • Next.js (App Router 기반)
  • TypeScript
  • TailwindCSS
  • Zustand
  • Jest
  • MSW

πŸ“¦ ν”„λ‘œμ νŠΈ ꡬ쑰

πŸ“ ν”„λ‘œμ νŠΈ 루트
β”œβ”€β”€ __tests__/                   # ν…ŒμŠ€νŠΈ 폴더 (src와 λ™μΌν•œ ꡬ쑰)
β”œβ”€β”€ public/                      # 정적 파일 (favicon, manifest λ“±)
β”œβ”€β”€ src/                         # μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ†ŒμŠ€ μ½”λ“œ
β”‚   β”œβ”€β”€ app/                     # Next.js μ•± λΌμš°νŒ… ꡬ쑰
β”‚   β”‚   β”œβ”€β”€ (with-audio)         # μ˜€λ””μ˜€ ν”Œλ ˆμ΄μ–΄κ°€ κ³ μ •λœ νŽ˜μ΄μ§€ λ ˆμ΄μ•„μ›ƒ κ·Έλ£Ή
β”‚   β”‚   β”œβ”€β”€ api                  # Next.js API 라우트 (μ„œλ²„ ν•¨μˆ˜ 기반 ν•Έλ“€λŸ¬)
β”‚   β”‚   └── assets               # μ•± 정적 μžμ‚°
β”‚   β”œβ”€β”€ components/              # μž¬μ‚¬μš© κ°€λŠ₯ν•œ UI 및 κΈ°λŠ₯ μ»΄ν¬λ„ŒνŠΈ
β”‚   β”‚   β”œβ”€β”€ common/              # 곡톡 μ»΄ν¬λ„ŒνŠΈ (AudioPlayer λ“±)
β”‚   β”‚   β”œβ”€β”€ country/             # 메인 νŽ˜μ΄μ§€ κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈ
β”‚   β”‚   β”œβ”€β”€ guest/               # λΉ„νšŒμ› μ „μš© νŽ˜μ΄μ§€ κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈ
β”‚   β”‚   β”œβ”€β”€ icons/               # ν”„λ‘œλ°”μ΄λ” μ•„μ΄μ½˜ μ»΄ν¬λ„ŒνŠΈ
β”‚   β”‚   β”œβ”€β”€ layouts/             # λ ˆμ΄μ•„μ›ƒ κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈ
β”‚   β”‚   β”œβ”€β”€ liked-songs/         # μ°œν•œ λ…Έλž˜ νŽ˜μ΄μ§€ κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈ
β”‚   β”‚   β”œβ”€β”€ me/                  # νšŒμ› μ „μš© νŽ˜μ΄μ§€ κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈ
β”‚   β”‚   └── ui/                  # shadcn/ui 기반 UI μ»΄ν¬λ„ŒνŠΈ 래퍼
β”‚   β”œβ”€β”€ constants/               # μƒμˆ˜κ°’ μ •μ˜ (κ΅­κ°€ μ½”λ“œ λ“±)
β”‚   β”œβ”€β”€ hooks/                   # μ»€μŠ€ν…€ ν›… μ •μ˜
β”‚   β”œβ”€β”€ lib/                     # μœ ν‹Έ ν•¨μˆ˜ 및 API fetch 둜직
β”‚   β”œβ”€β”€ providers                # ν”„λ‘œλ°”μ΄λ” 래퍼 μ»΄ν¬λ„ŒνŠΈ
β”‚   β”œβ”€β”€ stores/                  # Zustand μƒνƒœ 관리 μŠ€ν† μ–΄
β”‚   β”œβ”€β”€ types/                   # νƒ€μž… μ •μ˜
β”‚   β”œβ”€β”€ auth.ts                  # Auth.js ν™˜κ²½ μ„€μ •
β”‚   └── middleware.ts            # Next.js 미듀웨어
β”œβ”€β”€ jest.config.ts               # Jest μ„€μ •
β”œβ”€β”€ tsconfig.json                # TypeScript μ„€μ •
β”œβ”€β”€ README.md
└── package.json

🧠 μƒνƒœ 관리 섀계 원칙

  • μ „μ—­ μƒνƒœ

    • ν•΄λ‹Ή μƒνƒœλ₯Ό μ†ŒλΉ„ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈμ—μ„œ 직접 λΆˆλŸ¬μ™€μ„œ μ‚¬μš©
    • μ „μ—­ μƒνƒœλ₯Ό props둜 μ „λ‹¬ν•˜λŠ” 방식은 μ§€μ–‘
  • μ§€μ—­ μƒνƒœ

    • ν•΄λ‹Ή μƒνƒœλ₯Ό ν•„μš”λ‘œ ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈμ— props둜 μ „λ‹¬ν•˜μ—¬ μ‚¬μš©
    • μ»΄ν¬λ„ŒνŠΈ λŽμŠ€κ°€ 3단계 이상 λ„˜μ–΄κ°ˆ 경우 μ „μ—­ μƒνƒœλ‘œ 승격 κ³ λ €

✨ κ΅¬ν˜„ κΈ°λŠ₯

βœ… MVP (μ΅œμ†Œ κΈ°λŠ₯ κ΅¬ν˜„)

  • 일뢀 ꡭ가별 랜덀 곑 μΆ”μ²œ
  • Apple Music RSS 기반 미리듣기 지원
  • 곑 제λͺ©, μ•„ν‹°μŠ€νŠΈλͺ…, 앨범컀버, 앨범λͺ… 정보 제곡
  • λΉ„νšŒμ› μ°œν•œ λ…Έλž˜ λͺ¨μ•„보기 (μ „μ—­ μƒνƒœ 관리 + λ‘œμ»¬μŠ€ν† λ¦¬μ§€ 연동)

βž• μΆ”κ°€ κ΅¬ν˜„

  • μ˜€λ””μ˜€ ν”Œλ ˆμ΄μ–΄ μ»€μŠ€ν„°λ§ˆμ΄μ§• (μž¬μƒ / μΌμ‹œμ •μ§€ / μ •μ§€ / μž¬μƒ μœ„μΉ˜ ν‘œμ‹œ)
  • κ΅­κ°€ 선택 λ“œλ‘­λ‹€μš΄ 메뉴
  • 졜근 μΆ”μ²œ 기둝 μ €μž₯
  • νšŒμ› κΈ°λŠ₯ λ„μž… 및 κ°œλ³„ μ°œν•œ λ…Έλž˜ λͺ¨μ•„보기 (Auth.js + Supabase DB)
  • 핡심 둜직 μœ λ‹› ν…ŒμŠ€νŠΈ 및 λ Œλ”λ§ ν…ŒμŠ€νŠΈ

πŸ›  κ΅¬ν˜„ μ˜ˆμ •

  • μ•„ν‹°μŠ€νŠΈ / 앨범 상세 νŽ˜μ΄μ§€ κ΅¬ν˜„ (ν˜„μž¬λŠ” μ™ΈλΆ€ 링크 처리둜 λŒ€μ²΄)
  • λ‹€κ΅­μ–΄ λŒ€μ‘
  • ν…ŒμŠ€νŠΈ μ½”λ“œ 및 배포 μžλ™ν™”

πŸ§ͺ ν…ŒμŠ€νŠΈ

  • ν”„λ ˆμž„μ›Œν¬: Jest, MSW
  • ν…ŒμŠ€νŠΈ λŒ€μƒ
    • μœ ν‹Έ ν•¨μˆ˜
    • μ»€μŠ€ν…€ ν›…
    • Zustand μ „μ—­ μŠ€ν† μ–΄
    • μ™ΈλΆ€ API ν•¨μˆ˜ (MSWλ₯Ό ν™œμš©ν•œ API λͺ¨ν‚Ή ν…ŒμŠ€νŠΈ 포함)
  • ν–₯ν›„ μ μ§„μ μœΌλ‘œ 톡합 ν…ŒμŠ€νŠΈ -> E2E ν…ŒμŠ€νŠΈλ‘œ ν™•μž₯ μ˜ˆμ •

🧩 νŠΈλŸ¬λΈ” μŠˆνŒ…

  • 1. μ˜€λ””μ˜€ ν”Œλ ˆμ΄μ–΄ μ»΄ν¬λ„ŒνŠΈ: μž¬μƒ 쀑 κ³Όλ„ν•œ λ¦¬λ Œλ”λ§ λ°œμƒ (2025.05.05)

    • 원인
      • Progressκ°€ λŠκΈ°λ“― 움직여 requestAnimationFrame을 μ μš©ν•΄μ„œ UIλŠ” κ°œμ„ λ˜μ—ˆμœΌλ‚˜ currentTime μƒνƒœλ₯Ό λ§€ ν”„λ ˆμž„ μ—…λ°μ΄νŠΈν•˜μ—¬ λΆˆν•„μš”ν•œ λ¦¬λ Œλ”λ§μ΄ κ³Όλ„ν•˜κ²Œ λ°œμƒ
    • ν•΄κ²° 방법
      • currentTime을 ref둜 κ΄€λ¦¬ν•˜μ—¬ μ‹€μ œ μž¬μƒ μ‹œκ°„ 좔적은 DOM 기반으둜 처리
      • 화면에 ν‘œμ‹œν•˜λŠ” μž¬μƒ μ‹œκ°„μΈ displayTime을 0.1초 λ‹¨μœ„λ‘œ μƒνƒœ μ—…λ°μ΄νŠΈν•˜μ—¬ λ¦¬λ Œλ” λΉˆλ„λ₯Ό μ œμ–΄
      • AudioPlayer λ‚΄λΆ€ 콜백 ν•¨μˆ˜λŠ” useCallback으둜 κ΄€λ¦¬ν•˜κ³ , ν•˜μœ„ μ»΄ν¬λ„ŒνŠΈλŠ” React.memo둜 μ²˜λ¦¬ν•˜μ—¬ λΆˆν•„μš”ν•œ λ¦¬λ Œλ”λ§ μ΅œμ†Œν™”
    • κ²°κ³Ό
      • μ΅œμ ν™” μ „: 1802회 λ¦¬λ Œλ”λ§
      • μ΅œμ ν™” ν›„: 262회 λ¦¬λ Œλ”λ§
      • μ•½ 85.5% λ¦¬λ Œλ”λ§ κ°μ†Œ
      • μž¬μƒ μƒνƒœ κ΄€λ ¨ UI만 λ¦¬λ Œλ”λ§λ˜λ„λ‘ κ°œμ„ 
  • 2. 첫 둜그인 μ‹œ, 이미 μ‘΄μž¬ν•˜λŠ” μ΄λ©”μΌλ‘œ μΈμ‹λ˜μ–΄ νšŒμ›κ°€μž… μ²˜λ¦¬κ°€ λ˜μ§€ μ•ŠμŒ (2025.05.07)

    • μ΄ˆκΈ°μ— μΆ”μ •ν•œ 원인

      • 둜그인 μ‹œ / νŽ˜μ΄μ§€μ—μ„œ λ‹€μ‹œ /country/[code] λΌμš°ν„°λ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈ λ˜λ©΄μ„œ AuthUserInitializerκ°€ λ‘λ²ˆ μ‹€ν–‰λ˜μ–΄μ„œ createUser λ©”μ„œλ“œκ°€ 두 번 호좜됨
      • 이둜 인해 쀑볡 νšŒμ› 생성 μ‹œλ„λ‘œ μΈν•œ insert μ‹€νŒ¨λ‘œ μΆ”μ •
    • μ‹€μ œ 원인

      • AuthUserInitializer λ‚΄ μ‚¬μ΄λ“œ μ΄νŽ™νŠΈκ°€ λ‘κ°œλ‘œ λΆ„λ¦¬λ˜μ–΄ 있음
          1. 이메일 쀑볡 확인 -> μ—†μœΌλ©΄ insert
          1. μ°œν•œ λ…Έλž˜ λͺ©λ‘μ„ localStorage -> Supabase DB둜 이관 + μƒνƒœ ν”Œλž˜κ·Έ μ—…λ°μ΄νŠΈ
      • 비동기 μ‹€ν–‰ μˆœμ„œκ°€ 보μž₯λ˜μ§€ μ•Šμ•„ insert -> select와 select -> insert 레이슀 λ°œμƒν•˜μ—¬ 타이밍에 따라 insertμ—μ„œ 쀑볡킀 μ—λŸ¬ λ°œμƒ
    • ν•΄κ²° 방법

      • insert λŒ€μ‹  upsert둜 λ³€κ²½ν•˜μ—¬ 이메일이 μ‘΄μž¬ν•˜λ©΄ λ„˜μ–΄κ°€κ³  μ—†μœΌλ©΄ μƒˆλ‘œ μ‚½μž…ν•˜μ—¬ ꡬ쑰 μ•ˆμ •μ„± 확보
      const { data, error } = await supabase
        .from('users')
        .upsert(
          [
            {
              email,
              name: name ?? null,
              avatar_url: image ?? null,
              provider,
            },
          ],
          {
            onConflict: 'email',
          },
        )
        .select('id')
        .single()
  • 3. Production ν™˜κ²½μ—μ„œ getToken()이 항상 null을 λ°˜ν™˜ν•˜μ—¬ 프라이빗 λΌμš°ν„° μž‘λ™ μ•ˆν•¨ (2025.05.08)

    • 원인
      • Auth.js + App Router ν”„λ‘œμ νŠΈμ—μ„œ cookieName이 λ³΄μ•ˆ μƒμ˜ 이유둜 __Secure-authjs.session-token으둜 변경됨
      • getToken()은 기본적으둜 next-auth.session-token만 인식
      • μΏ ν‚€λŠ” λΈŒλΌμš°μ € 상 μ‘΄μž¬ν•˜μ§€λ§Œ, getToken()이 cookieName을 μ°Ύμ§€ λͺ»ν•˜μ—¬ null을 λ°˜ν™˜
    • ν•΄κ²° 방법
      • src/moddileware.ts λ‚΄ getToken() 호좜 μ‹œ cookieName을 λͺ…μ‹œμ μœΌλ‘œ μ§€μ •
      const token = await getToken({
        req: request,
        secret: process.env.AUTH_SECRET,
        cookieName: '__Secure-authjs.session-token',
      })

🌿 브랜치 정보

  • main β€” λ¦¬λ‰΄μ–Όλœ 버전
  • legacy/main β€” λ ˆκ±°μ‹œ 버전 (배포 쀑단, μ½”λ“œλ§Œ 확인 κ°€λŠ₯)

About

🎧 Next.js둜 λ§Œλ“œλŠ” 랜덀 μŒμ•… μΆ”μ²œ μ„œλΉ„μŠ€ with Apple Music RSS

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published