띵동팀의 SSE 기반 VOD 처리 흐름 구현기

keemsebeen·2025년 6월 20일
post-thumbnail

동아리 피드에 비디오 업로드 기능을 추가하는 과정에서, 예상보다 복잡한 기술적 도전들을 마주하게 되었습니다. 이번 글에서는 VOD 기능 개발 과정에서 겪은 문제들과, 이를 해결하기 위해 Server-Sent Events(SSE)를 도입하게 된 이유, 그리고 실제 구현 과정을 공유하고자 합니다.

Backgrounds — VOD 기능의 도입 배경


기존에는 동아리 피드에 사진만 업로드할 수 있었지만, 보다 생생한 홍보 콘텐츠를 원하는 목소리가 꾸준히 있었습니다. 실제로 설문조사를 통해 일반 학우들뿐만 아니라 동아리 운영진들 역시 영상 콘텐츠를 활용한 홍보 방식에 대한 니즈가 높음을 확인할 수 있었습니다.

이에 따라 운영진(ADMIN)이 피드에 영상을 업로드할 수 있는 기능을 제공하여, 동아리 홍보 콘텐츠의 다양성을 확대하고자 했습니다. 이를 통해 추후 개발 예정인 동아리 지원 프로세스와 결합되어 지원률을 높이는데도 긍정적인 영향을 줄 것으로 기대했습니다.

동영상 처리의 복잡성


하지만 영상 업로드 기능을 구현하려고 보니, 단순한 이미지 업로드와는 다른 복잡한 문제들이 있었습니다. 대표적인 문제는 다음과 같았습니다.

  1. 다양한 동영상 포맷 지원 필요
  2. 용량이 큰 파일의 안정적 처리
  3. 웹에서 재생 가능한 형태(hls)로의 변환
  4. 업로드 실패 또는 중단 처리 등 예외 상황 대응

또, 동영상을 S3에 업로드하는 데 걸리는 평균 시간을 측정해보았는데요. (네트워크 지연이 없다는 가정하에 측정했습니다.) 3분 영상의 경우 해상도에 따라 100-300MB 정도의 용량을 가지므로, 사용자는 최소 20초에서 최대 1분까지 기다려야 합니다. 이런 긴 처리 시간 동안 진행 상황을 알 수 없다면, 업로드가 실패했다고 오해하거나 페이지를 떠날 가능성이 높아집니다.

이처럼 동영상 업로드는 단순히 파일을 서버에 저장하는 수준을 넘어, 업로드 → 변환 → 저장 → 스트리밍 준비까지의 전체 파이프라인을 고려해야 했고, 각 단계마다 사용자에게 적절한 피드백을 제공하는 것이 핵심 과제였습니다.

AWS MediaConverter


띵동 서버팀은 다양한 포맷 지원과 HLS 변환 문제를 해결하기 위해 AWS MediaConverter를 도입했습니다.

AWS MediaConverter는 다양한 입력 형식의 미디어 파일을 웹에서 재생 가능한 형식으로 변환해주는 서비스로, HLS나 MPEG-DASH와 같은 스트리밍 포맷을 지원하며, 변환 결과물을 Amazon S3 버킷에 저장해줍니다.

하지만 MediaConverter는 비동기적으로 작동하기 때문에 변환이 완료되기까지 시간이 소요되며, 변환 결과를 실시간으로 확인하기 어렵다는 단점이 있습니다.

이러한 문제를 해결하기 위해 업로드 상태를 실시간으로 알릴 수 있는 시스템이 필요하다고 판단했고, 이를 바탕으로 기능의 범위(Goals)와 제외 범위(Non-Goals)를 명확히 정의하여 개발의 방향성을 설정했습니다.

