- Published on
강의 영상을 운영에 처음 올리고 재생할 때, 무엇이 막혔나
- Authors

- Name
- 김민석
강의 영상은 서버를 거치지 않고 S3와 CloudFront로 오간다
강의 영상은 크다. 테스트에 쓴 1시간 강의가 8.35GB다. 영상이 서버를 지나가면 업로드와 재생이 서버의 네트워크와 메모리를 그만큼 차지한다. 그래서 영상은 서버를 거치지 않게 만들었다.
- 관리자 화면은 서버가 발급한 Presigned URL로 영상을
10MB조각으로 나눠 S3에 바로 올린다 - 업로드가 끝나면 S3가 Lambda를 부르고, Lambda가 MediaConvert에 변환을 맡긴다. 결과는 화질
1080p,720p,480p의6초HLS 조각과, 조각 순서를 적은 목록 파일.m3u8이다 - 학생이 강의를 열면 서버가 수강 기간과 누적 시청 한도를 확인하고
2시간짜리 CloudFront 서명 URL을 준다

- 굵은 선이 영상이 지나가는 길이다. 서버는 업로드 주소와 서명 URL만 주고받는다
- ✕ 표시가 업로드 완료와 재생이 막힌 곳이다
학생에게 강의를 열기 전에, 관리자 화면으로 영상을 올리고 학생 화면에서 재생해 봤다. 서버 2대가 도는 운영 환경에 7분 영상과 1시간 영상을 올렸다.
7분 영상은 2분 18초 만에 올라가고 2초 뒤 변환이 시작됐다
| 항목 | 결과 |
|---|---|
| 영상 | 1.6GB, 7분 18초, 1080p 60fps |
| 조각 | 10MB × 154개 |
| 업로드 | 2분 18초, 약 93Mbps |
| 서버가 받은 요청 | 4번. 업로드 시작, 조각 주소 발급, 완료 주소 발급, 강의 등록 |
| 업로드 끝 → 변환 시작 | 2초 |
| 변환 | 4분 15초, 대기 0초 |
| 결과 파일 | 223개. 화질 3개 × 조각 73개 + 목록 파일 4개 |
조각 요청은 서버가 아니라 S3로 바로 갔다. 영상 바이트는 서버를 지나가지 않았다. 영상 길이 437.8초를 6초로 나누면 73조각이라 결과 파일 수도 계산과 맞았다.

- 위 두 줄
initiate,presigned-urls가 서버로 간 요청이고, 그 아래db81b334-…줄이 전부 S3로 간 조각이다. 가린 칸은 요청이 간 주소다 - 조각마다
867~980ms가 걸렸다
객체 키는 서버가 videos/{UUID}.mp4로 정한다. S3 알림은 객체 키를 URL 인코딩해서 보내는데, 예전 변환 경로는 원래 파일 이름을 키에 넣고 인코딩을 풀지 않아 한글 이름 영상이 변환에서 실패했다. 키를 UUID로 정하면 그 실패가 날 자리가 없다.

- 한글과 괄호가 든 파일
25년 7월 2주 언어이해(2).mp4가videos/db81b334-….mp4로 저장된다. 가린 줄은 업로드 번호다
첫 재생은 CloudFront 응답에 CORS 헤더가 없어 막혔다
학생 화면에서 강의를 여니 영상이 나오지 않고 "이 비디오 형식은 지원되지 않습니다"가 떴다. 서버는 서명 URL을 200으로 줬는데, 플레이어가 받으려던 첫 목록 파일에서 CORS error가 났다. 조각 요청은 0건이었다.

video-url이200이고, 이어서db81b334-…목록 요청이CORS error다988cda77…빨간 줄들은 분석 도구의 설정 요청이라 영상과 상관없다
실패한 요청의 응답을 열어 보니 상태는 200이고 목록 파일 588바이트가 그대로 와 있었다. 서명은 통과했고, CloudFront가 S3에서 파일을 가져온 것이다. 빠진 것은 Access-Control-Allow-Origin이었다. 플레이어는 목록과 조각을 XHR로 받기 때문에, 다른 주소에서 온 응답에 허락 헤더가 없으면 브라우저가 플레이어에 넘기지 않는다.

Server가AmazonS3,X-Cache가Miss from cloudfront다. 응답 헤더 어디에도Access-Control-Allow-Origin이 없다

