
회사에서 AWS CloundFront + S3 를 통해 m3u8 스트리밍 비디오 파일을 불러오는 와중에 CORS 에러가 발생하여 파일을 제대로 불러오지 못하는 일이 발생했습니다. 무엇이 문제였고 어떻게 해결했는지 그 과정에 대해서 정리해보려고 합니다.
회사에서 개발 중인 서비스에서 HLS(HTTP Live Streaming) 방식으로 비디오를 재생하는 기능을 구현하고 있었습니다. 백엔드에서는 AWS S3에 업로드된 m3u8 파일과 ts 세그먼트들을 CloudFront를 통해 배포하고 있었고, 프론트엔드에서는 video.js 라이브러리를 사용해 이를 재생하려고 했습니다.
(이해를 돕기위한 코드로 참고용으로 봐주세요 🙇♀️)
import { useRef, useEffect } from 'react';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
interface VideoPlayerProps {
src: string;
options?: any;
}
export const VideoPlayer: React.FC<VideoPlayerProps> = ({ src, options }) => {
const videoRef = useRef<HTMLVideoElement>(null);
const playerRef = useRef<any>(null);
useEffect(() => {
if (videoRef.current) {
playerRef.current = videojs(videoRef.current, {
...options,
sources: [{
src: src, // CloudFront URL: https://d1234567890.cloudfront.net/videos/sample.m3u8
type: 'application/x-mpegURL'
}]
});
}
return () => {
if (playerRef.current) {
playerRef.current.dispose();
}
};
}, [src, options]);
return (
<video
ref={videoRef}
className="video-js vjs-default-skin"
controls
preload="auto"
data-setup="{}"
/>
);
};
그런데 개발 환경에서 테스트해보니 CORS 에러가 발생해 동영상을 제대로 불러오지 못하고 있었습니다.
처음에는 "단순히 m3u8 파일을 가져오는 건데 왜 CORS 에러가 나지?" 라고 생각했습니다. 이후 원인에 대해 구글링을 해보다 Simple Request 와 Complex Request의 차이가 있다는 것을 알게 되었습니다.
CORS에서 Simple Request는 preflight 없이 바로 서버로 전송되는 요청으로, 조건이 생각보다 까다로웠습니다.
Simple Request가 되려면 다음 조건을 모두 만족해야 합니다.
자동으로 설정되는 헤더 (User-Agent, Accept 등) 외에 수동으로 설정할 수 있는 헤더는 다음과 같이 제한됩니다.
Simple Request 조건을 하나라도 벗어나면 Complex Request(복합 요청)가 됩니다.
비디오 스트리밍에서는 브라우저가 자동으로 Range 헤더를 추가합니다. 이는 비디오 파일의 특정 부분만 요청 하기 위한 헤더입니다.
GET /videos/sample.m3u8 HTTP/1.1
Host: d1234567890.cloudfront.net
Range: bytes=0-1023
Origin: http://localhost:3000
video.js나 HLS 플레이어(내가 사용한 것) 들은 종종 다음과 같은 헤더들을 추가로 사용합니다.
X-Requested-WithX-Playback-Session-Id이런 헤더들이 하나라도 있으면 Complex Request가 되어 preflight가 필요 합니다.
pre-flight는 브라우저가 실제 요청을 보내기전에 이런 요청을 보내도 괜찮은지 서버에게 미리 확인하는 과정을 말합니다.
OPTIONS /videos/sample.m3u8 HTTP/1.1
Host: d1234567890.cloudfront.net
Origin: http://localhost:3000
Access-Control-Request-Method: GET
Access-Control-Request-Headers: range
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Allow-Headers: range, content-type
Access-Control-Max-Age: 86400
GET /videos/sample.m3u8 HTTP/1.1
Host: d1234567890.cloudfront.net
Origin: http://localhost:3000
Range: bytes=0-1023
만약 preflight 단계에서 서버가 적절한 CORS 헤더를 반환하지 않으면, 브라우저는 실제 요청을 보내지 않고 CORS 에러를 발생시키게 되는 것입니다.
비디오 스트리밍에서는 브라우저가 자동으로 Range 헤더를 사용합니다. 이는 대용량 비디오 파일을 청크 단위로 나눠 받기 위함인데 이 때문에 simple Request 조건을 벗어나 pre-flight 가 필요하게 됩니다.
# 첫 1MB 요청 (초기 로딩)
Range: bytes=0-1048575
# 다음 1MB 요청 (스트리밍 계속)
Range: bytes=1048576-2097151
# 사용자가 건너뛰기 한 경우
Range: bytes=5242880-6291455
Range 헤더의 장점은 다음과 같습니다.
비디오 스트리밍은 Range 헤더 사용으로 인해 Complex Request가 되며, 이에 따라 브라우저는 사전에 preflight 요청(OPTIONS)을 전송하게 됩니다.
하지만 CloudFront에서 별도의 응답 헤더 정책(Response Headers Policy) 을 설정하지 않으면, 이러한 preflight 요청에 대해 적절한 CORS 허용 헤더를 포함하지 않게 됩니다.
즉, 브라우저는 CloudFront에서 오는 응답에 Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methods 같은 CORS 관련 헤더가 포함되지 않은 것을 확인하고, CORS 에러를 발생시킵니다.
해결 방법은 간단했습니다. CloudFront 콘솔에서 응답 헤더 정책을 설정해줌으로써 브라우저가 요청을 정상적으로 처리할 수 있게 만들었습니다.
CloudFront에서 다음과 같은 설정을 통해 문제를 해결할 수 있습니다.


설정이 완료되면 브라우저가 OPTIONS 요청에 대해 유효한 응답을 받게 되어 CORS 에러 없이 정상적으로 스트리밍 요청이 처리됩니다.
📚 참고