Goals - 기능 목표


  1. 동아리 운영진들(ADMIN)은 피드 영상을 업로드할 수 있다.
    • 최대 업로드 동영상 용량은 300MB으로 제한한다.
  2. 동영상 업로드 완료 시, 유저는 알림을 받을 수 있어야 한다.
    • 동영상 업로드 실패 시에도, 실패했다는 알림을 받을 수 있어야 한다.
    • 알림 전송 실패 시 서버에서 알림을 다시 보내야 한다.
      • 클라이언트와 커넥션이 끊겼을 경우, 재연결 후에 밀린 알림을 받을 수 있어야 한다.
      • 일정 시간이 지난 경우에는 완전 실패로 간주하고, 해당 알림은 폐기된다.(실효성이 떨어지기 때문)
  3. 동아리 운영진들(ADMIN)은 한 번에 하나의 피드(동영상) 만 업로드 가능하다.
    • 실패에 대한 재시도는 사용자가 직접 처리하도록 한다.

Non-Goals - 구현에서 제외된 사항


  • 서버측에서 영상 업로드 실패에 대한 자체 재시도는 처리하지 않는다.
  • 피드를 동시에 여러 개 만드는 케이스는 클라이언트에서 정책적으로 금지한다.

실시간 알림을 위한 통신 방식


앞서 언급했듯, 영상 업로드는 단순 저장을 넘어 변환과 스트리밍 준비까지 포함된 다단계 비동기 처리가 필요합니다. 특히 AWS MediaConverter는 변환이 완료될 때까지 시간이 걸리며, 이 과정을 사용자가 직접 확인할 수 없습니다.

사용자는 이 기다림 동안 어떤 일이 진행되고 있는지 실시간 피드백을 받을 수 있어야 하고, 서버는 업로드 성공/실패 여부를 즉시 사용자에게 알려야 합니다.

이처럼 서버에서 발생한 상태 변화를 즉시 클라이언트에 전달해야 했기 때문에, 비연결성을 특징(요청과 응답이 한 번 이루어진 후에는 연결이 종료)으로 갖고 있는 HTTP는 한계가 있었습니다. 따라서 서버에서 클라이언트로 업로드 완료 여부를 실시간으로 알리기 위해서는 별도의 통신 방식이 필요했습니다.

이에 따라, 지속적인 연결을 통해 서버에서 클라이언트로 실시간 알림을 푸시할 수 있는 통신 방식이 필요했고, 이를 해결하기 위해 어떤 기술을 사용할지 고민하게 되었습니다.

아래는 이러한 요구사항을 해결하기 위해 고려했던 주요 통신 방식들의 특징과 차이점을 정리한 것입니다.

Polling

Polling은 클라이언트가 일정 시간 간격으로 서버에 요청을 보내 변화가 있는지 상태를 확인하는 방식입니다. 예를 들어, 3초에 한 번씩 서버에 “업로드 완료됐나요?“라고 계속 묻는 구조입니다.

장점

  • 구현이 매우 간단합니다. 서버는 평소처럼 HTTP 요청을 처리하면 되고, 클라이언트도 setInterval 등을 이용해 주기적으로 요청만 하면 됩니다.
  • 특별한 라이브러리나 복잡한 설정 없이 바로 적용할 수 있습니다.

단점

  • 실시간성이 떨어집니다. 예를 들어, 5초 간격으로 확인한다면 최대 5초의 지연이 발생할 수 있습니다.
  • 불필요한 요청이 많아집니다. 상태가 변하지 않았더라도 계속 요청을 보내기 때문에 서버에 부하가 크고, 네트워크 자원이 낭비됩니다.
  • 업로드 같은 시간이 오래 걸리는 작업에는 과도한 요청이 누적될 수 있어 비효율적입니다.

도입하기 적합한 경우

  • 상태 변화가 자주 일어나지 않고, 어느 정도 지연을 허용할 수 있는 경우
  • 서버나 인프라가 간단하고, 리소스를 여유 있게 사용할 수 있는 경우

WebSocket

WebSocket은 클라이언트와 서버 사이에 하나의 TCP 연결을 지속적으로 유지하면서 양방향 통신이 가능한 프로토콜입니다. 연결이 한 번 수립되면, 서버와 클라이언트는 서로 자유롭게 메시지를 주고받을 수 있습니다.

