eunsoolib
React 컴포넌트·훅

audio-components

sync-store 기반 오디오 플레이어 컴포넌트와 상태

앱 전체에서 HTMLAudioElement 하나를 공유하는 싱글턴 store(audioStore)가 재생 상태를 들고, 컴포넌트는 useAudioStore로 필요한 slice만 구독합니다. 볼륨과 반복 여부는 localStorage에 저장됩니다.

설치

pnpm add @cbcruk/audio-components react

브라우저 전용입니다. 내부적으로 @cbcruk/sync-store, @cbcruk/utils, @use-gesture/react를 사용합니다.

Tailwind CSS 설정

CastAudioPlayer는 CSS 파일 없이 Tailwind 유틸리티 클래스(bg-blue-600, min-w-[3rem] 등)로만 스타일을 입힙니다. Tailwind는 이 패키지의 의존성이 아니므로, 앱에 Tailwind v4를 설정하고 패키지 빌드 결과물을 스캔 대상에 넣어야 스타일이 생성됩니다. Tailwind v4의 자동 소스 감지는 node_modules를 건너뛰므로 @source로 직접 지정합니다.

/* app.css — 경로는 이 CSS 파일 기준 상대 경로 */
@import 'tailwindcss';
@source '../node_modules/@cbcruk/audio-components/dist';

Tailwind를 쓰지 않는 앱에서는 CastAudioPlayer가 스타일 없이 렌더링됩니다. 이 경우 AudioPlayerSlideruseAudioStore / audioActions로 UI를 직접 만드세요.

사용법

플레이어 컴포넌트

AudioManager를 앱에 한 번 마운트해 오디오 엘리먼트를 만들고 이벤트를 store에 연결한 뒤, 플레이어를 원하는 만큼 렌더링합니다.

import { AudioManager, CastAudioPlayer } from '@cbcruk/audio-components'

function App() {
  return (
    <>
      <AudioManager />
      <CastAudioPlayer src="/audio/episode-1.mp3" />
      <CastAudioPlayer src="/audio/episode-2.mp3" />
    </>
  )
}

다른 src의 플레이어에서 재생하면 공유 오디오의 소스가 교체됩니다. 재생 상태·현재 시간· 길이는 지금 재생 중인 src의 플레이어에만 표시되고, 볼륨과 반복 여부는 모든 플레이어가 공유합니다.

직접 UI 만들기

import {
  audioActions,
  formatDuration,
  useAudioStore,
} from '@cbcruk/audio-components'

function MiniPlayer({ src }: { src: string }) {
  const currentSrc = useAudioStore((state) => state.src)
  const storeIsPlaying = useAudioStore((state) => state.isPlaying)
  const storeCurrentTime = useAudioStore((state) => state.currentTime)
  const isPlaying = currentSrc === src && storeIsPlaying
  const currentTime = currentSrc === src ? storeCurrentTime : 0

  return (
    <button
      onClick={() =>
        isPlaying ? audioActions.pause() : audioActions.play(src)
      }
    >
      {isPlaying ? '일시정지' : '재생'} {formatDuration(currentTime)}
    </button>
  )
}

액션은 React 밖에서도 호출할 수 있습니다. 단, AudioManager가 마운트되어 오디오 엘리먼트가 등록되기 전에는 재생 관련 액션이 아무것도 하지 않습니다.

API

<AudioManager />

new Audio()(preload = 'metadata')를 만들어 audioActions.setAudio로 등록하고, play / pause / ended / timeupdate / loadedmetadata / loadstart / canplay / error 이벤트를 store에 반영합니다. store의 src가 없을 때 발생한 error 이벤트는 무시합니다. 아무것도 렌더링하지 않으며, 언마운트 시 재생을 멈추고 소스를 비운 뒤 setAudio(null)로 등록을 해제해 재생 상태를 초기화합니다.

<CastAudioPlayer src />

재생/일시정지, 진행 바, 현재·전체 시간, 볼륨 슬라이더, 반복 토글이 있는 플레이어입니다. 스타일은 Tailwind 유틸리티 클래스로 작성되어 있어 위의 "Tailwind CSS 설정"이 필요합니다. 이전/다음 버튼은 비활성 상태로만 렌더링됩니다.

재생 상태·진행 바·시간은 store의 src가 이 플레이어의 src와 같을 때만 반영되며, 다른 플레이어의 진행 바를 끌어도 재생 위치는 바뀌지 않습니다. 반복 토글은 aria-pressed로 상태를 나타내고 라벨은 반복 켜짐 / 반복 꺼짐입니다.

PropTypeDefaultDescription
srcstring재생할 오디오 URL

<AudioPlayerSlider value onChange />

드래그로 0~1 값을 고르는 가로 슬라이더입니다. 스타일이 없는 마크업만 렌더링하므로 [data-scope], [data-part="rail" | "track" | "handle"] 선택자로 직접 꾸며야 합니다. trackwidthhandleleft는 인라인 %로 지정됩니다.

PropTypeDefaultDescription
valuenumber현재 값(0~1)
onChange(value: number) => void드래그 위치 비율(0~1)

audioStore

persist로 만든 싱글턴 Store<AudioState>입니다. volume, isLooping만 localStorage의 audio-storage 키에 저장됩니다.

interface AudioState {
  audio: HTMLAudioElement | null
  src: string | null
  isPlaying: boolean
  currentTime: number // 초
  duration: number // 초
  isLoading: boolean
  error: string | null
  volume: number // 0~1, 기본 1
  isLooping: boolean
}

useAudioStore(selector?)

useStore(audioStore, selector)의 얇은 래퍼입니다. selector를 생략하면 상태 전체를 반환합니다.

audioActions

Action설명
setAudio(audio)오디오 엘리먼트를 등록하고 저장된 volume / isLooping을 반영. null이면 등록 해제 후 재생 상태 초기화
play(src)src가 현재와 다르면 소스를 교체해 재생, 같으면 이어서 재생. 로딩 중이면 무시. 재생이 시작되거나 실패하면 isLoading 해제
togglePlay()isPlaying에 따라 재생/일시정지. 현재 src가 없으면 무시
pause()일시정지
stop()정지 후 src 속성을 제거하고, src, 재생 위치, 길이, 로딩·에러 상태 초기화
seek(time)재생 위치(초) 이동
setVolume(volume)0~1로 clamp해 반영
toggleLoop()반복 여부 토글
setIsPlayingsetIsPlaying, setCurrentTime, setDuration, setIsLoading, setError — 해당 필드만 갱신

재생 실패 시 error에 한국어 메시지가 들어갑니다. pause()·stop()으로 play()가 중단된 경우(AbortError)는 실패로 보지 않습니다.

formatDuration(seconds)

초를 mm:ss 문자열로 바꿉니다. 시간 단위로 올리지 않습니다(3665'61:05').

formatCount(count) (deprecated)

오디오와 관계없는 범용 포맷터라 @cbcruk/utils로 옮겼습니다. 기존 코드가 깨지지 않도록 @cbcruk/utilsformatCount를 그대로 다시 내보내며, 새 코드에서는 import { formatCount } from '@cbcruk/utils'를 사용하세요. 반올림 결과가 다음 단위에 닿으면 단위를 올립니다(1500'1.5K', 999999'1.0M').

On this page