본문 바로가기
실무.log

AWS MediaConvert로 숏츠 플랫폼 HLS 스트리밍 구현

by _2J 2025. 10. 24.
반응형

 

요즘 숏츠 영상이 대세잖아요?
회사에서 숏츠 서비스를 만들게 되었는데, 저는 그 중에서도 HLS 스트리밍 부분을 담당하게 되었습니다.
영상 업로드는 다른 팀에서, 인코딩은 동료와 함께 진행했고, 저는 주로 S3에 올라온 영상을 사용자에게 매끄럽게 스트리밍하는 부분에 집중했어요.
생각보다 까다로운 작업이었는데, 그 과정에서 배운 것들을 정리해봤습니다.

 

 

기술 스택

Backend Infrastructure

  • Video Processing: AWS MediaConvert
  • Storage: Amazon S3
  • CDN: Amazon CloudFront
  • Event Processing: AWS Lambda, EventBridge
  • Backend: Spring Boot

Frontend

  • Template Engine: Thymeleaf
  • Video Streaming: HLS.js
  • UI Framework: Splide.js
  • Language: JavaScript ES6+

 

HLS 인코딩 자동화 파이프라인

업로드된 영상을 자동으로 HLS 포맷으로 바꿔주는 완전 자동화 시스템이에요.
사용자가 영상만 올리면 알아서 다양한 해상도로 변환해서 스트리밍 파일을 만들어줘요.

영상 업로드부터 스트리밍까지의 전체 플로우는 이런 식이에요:

  1. 숏츠 영상 업로드 → S3 Bucket 저장
  2. Lambda Trigger → 업로드 이벤트 감지
  3. EventBridge Job State Change → 작업 상태 관리
  4. MediaConvert Transcode → MP4를 HLS로 변환
  5. Multi-Resolution Output → 480p, 720p, 1080p 생성
  6. CloudFront CDN → 컨텐츠 캐싱
  7. 웹 서비스 스트리밍 → 최종 사용자 제공

 

업로드 트리거

S3 버킷에 영상이 올라오면 자동으로 Lambda 함수가 실행되어서 MediaConvert 작업을 시작해요.
이벤트 기반으로 돌아가니까 실시간으로 처리가 가능하더라고요.

// S3 업로드 완료 시 Lambda 함수 트리거
const handleVideoUpload = async (event) => {
    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));

    // MediaConvert 작업 생성
    const jobParams = {
        Role: process.env.MEDIACONVERT_ROLE,
        Settings: {
            Inputs: [{
                FileInput: `s3://${bucket}/${key}`,
                VideoSelector: {},
                AudioSelectors: {
                    "Audio Selector 1": {
                        DefaultSelection: "DEFAULT"
                    }
                }
            }],
            OutputGroups: generateHLSOutputGroups(key)
        }
    };

    await mediaConvert.createJob(jobParams).promise();
};
javascript

 

 

HLS 스트리밍 설정

HLS.js 라이브러리의 최적화된 설정값들이에요.
빠른 시작과 안정적인 재생을 위해서 버퍼 크기랑 로딩 전략을 이것저것 테스트해보면서 조정했어요.

// HLS.js 최적화 설정
// 참고: https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning
const HLS_DEFAULT_OPTION = {
    autoStartLoad: true,           // 자동으로 세그먼트 로딩 시작
    startPosition: 0,              // 재생 시작 위치 (초)
    startFragPrefetch: true,       // 첫 번째 세그먼트 미리 가져오기
    maxBufferLength: 5,            // 최대 버퍼 길이 (초) - 메모리 사용량 제어
    maxBufferSize: 50 * 1000,      // 최대 버퍼 크기 (바이트) - 50KB
    maxMaxBufferLength: 3,         // 버퍼 길이 상한선 (초)
    maxBufferHole: 0.1,            // 버퍼 홀 허용 크기 (초)
    lowLatencyMode: true,          // 저지연 모드 활성화
    initialLiveManifestSize: 1,    // 초기 라이브 매니페스트 크기
    autoLevelEnabled: true,        // 자동 품질 조정 활성화
    startLevel: 1
};
javascript

 

동적 비디오 로딩

브라우저 호환성을 고려한 비디오 로딩 로직이에요. HLS를 지원하지 않는 브라우저에서는 그냥 네이티브 비디오 재생으로 자동 전환되도록 했어요.

const loadHls = () => {
    return new Promise((resolve) => {
        if (Hls.isSupported()) {
            hls.stopLoad();
            hls.loadSource(url);
            hls.attachMedia(shortsVideo);
            hls.startLoad(0);

            shortsVideo.onloadedmetadata = () => {
                resolve();
            };
        } else {
            shortsVideo.src = url;
            resolve();
        }
    });
};
javascript

 

핵심 기능

