SnowGrow 프로젝트) Client ↔ Server Job Polling ↔ Audio Range Streaming + TTL 운영

김기수·2026년 4월 26일

클론 코딩 기반의 오디오 시각화 웹사이트
Original Codepen: https://codepen.io/dilums/pen/MWjEqaa


0) 한 줄 요약

SnowGlow는 YouTube 링크를 입력하면 서버가 mp3로 변환하고, 클라이언트는 job 상태를 폴링하며 진행률(%)을 표시한 뒤, 완성된 mp3를 HTTP Range 스트리밍으로 재생/시크(시간 이동) 하는 구조로 만들었다.


1) 왜 “Job + Polling + Streaming” 구조가 필요했나?

초기엔 “YouTube URL → 서버가 변환 → mp3를 내려받아 decodeAudioData로 디코딩” 방식이 가장 단순해 보였다.
하지만 실제로는 아래 문제가 반복적으로 발생했다.

  • 변환 시간이 길다
    • 변환이 수 초~수 분 걸릴 수 있어, 한 번의 요청으로 끝내기 어렵다.
  • 진행률이 없으면 UX가 나쁘다
    • 사용자는 “멈춘 건지 / 진행 중인지” 모른다.
  • decodeAudioData 실패
    • 긴 mp3(특히 1시간 단위)에서 decodeAudioData failed가 발생하거나 메모리 이슈가 생길 수 있다.
  • 시크(Seek) 문제
    • 전체 파일을 다 받아 디코딩하기 전엔 시간 이동이 불편하다.
    • <audio> 기반 스트리밍 + Range가 훨씬 자연스럽다.

그래서 최종적으로 Job 기반 변환 + 진행률 폴링 + <audio> 스트리밍(HTTP Range) 로 구조를 바꿨다.


2) 전체 통신 흐름(Sequence)

아래가 현재 구조의 핵심 플로우다.

2-1) YouTube 변환 시작

  • Client → Server
  • POST /api/youtube/jobs
    • body: { "url": "<youtubeUrl>" }
    • response: { "jobId": "<uuid>" }

클라이언트는 jobId를 받아서 “진행률 폴링”을 시작한다.

2-2) 진행률 폴링 (percent UI)

  • Client → Server
  • GET /api/youtube/jobs/:jobId
    • response: { status, percent, stage, error? }

클라이언트는 일정 주기(예: 550ms)로 계속 호출한다.

UI 예시:

  • YouTube 변환 중… 84%
  • YouTube 변환 중… 99%

2-3) 변환 완료 후 오디오 스트리밍 재생

  • Client → Server
  • GET /api/youtube/jobs/:jobId/audio

클라이언트는 이 mp3 URL을 <audio src="...">로 재생한다.
이 방식은 전체 다운로드/디코딩 없이도 브라우저가 알아서 버퍼링/재생을 처리한다.


3) 시퀀스 다이어그램


4) 엔드포인트 예시 (요청/응답)

아래는 “형태”를 보여주기 위한 예시입니다.

4-1) 변환 job 생성

Request

POST /api/youtube/jobs
Content-Type: application/json

{"url":"https://www.youtube.com/watch?v=VIDEO_ID"}

Response

{
  "jobId": "114abf53-1956-409b-ab20-da06acecfe74"
}

4-2) job 상태 조회(폴링)

Request

GET /api/youtube/jobs/114abf53-1956-409b-ab20-da06acecfe74

Response (running)

{
  "status": "running",
  "percent": 84,
  "stage": "download"
}

Response (done)

{
  "status": "done",
  "percent": 100,
  "stage": "complete"
}

Response (error)

{
  "status": "error",
  "percent": 0,
  "stage": "starting",
  "error": "yt-dlp exited with code 1"
}

4-3) 오디오 스트리밍 (Range 예시)

Request (seek가 발생하면 브라우저가 Range를 보냄)

GET /api/youtube/jobs/114abf53-1956-409b-ab20-da06acecfe74/audio
Range: bytes=0-1048575

Response

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: audio/mpeg
Content-Range: bytes 0-1048575/123456789
Content-Length: 1048576

5) 서버 구현 포인트

5-1) Job 저장소

현재는 단일 서버 기준으로 job 상태를 메모리 Map에 저장한다.

  • jobId → { status, percent, stage, error, outputPath, cleanupAt }

확장(멀티 인스턴스/오토스케일)이 필요해지면 Redis/DynamoDB 같은 외부 저장소로 빼야 한다.

5-2) yt-dlp 진행률(%) 파싱

yt-dlp의 --progress-template을 이용해 stdout/stderr에서 파싱 가능한 라인을 뽑고, percent 값을 숫자로 추출해 job.percent를 계속 갱신한다.

5-3) HTTP Range 지원

/youtube/jobs/:jobId/audio는 Range 요청을 처리한다.

  • Range: bytes=start-end 헤더가 오면 206 Partial Content
  • Accept-Ranges, Content-Range, Content-Length 설정
  • fs.createReadStream(file, { start, end })로 부분 스트리밍

5-4) TTL(임시 파일 보관)

변환된 mp3는 서버 temp/에 저장된다.

  • 기본 TTL: 24시간
  • seek/Range 요청으로 TTL을 갱신(연장)하지 않는다

6) 클라이언트 구현 포인트

6-1) 로딩 UI

  • 변환 중에는 overlay에서 spinner + 진행 텍스트를 중앙에 표시
  • loading이 끝난 직후 topbar를 몇 초간 보여줘 사용자가 상태 변화를 인지하게 함

6-2) decodeAudioData 대신 <audio> 스트리밍

YouTube/업로드 mp3는 <audio>로 재생하고, WebAudio로 분석만 연결한다.

  • <audio>가 실제 재생 담당
  • AudioContext.createMediaElementSource(audioEl)로 analyser 연결
  • 시각화는 analyser FFT 데이터로 동작

장점:

  • 긴 mp3에서도 안정적
  • 시크/버퍼링이 브라우저 레벨에서 자연스럽게 처리됨

6-3) 상단 플레이바(시크)

  • input[type="range"]로 시간을 이동
  • <audio> 재생 경로는 audio.currentTime을 바꾸면 즉시 이동
  • 재생/일시정지는 audio.play() / audio.pause()

7) 에러 처리(UX)

  • 잘못된 YouTube URL은 홈에서 1차 검증(도메인/프로토콜)
  • 서버 에러는 클라이언트에서 한국어 문구로 정리해 표시

예시:

  • “유효한 URL이 아니에요. YouTube 링크를 확인해 주세요.”
  • “YouTube 변환에 실패했어요. 다른 링크로 시도해 주세요.”
  • “서버 접근이 차단됐어요(CORS). 배포 도메인/서버 설정을 확인해 주세요.”

8) 다음 단계(확장/운영)

트래픽이 늘면 아래 구조가 안정적이다.

  • job 상태 저장소 외부화(Redis/DynamoDB)
  • 변환 결과 오디오를 S3에 저장 + CloudFront로 Range 서빙
  • 서버는 job 관리/변환만 담당하고 “오디오 서빙”은 CDN으로 분리

9) 저장소

🙏 Credits

  • Original Inspiration: https://codepen.io/dilums/pen/MWjEqaa
profile
엄청난 클라우드 고수

0개의 댓글