장점

  • 진짜 실시간 통신이 가능합니다. 서버 상태가 바뀌는 즉시 클라이언트에 알려줄 수 있습니다.
  • 양방향 통신이 가능하므로 채팅, 공동 편집 도구 등 실시간 상호작용이 필요한 서비스에 적합합니다.

단점

  • 구현이 복잡합니다. 서버 측에서는 HTTP와는 별도의 WebSocket 핸들러를 구현해야 하며, 클라이언트도 상태 관리가 필요합니다.
  • 지속적인 연결을 유지해야 하므로, 서버 리소스를 더 많이 사용하게 됩니다.

도입하기 적합한 경우

  • 실시간 채팅, 실시간 주식 가격 알림, 실시간 협업 툴 등 사용자 간 실시간 상호작용이 필요한 경우
  • 복잡한 메시지 흐름이나 명령어 처리(서버에서 클라이언트에게 작업 지시 등)가 필요한 경우

SSE(Server-Sent Events)

SSE는 클라이언트가 서버에 연결을 맺고, 서버가 필요한 정보를 단방향으로 클라이언트에 푸시하는 방식입니다. HTTP 기반으로 동작하며, 클라이언트에서 EventSource 객체를 사용해 쉽게 구현할 수 있습니다.

장점

  • HTTP 기반이라 익숙하고 구현이 간단합니다. 별도의 프로토콜을 도입할 필요 없이 서버에서 text/event-stream 형식으로 응답만 주면 됩니다.
  • 연결이 끊기더라도 자동으로 재연결(reconnect) 기능을 제공합니다.
  • 단방향 푸시 기반이라 알림이나 상태 업데이트 전송에 매우 적합합니다.

단점

  • 단방향 통신만 지원되므로, 클라이언트 → 서버로는 기존 HTTP 요청을 따로 보내야 합니다.

도입하기 적합한 경우

  • 서버에서 클라이언트에게 알림을 실시간으로 보내야 하는 경우 (ex. 주문 상태 업데이트, 알림 시스템)
  • 업로드/변환 완료, 빌드 상태 등 비동기 작업 결과를 서버가 먼저 알려줘야 하는 경우
  • 클라이언트의 요청보다 서버의 이벤트 전송이 중심인 구조

왜 SSE를 선택했는가?

동영상 업로드 기능은 다음과 같은 시나리오를 갖고 있습니다.

  1. 클라이언트가 업로드 요청을 보낸 후
  2. AWS MediaConverter가 백그라운드에서 비동기적으로 인코딩 작업을 수행하며,
  3. 작업 완료 시점에 서버가 클라이언트에게 상태를 알려줘야 한다.

이 상황에서 필요한 것은 다음과 같은 요구사항을 만족하는 통신 방식입니다.

  • 업로드 진행 상태 또는 완료 여부를 실시간으로 전달받을 수 있어야 한다.
  • 클라이언트가 서버에 요청을 반복적으로 보내지 않아도 된다.
  • 상태 변화가 발생했을 때에만 서버가 클라이언트에 단방향으로 알림을 푸시할 수 있으면 충분하다.

즉, 양방향 통신(WebSocket)은 오버엔지니어링이며, 단방향 푸시 기반의 실시간 전송만 필요한 상황이었습니다. 이러한 요구사항을 가장 효율적으로 충족하는 기술이 바아로 SSE였습니다.

결론적으로 이번 기능에서는 복잡한 설정 없이, 클라이언트에서 서버 상태를 실시간으로 전달할 수 있는 방식이 필요했고, SSE는 기술적 요구사항를 고려했을 때 가장 합리적인 선택이었습니다.

구현

EventSource 객체

EventSource브라우저에서 SSE를 사용할 때 쓰는 JavaScript API입니다.

SSE는 서버로부터 데이터를 지속적으로 받아야 하는데, 이때 브라우저가 서버와 스트림을 유지하기 위해 사용하는 객체가 EventSource입니다.

  1. EventSource 인스턴스 생성