숏츠 플랫폼에서 사용자 경험을 좌우하는 핵심 기능들이에요. 무한 스크롤, 다양한 입력 방식, 그리고 자동 음소거 기능으로 직관적이고 편리하게 쓸 수 있도록 만들었어요.

1. 무한 스크롤

사용자가 마지막 영상에 도달하면 자동으로 다음 페이지 콘텐츠를 가져와요. 끊김 없이 계속 볼 수 있도록 해줘요.

const isLastSlide = newIndex === (this.splide.length - 1);
if (isLastSlide && (!this.isLastPage || this.allCategoryMode)) {
    await this.setSlides();
}
javascript

 

2. 다양한 입력 방식 지원

키보드 화살표, 마우스 휠, 터치 제스처 등등 여러 방식으로 영상을 넘길 수 있어요. 사용자가 편한 방식으로 자연스럽게 조작할 수 있도록 했어요.

// 키보드, 마우스 휠, 터치 제스처 통합 지원
document.addEventListener('keydown', this.handleKeydown.bind(this));
document.addEventListener('wheel', this.handleWheel);
this.shortsList.addEventListener('touchstart', this.handleDragStart.bind(this));
this.shortsList.addEventListener('touchend', this.handleDragEnd.bind(this));
javascript

 

3. 음소거

브라우저의 자동재생 정책 때문에 초기에는 음소거 상태로 재생을 시작해요. 특히 Chrome의 경우 사용자가 뭔가 클릭하기 전까지는 소리가 있는 영상을 자동재생할 수 없어서 꽤 애를 먹었어요. 결국 음소거로 시작해서 사용자가 클릭하면 소리를 켜는 방식으로 해결했어요.

playVideo(isMuted, biReq = true) {
    shortsVideo.muted = isMuted;
    if (isMuted) {
        shortsVideo.play().catch((e) => {
            console.log('play error', e);
        });
    } else {
        shortsVideo.play()
            .then(() => {
                shortsAudio.classList.remove('muted');
            })
            .catch(() => {
                shortsVideo.muted = true;
                shortsVideo.play();
                shortsAudio.classList.add('muted');
            });
    }
}
javascript

 

반응형

 

성능 최적화 및 문제 해결

 

사용자들한테서 “첫 로딩이랑 비디오 넘길 때 너무 느리다”는 피드백을 받아서 다음과 같은 최적화 작업을 진행했어요. 특히 인도 쪽 서비스를 하다 보니 네트워크 환경이 한국보다 불안정해서 더 신경 써야 했어요.

 

1. 초기 로딩 속도 개선

사용자 피드백을 바탕으로 첫 로딩 시간을 확 줄여본 최적화 방법들이에요. 인도 지역의 느린 네트워크 환경을 고려해서 썸네일을 먼저 보여주고 네트워크 상황에 맞춰서 적응형 로딩을 구현했어요.

썸네일 우선 렌더링

const optimizeInitialLoading = (file) => {
    let thumbnail = file[0].thumbnail;
    if (CommonUtil.checkUndefinedText(thumbnail)) {
        const shortsLink = file[0].encoded_url;
        thumbnail = shortsLink.substring(0, shortsLink.lastIndexOf('/') + 1) + THUMBNAIL_FILE_NAME;
    }

    // 썸네일을 먼저 보여줘서 바로 시각적 피드백을 제공해요
    // 실제 로딩 시간은 줄지 않지만 UX적으로 빨라 보이는 효과가 있어요
    const videoElement = document.querySelector('.shorts-video');
    videoElement.poster = thumbnail;

    // 백그라운드에서 비디오를 로드해요
    setTimeout(() => {
        loadHls();
    }, 100);
};
javascript

 

네트워크 기반 HLS 로딩

const adaptiveInitialLoading = () => {
    const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;

    if (connection) {
        let initialLevel = 1;

        // 네트워크 타입에 따라서 초기 품질을 설정해요
        // 인도 지역의 3G 환경을 고려해서 더 보수적으로 설정
        switch(connection.effectiveType) {
            case 'slow-2g':
            case '2g':
                initialLevel = 0; // 480p
                break;
            case '3g':
                initialLevel = 0; // 인도 3G는 480p로 시작
                break;
            case '4g':
                initialLevel = 1; // 720p
                break;
            default:
                initialLevel = 0; // 안전하게 480p로 시작
        }

        HLS_DEFAULT_OPTION.startLevel = initialLevel;

        // 대역폭에 따라서 버퍼를 최적화해요
        // 인도 네트워크 환경에 맞춰 버퍼 크기 조정
        if (connection.downlink) {
            if (connection.downlink < 1.5) {
                HLS_DEFAULT_OPTION.maxBufferLength = 2; // 더 작은 버퍼
            } else if (connection.downlink > 10) {
                HLS_DEFAULT_OPTION.maxBufferLength = 6; // 보수적 버퍼
            }
        }
    }

    HLS_DEFAULT_OPTION.autoLevelEnabled = true;
};
javascript

 

 