- 서명 확인을 통과하고 목록 파일도 왔지만, 응답에 허락 헤더가 없어 브라우저가 플레이어에 넘기지 않는다
영상 배포의 기본 동작을 보니 응답 헤더 정책도, 원본 요청 정책도 없었다. CORS 헤더를 붙이는 방법은 3가지였다.
- 모든 출처를 허용하는 관리형 정책을 붙이는 방법은 설정이 가장 간단하지만, 학생 사이트에서만 쓰는 영상을 모든 사이트에 여는 것이라 고르지 않았다
- S3 버킷에 CORS 규칙을 넣는 방법은 원본 요청 정책으로
Origin헤더를 S3까지 넘기고 캐시 키에도Origin을 넣어야 해서 바꿀 곳이3곳이다 - CloudFront 응답 헤더 정책은 CloudFront가 응답을 내보낼 때 헤더를 붙이므로 이미 캐시된 파일에도 바로 적용된다. 학생 사이트 주소
2개만 허용하고GET,HEAD,OPTIONS만 열어 붙였다
정책을 붙이고 다시 재생하자 영상이 나왔다. 목록과 조각이 모두 200이고, 그 사이 30초마다 시청 위치 저장 position이 서버로 갔다.

- 오른쪽 목록에서
db81b334-…줄이 조각,position줄이 시청 위치 저장이다. 조각마다164~250ms - 왼쪽 영상 화면은 흐리게 처리했다

Access-Control-Allow-Origin에 학생 사이트 주소가 붙었다. 가린 칸이 그 주소다X-Cache가Hit from cloudfront,Age가373이다. 정책을 붙이기 전부터 CloudFront에 캐시돼 있던 응답에도 헤더가 붙어, 캐시를 지우지 않아도 됐다
1시간 영상은 855조각을 다 올리고도 강의가 등록되지 않았다
1시간 영상을 올리니 첫 조각에서 바로 CORS error가 났다. 브라우저 콘솔에는 응답의 Access-Control-Allow-Origin 값이 요청한 사이트 주소와 다르다고 찍혔다. 그런데 같은 요청의 preflight 응답에 S3가 돌려준 값은 관리자 사이트 주소 그대로였다. 응답이 브라우저에 들어온 뒤 헤더가 바뀐 것이다. 개발에 쓰던 크롬에 CORS를 허용해 주는 확장 프로그램이 켜져 있었다.
확장 프로그램을 끄고 다시 올리자 조각이 올라가기 시작했다.

- 조각
855개중53%까지 올라간 화면이다
조각은 끝까지 올라갔지만 마지막 단계에서 막혔다. 100%에서 "Failed to fetch"가 뜨고 강의는 등록되지 않았다.

- 마지막 조각들 다음 완료 주소 발급
complete-presign이200이고, 이어서 S3로 보낸 완료 요청이 빨간 줄로 실패했다. 강의 등록 요청lectures는 없다 Date의16:29:04 GMT가 한국 시각01:29:04다
2초 뒤인 01:29:06에 MediaConvert로 올린 영상의 변환 작업이 들어왔다. S3는 완료 요청을 받아 조각을 합쳤고, 브라우저만 그 응답을 읽지 못한 것이다.
원인은 S3 입력 버킷의 CORS 규칙이었다. 허용 메서드가 GET, PUT뿐이고 POST가 없었다. 조각을 올리는 PUT은 preflight로 허락을 받는다. 마지막 완료 요청은 헤더를 따로 붙이지 않고 본문만 보내는 POST라 브라우저가 preflight 없이 보내고, S3에 도착해 처리는 되지만 응답에 허락 헤더가 붙지 않는다.