const eventSource = new EventSource(url)

서버와의 SSE 연결을 생성합니다. 서버는 해당 url을 통해 인코딩 진행 상태 또는 완료 여부를 스트리밍합니다.

  1. 서버로부터 특정 이벤트 수신
eventSource.addEventListener('connect', (event) => {
	toast.loading('비디오 업로드 중입니다.', { id: toastId });
	// ..
});

connect 는 서버에서 보내는 커스텀 이벤트 이름입니다. 초기 연결에 성공했을 때 해당 이벤트를 클라이언트로 보낼 수 있습니다. 해당 이벤트를 통해 클라이언트에서는 최초 연결 상태를 확인하거나 초기 메시지를 받을 수 있습니다.

  1. 동영상 변환 상태 이벤트 수신
eventSource.addEventListener('sse', (event) => {
  const parsedData = JSON.parse(event.data);
	
	// 변환 완료 처리
	if (parsedData.data.convertJobStatus === 'COMPLETE') {
	  toast.success('피드가 생성되었어요.', { id: toastId });
	  // ..
	  eventSource.close();
	}
	
   // 변환 실패 처리 
	if (parsedData.data.convertJobStatus === 'ERROR') {
	  toast.success('비디오 업로드 중 문제가 발생했습니다.', {
      	id: toastId,
      	duration: 5000,
      });
	  // ..
	  eventSource.close();
	}
});

sse 역시 서버에서 정의한 커스텀 이벤트 이름입니다. event.data는 문자열 형태로 전달되므로, JSON.parse()를 사용해 객체로 변환해야 합니다. 변환이 완료되거나 오류가 발생하면 연결을 종료하기 위해 eventSource.close()를 호출합니다.

  1. 에러처리
eventSource.onerror = () => {
  toast( `동영상 처리에 시간이 조금 더 필요합니다.\n 곧 완료되니 잠시만 기다려 주세요!`);
  // ..
};

SSE 연결 중 에러가 발생하면 자동으로 호출됩니다. 예외 상황에 대해 사용자에게 알릴 수 있습니다.

개발 과정에서의 팁

SSE(Server-Sent Events)를 처음 구현하면서 가장 헷갈렸던 부분 중 하나가 바로 이벤트 이름과 데이터 포맷이었어요.
친절한 백엔드 팀원 덕분에 서버에서 어떤 이벤트 이름으로 어떤 형식의 데이터를 보내는지 명확히 전달받았기 때문에, 훨씬 수월하게 클라이언트 로직을 구현할 수 있었습니다.. 😊 

위 사진에서처럼 event는 서버에서 설정한 커스텀 이벤트 이름이며, data는 전달된 실제 상태 정보(JSON)입니다.

결과

결과적으로 15초 가까이 걸리는 동영상 처리 시간 동안에도 사용자는 자유롭게 서비스를 이용할 수 있으면서, 동시에 처리 결과를 놓치지 않고 실시간으로 확인할 수 있는 매끄러운 사용자 경험을 제공할 수 있게 되었습니다.

SSE 구현을 통해 다양한 문제를 마주하고 하나하나 해결해 나가는 과정은 쉽지만은 않았습니다. 하지만 실제로 제가 다양한 서비스들에서 비디오를 업로드하면서 느꼈던 답답함과 불편함을, 제 서비스를 사용하는 사용자들은 겪지 않기를 바라는 마음으로 개발에 임했기에 더 몰입할 수 있었던 것 같아요.

기술은 결국 사람을 위한 것이라는 점을 다시금 느꼈고, 앞으로도 사용자 입장에서 고민하며 더 나은 경험을 만들기 위해 노력하고 싶습니다. 😊


구현한지는 5달이 지났지만, 이제야 글을 쓰네요. 반성합니다..

profile
프론트엔드 개발자 김세빈입니다. 👩🏻‍💻

0개의 댓글