2. 프리로딩

다음 영상을 미리 로드해서 사용자가 스크롤할 때 바로 재생할 수 있도록 했어요. 끊김 없이 연속으로 볼 수 있어요.

const preloadNextVideo = () => {
    const nextIndex = this.activeIndex + 1;
    if (nextIndex < this.posts.length) {
        const nextVideoUrl = this.posts[nextIndex].file[0].encoded_url;

        const preloadVideo = document.createElement('video');
        preloadVideo.preload = 'metadata';
        preloadVideo.src = nextVideoUrl;

        preloadVideo.addEventListener('loadedmetadata', () => {
            this.preloadedVideos[nextIndex] = preloadVideo;
        });
    }
};
javascript

 

 

3. 메모리 관리

오래 쓰다 보면 생길 수 있는 메모리 누수를 방지해요. 안 쓰는 비디오 리소스들을 정리해서 계속 안정적으로 돌아가도록 했어요.

const cleanupVideoResources = () => {
    if (hls) {
        hls.stopLoad();
        hls.detachMedia();
        hls.destroy();
        hls = null;
    }

    if (shortsVideo) {
        shortsVideo.pause();
        shortsVideo.removeAttribute('src');
        shortsVideo.load();
    }

    Object.keys(this.preloadedVideos).forEach(key => {
        const video = this.preloadedVideos[key];
        video.pause();
        video.removeAttribute('src');
        video.load();
        delete this.preloadedVideos[key];
    });
};

window.addEventListener('beforeunload', cleanupVideoResources);
javascript

 

분석 및 모니터링

BI 데이터 수집

사용자가 영상을 어떻게 보는지, 얼마나 오래 보는지, 어디서 나가는지 등등을 상세하게 추적해요. 이 데이터로 콘텐츠 추천 알고리즘이랑 UX 개선에 활용하고 있어요.

sendBiData(status, idx = this.activeIndex, isLeave = false, leaveAction = '') {
    if (status === 'start') {
        this.bi_start_time_obj = {
            id: this.posts[this.activeIndex].post_id,
            start_time: new Date().getTime()
        };
    }

    biAssistance.sendShortFormBiData({
        status: status,
        duration: this.bi_total_play_time,
        current_play_time: this.bi_video_current_time,
        element_id_number: idx + 1,
        start_time_stamp: this.bi_start_time_obj.start_time,
        is_leave: isLeave,
        leave_action: leaveAction,
        is_mobile: Config.instance.DEVICE.isMobile
    }, this.posts[idx]);
}
javascript

 

최적화 전후 비교

성능 최적화 작업의 정량적 결과에요. 로딩 시간, 버퍼링, 사용자 참여도 등 핵심 지표에서 꽤 괜찮은 개선을 이뤄냈어요.

지표초기 버전개선 버전개선율

평균 로딩 시간 2.1초 0.3초 85% 개선
버퍼링 발생률 4.8% 1.2% 75% 감소
사용자 체류 시간 +40% +78% 38%p 추가 증가
완주율 65% 82% 17%p 증가

 

사용자 경험 개선

기술적 최적화가 실제 사용자 경험에 미친 좋은 영향들이에요.
빠른 로딩과 안정적인 재생으로 사용자 만족도가 많이 올라갔어요.

  • 즉시 재생: 썸네일 우선 렌더링으로 체감 로딩 시간 단축
  • 끊김 없는 재생: 적응형 품질 조절로 네트워크 변화에 대응
  • 부드러운 전환: 프리로딩으로 다음 영상 즉시 재생
  • 안정적인 성능: 메모리 누수 방지로 장시간 사용 가능

 

맺으면서

 

이번 프로젝트를 통해 영상 스트리밍이 생각보다 복잡한 영역이라는 걸 깨달았습니다.
단순히 영상을 재생하는 것 같지만, 네트워크 상황, 브라우저 정책, 사용자 경험까지 고려해야 할 요소가 정말 많더라고요.

특히 사용자들의 “느리다”는 피드백을 받고 최적화 작업을 진행하면서, 개발자 환경에서는 괜찮아 보이던 것들이 실제 사용자 환경에서는 전혀 다를 수 있다는 점을 다시 한번 느꼈습니다. 앞으로는 성능 지표를 더 꼼꼼히 모니터링하고, 사용자 피드백에 더 귀 기울여야겠다는 생각이 듭니다.

 

 


https://leeworms.github.io/post/hls-shorts-platform/

 

AWS MediaConvert로 숏츠 플랫폼 HLS 스트리밍 구현

유튜브 숏츠 같은 서비스를 만들어보자! AWS MediaConvert 자동 인코딩부터 HLS.js 최적화까지, 실제 프로덕션 환경에서 검증된 영상 스트리밍 아키텍처와 성능 개선 노하우를 공유합니다.

leeworms.github.io

 

 

 

반응형