- S3가 조각을 합쳐 파일을 만들자 변환이 시작되고, 브라우저는 완료 응답을 읽지 못해 업로드 화면이 실패로 끝난다
- 실패로 끝난 업로드 화면은 강의 등록을 부르지 않는다
강의는 없는데 영상은 변환되어 요금이 나가는 상태다.
확장 프로그램은 응답의 허락 헤더를 고쳐 쓴다. S3가 보낸 값을 바꿔 1시간 영상의 첫 조각을 막았고, 앞서 7분 영상을 올릴 때는 완료 응답에 빠진 허락 헤더를 붙여 업로드를 끝까지 통과시킨 것으로 보인다. 확장 프로그램이 없는 브라우저에서는 업로드 마지막 단계가 처음부터 막혀 있었다.
입력 버킷 CORS 규칙의 허용 메서드에 POST를 넣었다. 확장 프로그램을 끈 채 7분 영상을 다시 올리자 강의까지 등록됐다. 강의에 연결되지 않은 채 돌던 1시간 변환 작업은 끝나기 전에 취소했다. MediaConvert는 끝난 작업에만 요금을 매긴다. 그리고 1시간 영상을 처음부터 다시 올렸다.
| 항목 | 결과 |
|---|---|
| 영상 | 8.35GB, 55분 59초 |
| 조각 | 10MB × 855개 |
| 업로드 | 12분 42초, 약 94Mbps |
| 업로드 끝 → 변환 시작 | 2초 |
| 변환 | 42분 36초 |
| 업로드 시작 → 학생이 볼 수 있을 때까지 | 55분 20초 |
조각 주소는 처음에 1시간짜리로 한꺼번에 발급되고, 업로드가 12분 42초에 끝나 만료 전에 마쳤다.
업로드 요청 목록에는 조각마다 preflight와 PUT이 짝을 지어 855쌍 찍혔다. 브라우저는 preflight 결과를 주소마다 캐시하는데, 조각마다 서명된 주소가 달라 캐시가 맞지 않는다. 캐시를 끄고 올린 첫 시도 12분 44초와 켜고 올린 두 번째 12분 42초가 같은 것도 그래서다.
업로드가 끊기면 이미 올린 조각 48개가 S3에 남았다
7분 영상을 올리다가 31%에서 네트워크를 끊었다. 그 순간 "Failed to fetch"가 뜨고 업로드가 멈췄다. 네트워크를 되살려도 이어지지 않았다.

- 오른쪽 위 네트워크 설정이
Offline이고, 진행률이31%에서 멈췄다

- 끊긴 순간의 조각 요청이
net::ERR_INTERNET_DISCONNECTED로 실패했다
멀티파트 업로드는 완료 요청을 보내야 파일이 생긴다. 업로드 화면은 실패하면 오류를 띄우고 끝나서, 완료 요청도 중단 요청도 보내지 않는다. S3에 남은 미완료 업로드를 조회하니 2건이었다.
videos/fc5cd89e-1004-4c61-8a80-5b932ebc3e50.mp4 시작=2026-09-30T18:02:29Z 조각=48
videos/fdba781f-1d0f-4faf-8b39-fb7496bf85a5.mp4 시작=2026-09-30T15:32:50Z 조각=0
- 위 줄이 방금 끊은 업로드다. 조각
48개,503MB가 남았다. 아래 줄은 확장 프로그램 때문에 첫 조각에서 실패한1시간영상이다
미완료 업로드는 S3 파일 목록에 보이지 않는데, 남은 조각에는 저장 요금이 나간다. 1시간 영상이 90%에서 끊기면 7.5GB가 남는다.
입력 버킷에 수명 주기 규칙을 넣었다. 시작한 지 1일이 지난 미완료 멀티파트 업로드를 지우는 규칙이고, 영상 파일 자체는 건드리지 않는다.
남은 것
- 끊긴 업로드 이어 올리기, 조각 재시도
- 조각을 차례로 올리는 업로드.
1시간영상12분 42초 - 변환이 끝나기 전 학생 화면의 형식 오류 표시
- 개발 환경 영상 배포와 입력 버킷의 같은 CORS 설정
- 수명 주기 규칙이 남은 조각을 지웠는지 확인
정리
| 문제 | 고친 것 | 고치기 전 → 고친 뒤 |
|---|---|---|
| 첫 재생에서 목록 파일이 CORS로 막힘 | CloudFront 응답 헤더 정책, 학생 사이트 주소만 허용 | 목록 요청 CORS error, 조각 0개 → 목록과 조각 모두 200 |
1시간 업로드가 마지막 완료 단계에서 실패 | S3 CORS 허용 메서드에 POST 추가 | 조각 855개를 올리고 강의 미등록 → 12분 42초에 업로드와 강의 등록 |
| 끊긴 업로드의 조각이 S3에 남음 | 미완료 멀티파트 업로드 1일 뒤 삭제 | 미완료 업로드 2건, 조각 48개 → 확인 중 |