Use Cases (업데이트: 2026. 6. 2.)

Claude Code로 오디오 플레이어 구축하기

Claude Code를 사용한 오디오 플레이어 구축 방법을 알아봅니다. 실용적인 코드 예시와 단계별 가이드를 포함합니다.

Claude Code로 오디오 플레이어 구축하기

Claude Code로 오디오 플레이어 구축하기

음악 스트리밍, 팟캐스트, 교육 콘텐츠 등 커스텀 오디오 플레이어가 필요한 장면은 많습니다. Claude Code를 활용하면 파형 표시와 플레이리스트 기능을 갖춘 본격적인 플레이어를 구축할 수 있습니다.

기본 플레이어 구현

> 커스텀 오디오 플레이어를 만들어줘.
> 재생/일시정지, 시크바, 볼륨, 재생 속도 변경을 지원해줘.
> 플레이리스트 기능과 파형 표시도 구현해줘.
// src/components/AudioPlayer.tsx
'use client';
import { useRef, useState, useEffect } from 'react';

interface Track {
  id: string;
  title: string;
  artist: string;
  src: string;
  duration: number;
  coverArt?: string;
}

interface AudioPlayerProps {
  tracks: Track[];
  initialTrackIndex?: number;
}

export function AudioPlayer({ tracks, initialTrackIndex = 0 }: AudioPlayerProps) {
  const audioRef = useRef<HTMLAudioElement>(null);
  const [currentTrackIndex, setCurrentTrackIndex] = useState(initialTrackIndex);
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [volume, setVolume] = useState(0.8);

  const currentTrack = tracks[currentTrackIndex];

  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const onTimeUpdate = () => setCurrentTime(audio.currentTime);
    const onLoadedMetadata = () => setDuration(audio.duration);
    const onEnded = () => playNext();

    audio.addEventListener('timeupdate', onTimeUpdate);
    audio.addEventListener('loadedmetadata', onLoadedMetadata);
    audio.addEventListener('ended', onEnded);

    return () => {
      audio.removeEventListener('timeupdate', onTimeUpdate);
      audio.removeEventListener('loadedmetadata', onLoadedMetadata);
      audio.removeEventListener('ended', onEnded);
    };
  }, [currentTrackIndex]);

  const togglePlay = () => {
    const audio = audioRef.current;
    if (!audio) return;
    if (audio.paused) {
      audio.play();
      setIsPlaying(true);
    } else {
      audio.pause();
      setIsPlaying(false);
    }
  };

  const playNext = () => {
    const nextIndex = (currentTrackIndex + 1) % tracks.length;
    setCurrentTrackIndex(nextIndex);
    setTimeout(() => {
      audioRef.current?.play();
      setIsPlaying(true);
    }, 100);
  };

  const playPrevious = () => {
    if (currentTime > 3) {
      // 3초 이상이면 처음으로 돌아감
      audioRef.current!.currentTime = 0;
    } else {
      const prevIndex = (currentTrackIndex - 1 + tracks.length) % tracks.length;
      setCurrentTrackIndex(prevIndex);
      setTimeout(() => {
        audioRef.current?.play();
        setIsPlaying(true);
      }, 100);
    }
  };

  const formatTime = (sec: number) => {
    const m = Math.floor(sec / 60);
    const s = Math.floor(sec % 60);
    return `${m}:${s.toString().padStart(2, '0')}`;
  };

  return (
    <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden max-w-md mx-auto">
      <audio ref={audioRef} src={currentTrack.src} preload="metadata" />

      {/* 커버 아트 */}
      <div className="aspect-square bg-gray-200 dark:bg-gray-700 relative">
        {currentTrack.coverArt ? (
          <img src={currentTrack.coverArt} alt={currentTrack.title} className="w-full h-full object-cover" />
        ) : (
          <div className="w-full h-full flex items-center justify-center text-6xl text-gray-400">♪</div>
        )}
      </div>

      {/* 트랙 정보 */}
      <div className="p-6">
        <h3 className="text-lg font-bold dark:text-white truncate">{currentTrack.title}</h3>
        <p className="text-gray-500 dark:text-gray-400 text-sm">{currentTrack.artist}</p>

        {/* 시크바 */}
        <div className="mt-4">
          <input
            type="range"
            min={0}
            max={duration || 0}
            value={currentTime}
            onChange={(e) => {
              const time = Number(e.target.value);
              audioRef.current!.currentTime = time;
              setCurrentTime(time);
            }}
            className="w-full h-1 accent-blue-600"
          />
          <div className="flex justify-between text-xs text-gray-400 mt-1">
            <span>{formatTime(currentTime)}</span>
            <span>{formatTime(duration)}</span>
          </div>
        </div>

        {/* 컨트롤 */}
        <div className="flex items-center justify-center gap-6 mt-4">
          <button onClick={playPrevious} className="text-2xl dark:text-white hover:text-blue-600">⏮</button>
          <button
            onClick={togglePlay}
            className="w-14 h-14 rounded-full bg-blue-600 text-white text-2xl flex items-center justify-center hover:bg-blue-700"
          >
            {isPlaying ? '⏸' : '▶'}
          </button>
          <button onClick={playNext} className="text-2xl dark:text-white hover:text-blue-600">⏭</button>
        </div>

        {/* 볼륨 */}
        <div className="flex items-center gap-2 mt-4">
          <span className="text-sm dark:text-gray-400">🔊</span>
          <input
            type="range"
            min={0}
            max={1}
            step={0.05}
            value={volume}
            onChange={(e) => {
              const vol = Number(e.target.value);
              audioRef.current!.volume = vol;
              setVolume(vol);
            }}
            className="flex-1 h-1 accent-blue-600"
          />
        </div>
      </div>

      {/* 플레이리스트 */}
      <div className="border-t dark:border-gray-700 max-h-60 overflow-y-auto">
        {tracks.map((track, index) => (
          <button
            key={track.id}
            onClick={() => {
              setCurrentTrackIndex(index);
              setTimeout(() => { audioRef.current?.play(); setIsPlaying(true); }, 100);
            }}
            className={`w-full flex items-center gap-3 p-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 ${
              index === currentTrackIndex ? 'bg-blue-50 dark:bg-blue-900/30' : ''
            }`}
          >
            <span className="text-xs text-gray-400 w-6 text-right">
              {index === currentTrackIndex && isPlaying ? '♪' : index + 1}
            </span>
            <div className="flex-1 min-w-0">
              <p className="text-sm font-medium dark:text-white truncate">{track.title}</p>
              <p className="text-xs text-gray-500 truncate">{track.artist}</p>
            </div>
            <span className="text-xs text-gray-400">{formatTime(track.duration)}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

Web Audio API로 파형 표시

고급 기능으로, Web Audio API를 사용한 실시간 파형 표시도 Claude Code에 요청할 수 있습니다. AudioContext와 AnalyserNode를 조합하여 재생 중인 오디오의 파형을 Canvas에 그릴 수 있습니다.

관련 글

동영상 플레이어 구현은 동영상 플레이어 구축 가이드를, 반응형 대응은 반응형 디자인을 참고하세요.

Web Audio API에 대한 자세한 내용은 MDN(developer.mozilla.org/Web/API/Web_Audio_API)을 확인하세요.

2026 프로덕션 업그레이드: 재생 버튼을 넘어서기

오디오 플레이어는 단순히 음성 파일을 재생하는 위젯이 아닙니다. 사용자가 어떤 강의나 에피소드를 듣는지, 얼마나 진행했는지, 어디로 돌아갈 수 있는지, 다음 행동이 무엇인지 이해하게 만드는 오디오 UI입니다. 학습 콘텐츠, 팟캐스트, 멤버십 미디어, 상품 미리듣기에서는 재생 상태, 진행률, 오류 메시지, 접근성, 분석 이벤트, CTA가 모두 제품 경험에 포함됩니다.

위 React/TypeScript 코드는 실행 가능한 기본 구조입니다. HTMLAudioElementuseRef로 잡고, React는 현재 트랙, 재생 여부, 시간, 볼륨을 관리합니다. 프로젝트에 붙여 넣을 때는 tracks가 비어 있지 않게 하고, src는 실제 오디오 파일을 가리키게 하며, 컴포넌트를 클라이언트 환경에서 렌더링하세요. 이 기반 위에 파형, 학습 진도 저장, 유료 전환 버튼을 더하면 실무용 플레이어가 됩니다.

HTMLAudioElement와 Web Audio API 차이

HTMLAudioElement는 브라우저의 audio 요소를 JavaScript에서 다루는 객체입니다. 로딩, 재생, 일시정지, 볼륨, 현재 시간, 길이, 메타데이터, 종료 이벤트를 처리합니다. 일반적인 강의 플레이어, 팟캐스트, 음원 샘플은 먼저 이 API로 안정적인 재생을 만드는 것이 좋습니다. 공식 속성과 이벤트는 MDN의 audio 요소 문서를 확인하세요.

Web Audio API는 더 낮은 수준의 오디오 그래프입니다. AudioContext, MediaElementAudioSourceNode, AnalyserNode, GainNode를 연결하면 파형, 스펙트럼, 이퀄라이저, 페이드, 오디오 반응형 UI를 만들 수 있습니다. 하지만 버튼과 시크바를 대신하는 API는 아닙니다. 재생은 HTMLAudioElement가 맡고, 분석이나 효과가 필요할 때 Web Audio API를 붙이는 구조가 유지보수에 유리합니다. React에서 DOM 오디오 요소를 안전하게 보관하는 방식은 useRef를 참고하세요.

계층책임운영 시 체크 포인트
트랙 데이터제목, 작성자, URL, 길이, 커버, 자막 ID빈 플레이리스트를 렌더링하지 않기
재생 엔진HTMLAudioElement 재생, 정지, 탐색, 볼륨loadedmetadata, timeupdate, ended, 오류 이벤트 처리
시각화Web Audio API로 파형 또는 스펙트럼 생성CDN CORS와 사용자 제스처 후 AudioContext.resume() 확인
UI 컨트롤버튼, 슬라이더, 목록, 시간 표시aria-label, 키보드 포커스, 스크린 리더 문구 추가
제품 계층진도 저장, 분석, 유료 CTA, 권한재생 시작, 50%, 완료, 재생 반복, CTA 클릭 측정

실제 use case 4가지

첫 번째 use case는 언어 학습과 온라인 강의입니다. 학습자는 배속, 10초 되감기, 북마크, 스크립트 동기화, 이어듣기가 필요합니다. 파형보다 중요한 것은 복습하기 쉬운 흐름입니다.

두 번째 use case는 팟캐스트와 미디어 사이트입니다. 커버, 챕터, 관련 에피소드, 뉴스레터 가입, 멤버십 안내를 플레이어 주변에 배치하면 오디오가 전환 경로의 중심이 됩니다.

세 번째 use case는 크리에이터 상품 페이지입니다. 음악, 내레이션, 효과음, 강의 샘플은 구매 전에 들어 볼 수 있어야 신뢰가 생깁니다. 짧은 미리듣기와 Gumroad 또는 결제 링크를 연결하면 monetization 흐름이 명확해집니다.

네 번째 use case는 사내 교육입니다. 영업 스크립트, 고객 응대 사례, 발음 연습, 컴플라이언스 교육은 짧은 오디오 목록으로 운영하기 좋습니다. 여기서는 예쁜 애니메이션보다 완료율, 오류 복구, 관리자용 기록이 더 중요합니다.

pitfall과 실패 사례

가장 흔한 pitfall은 자동 재생을 믿는 것입니다. 최신 브라우저는 사용자의 클릭이나 탭 없이 audio.play()를 거부할 수 있습니다. play()는 Promise를 반환하므로 실패를 잡아 사용자에게 다시 재생 버튼을 누르도록 안내해야 합니다.

두 번째 실패는 duration을 너무 빨리 믿는 것입니다. 메타데이터가 로드되기 전에는 길이가 NaN이거나 0일 수 있습니다. 시크바의 max를 보호하고, loadedmetadata 이후에 확정된 시간을 표시하세요.

세 번째 실패는 파형 기능의 CORS 문제입니다. 다른 도메인의 오디오를 Web Audio API에 연결하려면 서버가 올바른 CORS 헤더를 보내야 합니다. 로컬 파일로는 성공했지만 CDN 배포 후 파형이 멈추는 사례가 많습니다.

네 번째 문제는 트랙 변경 직후의 상태 경쟁입니다. 데모에서는 setTimeout으로 재생해도 보이지만, 운영에서는 canplay 또는 loadedmetadata를 기다리는 편이 안전합니다.

링크와 monetization CTA

React 상태 설계는 Claude Code React development, 파형과 분석은 Claude Code Web Audio API, 버튼과 키보드 접근성은 Claude Code accessibility를 함께 보세요.

오디오를 강의, 유료 팟캐스트, 멤버십, 상품 미리듣기로 연결하려면 CTA를 마지막에 붙이지 말고 처음부터 설계해야 합니다. 무료 미리듣기, 이메일 등록, 유료 콘텐츠, team consultation으로 이어지는 흐름을 만들면 제품 가치가 분명해집니다. ClaudeCodeLab의 training / consultation에서는 실제 저장소 기준으로 Claude Code 규칙, 리뷰 체크리스트, 이벤트 측정, 접근성 기준을 정리할 수 있습니다.

#Claude Code #audio #Web Audio API #React #player
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.