REACT

모바일에서만 영상이 안 나와? 내 삽질기 (ft. Range 헤더)

user-anonymous 2025. 7. 23. 18:19
728x90

Node.js로 구성된 환경에서 나는 영상을 가져오는 기능을 담당하는 api를 개발했다.
정말 그냥 http://localhost/file/test.mp4하면 아래처럼 영상을 보여주는 api다.. 

 

 

  • PC에선 잘 보이는 영상이, 모바일에선 깨져서 나오거나 재생이 안 되는 경우가 있다.
  • 영상 스트리밍에 필요한 HTTP Range 요청과 206 Partial Content 응답을 서버가 제대로 지원하지 않으면 모바일 브라우저에서 재생 실패한다.
  • 이 글은 내가 직접 겪은 삽질 과정을 정리한 글이다. 혹시 같은 문제로 고생 중이라면 도움이 되기를,,.(과연?)

 

 

 

Node.js 기반의 API에서 아래처럼 단순히 동영상을 반환하는 기능을 만들었다.

GET http://localhost:8080/file/test.mp4​
 
데스크톱에서는 아무 문제 없이 video 태그로 잘 재생되었다. 그런데.. 모바일에서만 영상이 깨지는 문제가 발생했다.
  • <video src="..." controls> 태그 사용
  • 모바일에서는 00:00~00:00으로 시간 표시만 되고 영상은 재생되지 않음

 

왜 데스크톱에선 잘 불러오는데? 왜 모바일에서만???!!? 

디버깅 : curl로 API 응답 분석

정말 이런 생각으로 약 2시간동안 서칭했다. 먼저 내가 계속 붙잡고 있던 api를 curl 날려본 결과

 

- Content-Type 
- Accept-Ranges
- Content-Length 

 

음… 괜찮아 보이는데?

 

그래서 다른 서버도 테스트해봤다

http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4
http면서 해당 위 url을 모바일에서 여니까 너무 영상이 잘 나오는 것이었다ㅎ
 
그래서 당장 curl 요청을 날려봤다

열어보니 뭔가 내가 개발한 api의 응답값보다  항목이 많다. x-goog 항목은 저 영상 url이 googleApi를 통해 가져오므로 관련된 항목같았다. 그러므로 prefix goog는 패스! 이렇게 비교해보니

 

응답 결과에는 다음과 같은 추가적인 헤더들이 있었다:

Cache-Control : 캐시 메커니즘을 지정하기 위해 사용되는 헤더
ex:) Cache-Control : public, max-age=3600 : 초 단위로 캐시가 유효한 시간 제공
Expires
Last-Modified
ETag : Entity-Tag의 약자, 리소스를 식별하는 식별자
Date

 

그중에서도 가장 큰 차이는 바로 응답 코드 Range 처리였다.

 

더보기

스트리밍/영상 콘텐츠에서 중요하게 봐야 할 헤더

 

  1. Accept-Ranges: bytes
    • 이게 있어야 **동영상 스트리밍 시 점프(Seeking)**가 가능합니다.
    • 예: <video controls src="...">에서 10분 뒤로 이동할 수 있는 기능.
  2. Content-Type: video/mp4
    • 브라우저나 클라이언트가 어떤 형식인지 파악하는 데 필요.
  3. Access-Control-Allow-Origin: *
    • CORS 허용됨 → 다른 출처의 웹 페이지에서도 이 리소스를 사용할 수 있음.
  4. Cache-Control / Etag
    • 브라우저 캐시 정책 / 리소스 변경 여부 판단

 

용도 헤더

스트리밍 지원 Accept-Ranges, Content-Length, Content-Type
캐싱 정책 Cache-Control, Last-Modified, Etag
CORS 허용 여부 Access-Control-Allow-Origin, Access-Control-Expose-Headers
서버 메타정보 x-goog-*, Server

 

 

핵심 문제: Range 요청 미처리

모바일 브라우저는 영상 스트리밍 시 반드시 Range 요청을 보낸다

Range: bytes=0​

 

이에 대해 올바른 응답은 206 Partial Content로 부분 응답을 보내야 된다.

HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1048575/29119704
Content-Type: video/mp4
Content-Length: 1048576

 

하지만 내가 만든 API는 항상 200 OK + 전체 파일을 응답하고 있었다.
모바일 브라우저는 이를 "스트리밍 불가능한 응답"으로 인식하고 재생을 포기한다.

 

 

해결 방법: Range 요청을 처리하자

 

app.get('/file/:filename', (req, res) => {
  const filePath = path.join(__dirname, 'files', req.params.filename);
  const stat = fs.statSync(filePath);
  const total = stat.size;

  const range = req.headers.range;
  if (range) {
    const [startStr, endStr] = range.replace(/bytes=/, '').split('-');
    const start = parseInt(startStr, 10);
    const end = endStr ? parseInt(endStr, 10) : total - 1;
    const chunkSize = end - start + 1;

    const file = fs.createReadStream(filePath, { start, end });
    res.writeHead(206, {
      'Content-Range': `bytes ${start}-${end}/${total}`,
      'Accept-Ranges': 'bytes',
      'Content-Length': chunkSize,
      'Content-Type': 'video/mp4',
    });
    file.pipe(res);
  } else {
    res.writeHead(200, {
      'Content-Length': total,
      'Content-Type': 'video/mp4',
    });
    fs.createReadStream(filePath).pipe(res);
  }
});

 

추가로 다음 헤더도 설정하면 캐싱 및 성능에 도움된다:

res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('ETag', 'some-value');
res.setHeader('Access-Control-Allow-Origin', '*');​
728x90
반응형