이미지 로딩 중...

WebRTC 녹화 기능 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 20. · 3 Views

WebRTC 녹화 기능 완벽 가이드

WebRTC를 사용한 화면 및 오디오 녹화 기능을 초급자도 쉽게 이해할 수 있도록 설명합니다. MediaRecorder API 사용법부터 서버 측 녹화, 클라우드 스토리지 업로드까지 실무에서 바로 활용할 수 있는 완벽한 가이드를 제공합니다.


목차

  1. MediaRecorder API 사용
  2. 화면+오디오 녹화
  3. 녹화 시작/중지/일시정지
  4. 녹화 파일 다운로드
  5. 서버 측 녹화
  6. 클라우드 스토리지 업로드

1. MediaRecorder API 사용

시작하며

여러분이 화상 회의 앱을 만들거나 온라인 교육 플랫폼을 개발할 때 이런 상황을 겪어본 적 있나요? 사용자가 "회의 내용을 녹화하고 싶어요"라고 요청하거나, "강의를 나중에 다시 볼 수 있게 저장하고 싶어요"라는 피드백을 받은 경험 말입니다.

이런 문제는 실제 개발 현장에서 매우 자주 발생합니다. 사용자들은 중요한 내용을 기록하고 싶어하지만, 별도의 녹화 프로그램을 설치하는 것은 번거롭고 복잡합니다.

또한 웹 브라우저에서 바로 녹화할 수 있다면 훨씬 편리하겠죠. 바로 이럴 때 필요한 것이 MediaRecorder API입니다.

이 API를 사용하면 별도의 프로그램 없이 웹 브라우저에서 직접 화면과 오디오를 녹화할 수 있습니다.

개요

간단히 말해서, MediaRecorder API는 웹 브라우저에서 미디어 스트림(비디오, 오디오)을 녹화할 수 있게 해주는 강력한 도구입니다. MediaRecorder API가 필요한 이유는 웹 애플리케이션에서 사용자의 화면이나 음성을 녹화해야 하는 경우가 많기 때문입니다.

예를 들어, 화상 회의 앱에서 회의 내용을 저장하거나, 온라인 교육 플랫폼에서 강의를 녹화하는 경우에 매우 유용합니다. 기존에는 녹화를 위해 별도의 데스크톱 프로그램을 설치하거나 복잡한 서버 설정이 필요했다면, 이제는 단 몇 줄의 JavaScript 코드만으로 브라우저에서 바로 녹화 기능을 구현할 수 있습니다.

MediaRecorder API의 핵심 특징은 첫째, 브라우저 네이티브 API라서 별도의 라이브러리 설치가 필요 없고, 둘째, 다양한 포맷(webm, mp4 등)을 지원하며, 셋째, 실시간으로 데이터를 처리할 수 있다는 점입니다. 이러한 특징들이 개발자에게 유연하고 강력한 녹화 기능을 제공합니다.

코드 예제

// 카메라와 마이크 스트림을 가져옵니다
const stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
});

// MediaRecorder 인스턴스를 생성합니다
const mediaRecorder = new MediaRecorder(stream, {
  mimeType: 'video/webm;codecs=vp9'
});

// 녹화된 데이터를 저장할 배열
const chunks = [];

// 데이터가 사용 가능할 때마다 chunks 배열에 추가
mediaRecorder.ondataavailable = (event) => {
  if (event.data.size > 0) {
    chunks.push(event.data);
  }
};

// 녹화 시작
mediaRecorder.start();

설명

이것이 하는 일: 위 코드는 사용자의 카메라와 마이크에서 미디어 스트림을 가져와서 MediaRecorder 객체를 생성하고, 녹화를 시작하는 전체 과정을 보여줍니다. 첫 번째로, navigator.mediaDevices.getUserMedia()는 사용자의 카메라와 마이크에 접근합니다.

이 함수는 사용자에게 권한을 요청하는 브라우저 팝업을 띄우고, 사용자가 허용하면 미디어 스트림 객체를 반환합니다. video: true는 카메라를, audio: true는 마이크를 의미합니다.

그 다음으로, new MediaRecorder(stream)이 실행되면서 녹화를 담당하는 객체가 생성됩니다. mimeType 옵션은 녹화될 파일의 포맷을 지정하는데, 'video/webm;codecs=vp9'는 고품질의 webm 포맷을 의미합니다.

내부적으로 MediaRecorder는 스트림의 오디오와 비디오 트랙을 분석하고 인코딩 준비를 합니다. 세 번째로, ondataavailable 이벤트 핸들러가 설정됩니다.

녹화가 진행되는 동안 일정한 시간 간격으로 녹화된 데이터 조각(chunk)이 생성되는데, 이 이벤트가 발생할 때마다 chunks 배열에 데이터를 저장합니다. 마지막으로 start() 메서드가 실제 녹화를 시작하여 미디어 스트림을 캡처하기 시작합니다.

여러분이 이 코드를 사용하면 웹 애플리케이션에서 바로 녹화 기능을 구현할 수 있고, 사용자 경험을 크게 향상시킬 수 있습니다. 또한 녹화된 데이터를 실시간으로 처리하거나 서버에 업로드하는 등 다양한 활용이 가능합니다.

실전 팁

💡 브라우저 호환성을 반드시 확인하세요. Chrome, Firefox, Edge는 잘 지원하지만 Safari는 일부 제약이 있을 수 있습니다. MediaRecorder.isTypeSupported() 메서드로 지원 여부를 미리 체크하는 것이 좋습니다.

💡 mimeType을 지정할 때 흔한 실수는 브라우저가 지원하지 않는 코덱을 사용하는 것입니다. 먼저 'video/webm'처럼 기본 포맷으로 시작하고, 필요하면 'video/webm;codecs=vp8' 같은 특정 코덱을 지정하세요.

💡 메모리 관리를 위해 start() 메서드에 timeslice 파라미터를 전달하세요. 예: mediaRecorder.start(1000)은 1초마다 데이터를 생성합니다. 이렇게 하면 대용량 파일을 한 번에 메모리에 올리지 않아 성능이 좋아집니다.

💡 사용자가 녹화를 종료한 후에는 반드시 stream.getTracks().forEach(track => track.stop())으로 미디어 트랙을 정리하세요. 그렇지 않으면 카메라 표시등이 계속 켜져 있어 사용자가 불안해할 수 있습니다.

💡 에러 처리를 꼭 추가하세요. mediaRecorder.onerror 이벤트 핸들러를 설정하여 녹화 중 발생할 수 있는 문제를 사용자에게 친절하게 알려주세요.


2. 화면+오디오 녹화

시작하며

여러분이 화면 공유 기능이 있는 프레젠테이션 앱을 만들 때 이런 요청을 받아본 적 있나요? "발표자의 화면과 음성을 동시에 녹화하고 싶어요"라는 요구사항 말입니다.

단순히 카메라만 녹화하는 것이 아니라 전체 화면이나 특정 창을 녹화해야 하는 경우가 많습니다. 이런 문제는 온라인 교육, 원격 근무, 기술 지원 등 다양한 분야에서 발생합니다.

사용자는 자신의 화면을 보여주면서 동시에 설명을 하고 싶어하는데, 이 두 가지를 따로 녹화하면 나중에 합치는 작업이 번거롭고 품질도 떨어질 수 있습니다. 바로 이럴 때 필요한 것이 화면과 오디오를 동시에 녹화하는 기능입니다.

getDisplayMedia() API를 사용하면 사용자의 화면과 시스템 오디오, 마이크 음성을 모두 캡처할 수 있습니다.

개요

간단히 말해서, 화면+오디오 녹화는 사용자의 화면 콘텐츠와 오디오를 동시에 캡처하여 하나의 비디오 파일로 저장하는 기술입니다. 이 기능이 필요한 이유는 실무에서 화면 공유만으로는 부족한 경우가 많기 때문입니다.

예를 들어, 코딩 튜토리얼을 만들거나, 버그 리포트를 위해 화면을 녹화하거나, 고객 지원을 위해 문제 상황을 기록할 때 화면과 음성이 모두 필요합니다. 기존에는 화면 녹화를 위해 OBS나 Camtasia 같은 무거운 프로그램을 설치해야 했다면, 이제는 웹 브라우저에서 getDisplayMedia() API 한 줄로 화면과 오디오를 동시에 캡처할 수 있습니다.

이 기술의 핵심 특징은 첫째, 사용자가 녹화할 화면(전체 화면, 특정 창, 브라우저 탭)을 선택할 수 있고, 둘째, 시스템 오디오(컴퓨터에서 나는 소리)와 마이크 오디오를 동시에 녹화할 수 있으며, 셋째, 사용자 프라이버시를 보호하기 위해 브라우저가 자동으로 권한 요청 UI를 제공한다는 점입니다. 이러한 특징들이 안전하고 강력한 화면 녹화 경험을 제공합니다.

코드 예제

// 화면 스트림 가져오기 (시스템 오디오 포함)
const displayStream = await navigator.mediaDevices.getDisplayMedia({
  video: { cursor: 'always' },
  audio: { echoCancellation: true, noiseSuppression: true }
});

// 마이크 스트림 가져오기
const audioStream = await navigator.mediaDevices.getUserMedia({
  audio: true
});

// 화면 비디오 트랙과 두 오디오 트랙을 합침
const videoTrack = displayStream.getVideoTracks()[0];
const systemAudio = displayStream.getAudioTracks()[0];
const micAudio = audioStream.getAudioTracks()[0];

// 합친 스트림 생성
const combinedStream = new MediaStream([videoTrack, systemAudio, micAudio]);

// MediaRecorder로 녹화
const recorder = new MediaRecorder(combinedStream);
recorder.start();

설명

이것이 하는 일: 위 코드는 사용자의 화면 콘텐츠, 시스템에서 나는 소리, 마이크 음성을 모두 캡처하여 하나의 녹화 파일로 만드는 전체 과정을 보여줍니다. 첫 번째로, getDisplayMedia()는 사용자에게 어떤 화면을 공유할지 선택하는 다이얼로그를 보여줍니다.

cursor: 'always' 옵션은 마우스 커서도 녹화에 포함시키고, audio 옵션의 echoCancellation과 noiseSuppression은 시스템 오디오의 에코와 노이즈를 제거하여 깨끗한 소리를 녹음합니다. 사용자가 화면을 선택하면 해당 화면의 비디오 트랙과 오디오 트랙을 포함한 스트림이 반환됩니다.

그 다음으로, getUserMedia({ audio: true })가 실행되어 마이크 스트림을 별도로 가져옵니다. 왜 별도로 가져올까요?

화면 공유의 시스템 오디오는 컴퓨터에서 재생되는 소리(예: 유튜브 영상)를 캡처하고, 마이크는 발표자의 목소리를 녹음하기 위함입니다. 이 두 가지를 합치면 완벽한 프레젠테이션 녹화가 됩니다.

세 번째로, getVideoTracks()getAudioTracks() 메서드로 각 스트림에서 필요한 트랙들을 추출합니다. 하나의 스트림은 여러 트랙을 포함할 수 있는데, 우리는 화면의 비디오 트랙 1개와 오디오 트랙 2개(시스템 오디오, 마이크)를 꺼내서 새로운 MediaStream 객체로 합칩니다.

마지막으로, 합쳐진 combinedStream을 MediaRecorder에 전달하여 녹화를 시작합니다. 이제 화면과 모든 오디오가 하나의 파일로 녹화됩니다.

여러분이 이 코드를 사용하면 사용자는 별도의 프로그램 없이 브라우저에서 전문가 수준의 화면 녹화를 할 수 있습니다.

실전 팁

💡 Safari에서는 시스템 오디오 캡처가 제한될 수 있습니다. 크로스 브라우저 호환성을 위해 오디오 트랙 존재 여부를 체크하세요: if (displayStream.getAudioTracks().length > 0)

💡 사용자가 화면 공유를 중단하면 녹화도 자동으로 멈춰야 합니다. videoTrack에 'ended' 이벤트 리스너를 추가하세요: videoTrack.onended = () => recorder.stop()

💡 여러 오디오 트랙을 합칠 때 음량 밸런스를 조절하려면 Web Audio API의 GainNode를 사용하세요. 시스템 오디오는 작게, 마이크는 크게 설정하면 발표자 목소리가 더 명확해집니다.

💡 모바일 브라우저에서는 화면 공유가 제한적일 수 있습니다. 데스크톱 환경인지 먼저 확인하거나, 사용자에게 안내 메시지를 보여주세요.

💡 개인정보 보호를 위해 녹화 시작 전에 사용자에게 명확한 안내를 제공하세요. "화면과 오디오가 녹화됩니다"라는 메시지와 함께 녹화 중임을 표시하는 UI 인디케이터를 추가하는 것이 좋습니다.


3. 녹화 시작/중지/일시정지

시작하며

여러분이 녹화 기능을 구현했는데 사용자가 이렇게 요청하는 경우를 겪어본 적 있나요? "녹화 중에 잠깐 멈췄다가 다시 시작할 수 있으면 좋겠어요" 또는 "실수로 녹화한 부분을 다시 찍고 싶어요"라는 피드백 말입니다.

이런 문제는 실제 사용자 경험에서 매우 중요합니다. 긴 프레젠테이션을 녹화할 때 중간에 쉬거나, 민감한 정보가 나올 때 잠시 멈추거나, 실수한 부분을 재녹화하고 싶은 경우가 자주 발생합니다.

단순히 시작과 정지만 가능한 녹화는 사용자에게 불편함을 줍니다. 바로 이럴 때 필요한 것이 녹화 제어 기능입니다.

MediaRecorder는 start(), stop(), pause(), resume() 메서드를 제공하여 녹화를 완벽하게 제어할 수 있게 해줍니다.

개요

간단히 말해서, 녹화 제어 기능은 녹화를 시작하고, 일시정지하고, 재개하고, 완전히 종료하는 모든 과정을 프로그래밍 방식으로 관리하는 것입니다. 이 기능이 필요한 이유는 사용자에게 유연한 녹화 경험을 제공하기 위함입니다.

예를 들어, 교육 비디오를 녹화할 때 강사가 중간에 쉬고 싶거나, 화상 회의에서 민감한 정보를 논의할 때 녹화를 잠시 중단하고 싶은 경우, 또는 녹화 오류가 발생했을 때 즉시 중지하고 싶은 경우에 매우 유용합니다. 기존에는 녹화를 제어하기 위해 복잡한 상태 관리와 타이머가 필요했다면, 이제는 MediaRecorder의 내장 메서드와 상태 속성(state)을 사용하여 간단하고 안전하게 녹화를 제어할 수 있습니다.

이 기능의 핵심 특징은 첫째, 녹화 상태(recording, paused, inactive)를 실시간으로 추적할 수 있고, 둘째, 각 상태 전환 시 이벤트(onstart, onpause, onresume, onstop)가 발생하여 UI를 업데이트하기 쉬우며, 셋째, pause()와 resume()을 사용하면 하나의 연속된 파일로 녹화할 수 있다는 점입니다. 이러한 특징들이 전문적인 녹화 애플리케이션을 만들 수 있게 해줍니다.

코드 예제

let mediaRecorder;
let recordedChunks = [];

// 녹화 시작
function startRecording(stream) {
  recordedChunks = [];
  mediaRecorder = new MediaRecorder(stream);

  mediaRecorder.ondataavailable = (e) => {
    if (e.data.size > 0) recordedChunks.push(e.data);
  };

  mediaRecorder.onstart = () => {
    console.log('녹화 시작됨');
    updateUI('recording');
  };

  mediaRecorder.start(100); // 100ms마다 데이터 생성
}

// 일시정지
function pauseRecording() {
  if (mediaRecorder.state === 'recording') {
    mediaRecorder.pause();
    updateUI('paused');
  }
}

// 재개
function resumeRecording() {
  if (mediaRecorder.state === 'paused') {
    mediaRecorder.resume();
    updateUI('recording');
  }
}

// 중지
function stopRecording() {
  if (mediaRecorder.state !== 'inactive') {
    mediaRecorder.stop();
    updateUI('stopped');
  }
}

설명

이것이 하는 일: 위 코드는 녹화의 전체 생명주기를 관리하는 네 가지 핵심 함수를 보여줍니다. 시작, 일시정지, 재개, 중지를 안전하게 처리합니다.

첫 번째로, startRecording() 함수는 새로운 녹화 세션을 시작합니다. recordedChunks 배열을 비워서 이전 녹화 데이터를 제거하고, MediaRecorder 인스턴스를 생성합니다.

start(100) 메서드는 100밀리초마다 데이터를 생성하라는 의미인데, 이렇게 하면 메모리에 모든 데이터를 한 번에 올리지 않아 대용량 녹화도 안전하게 처리할 수 있습니다. 그 다음으로, pauseRecording() 함수는 현재 녹화 상태를 확인한 후에만 일시정지를 실행합니다.

mediaRecorder.state === 'recording' 체크가 중요한 이유는, 이미 일시정지 상태이거나 중지된 상태에서 pause()를 호출하면 에러가 발생하기 때문입니다. 일시정지는 녹화 스트림을 유지하면서 데이터 캡처만 멈춥니다.

세 번째로, resumeRecording() 함수는 일시정지된 녹화를 재개합니다. pause()와 마찬가지로 상태를 먼저 확인하여 안전성을 보장합니다.

resume()이 호출되면 녹화는 멈췄던 지점부터 다시 시작되며, 최종 파일은 일시정지 시간을 제외한 연속된 하나의 비디오가 됩니다. 마지막으로, stopRecording() 함수는 녹화를 완전히 종료합니다.

state가 'inactive'가 아닌지 확인하여 이미 중지된 녹화에 stop()을 다시 호출하는 것을 방지합니다. stop()이 호출되면 onstop 이벤트가 발생하고, 그동안 수집한 recordedChunks를 하나의 Blob으로 합쳐서 파일로 만들 수 있습니다.

여러분이 이 코드를 사용하면 사용자에게 전문가 수준의 녹화 제어 기능을 제공할 수 있습니다. 각 상태 전환마다 updateUI() 함수를 호출하여 버튼 색상을 변경하거나 녹화 시간을 표시하는 등 직관적인 사용자 인터페이스를 만들 수 있습니다.

실전 팁

💡 녹화 상태를 항상 확인한 후에 메서드를 호출하세요. 잘못된 상태에서 pause()나 resume()을 호출하면 브라우저에서 에러를 던집니다. mediaRecorder.state를 활용하여 방어적으로 코딩하세요.

💡 사용자가 실수로 여러 번 버튼을 클릭하는 것을 방지하려면 디바운싱을 적용하거나, 버튼을 비활성화하세요. 예: button.disabled = true 후 상태 전환이 완료되면 다시 활성화.

💡 녹화 시간을 실시간으로 표시하려면 onstart 이벤트에서 setInterval()을 시작하고, onstop에서 clearInterval()하세요. pause() 시에는 타이머를 멈추고, resume() 시 다시 시작하면 정확한 녹화 시간을 보여줄 수 있습니다.

💡 장시간 녹화를 위해 start(timeslice) 파라미터를 적절히 설정하세요. 1000-3000ms가 적당하며, 너무 짧으면 ondataavailable 이벤트가 너무 자주 발생하여 성능에 영향을 줄 수 있습니다.

💡 녹화 중 사용자가 브라우저 탭을 닫으려고 할 때 경고를 표시하세요. window.onbeforeunload 이벤트를 사용하여 "녹화 중입니다. 정말 나가시겠습니까?"라는 메시지를 보여주면 실수로 녹화를 잃는 것을 방지할 수 있습니다.


4. 녹화 파일 다운로드

시작하며

여러분이 녹화 기능을 완벽하게 구현했는데 사용자가 이런 질문을 한 적 있나요? "녹화한 파일은 어디로 가나요?" 또는 "녹화한 영상을 내 컴퓨터에 저장하고 싶어요"라는 요청 말입니다.

녹화를 했지만 파일로 저장하지 못하면 아무 소용이 없겠죠. 이런 문제는 매우 흔합니다.

MediaRecorder로 녹화한 데이터는 메모리에만 존재하기 때문에, 브라우저를 닫거나 페이지를 새로고침하면 사라집니다. 사용자는 녹화한 내용을 영구적으로 보관하고 싶어하는데, 파일 다운로드 기능이 없으면 모든 노력이 물거품이 됩니다.

바로 이럴 때 필요한 것이 녹화 파일 다운로드 기능입니다. Blob API와 URL.createObjectURL()을 사용하면 메모리의 녹화 데이터를 실제 파일로 변환하여 사용자의 컴퓨터에 저장할 수 있습니다.

개요

간단히 말해서, 녹화 파일 다운로드는 메모리에 저장된 녹화 데이터 조각들을 하나의 비디오 파일로 합치고, 사용자가 다운로드할 수 있게 만드는 과정입니다. 이 기능이 필요한 이유는 웹 애플리케이션에서 생성한 데이터를 사용자의 로컬 저장소에 영구적으로 보관하기 위함입니다.

예를 들어, 온라인 교육 플랫폼에서 강의를 녹화한 후 학생들이 다운로드하여 오프라인에서 볼 수 있게 하거나, 화상 회의 녹화본을 참석자들이 각자 저장할 수 있게 하는 경우에 필수적입니다. 기존에는 파일 다운로드를 위해 서버에 업로드한 후 다시 다운로드 링크를 받아야 했다면, 이제는 Blob과 <a> 태그의 download 속성을 사용하여 서버를 거치지 않고 브라우저에서 직접 파일을 다운로드할 수 있습니다.

이 기능의 핵심 특징은 첫째, 서버 없이 클라이언트 측에서만 파일 다운로드가 가능하고, 둘째, Blob URL을 사용하여 메모리 효율적으로 대용량 파일을 처리하며, 셋째, 파일 이름과 확장자를 자유롭게 지정할 수 있다는 점입니다. 이러한 특징들이 빠르고 안전한 파일 다운로드 경험을 제공합니다.

코드 예제

let recordedChunks = [];

// MediaRecorder 설정
mediaRecorder.ondataavailable = (event) => {
  if (event.data.size > 0) {
    recordedChunks.push(event.data);
  }
};

// 녹화 종료 시 파일 다운로드
mediaRecorder.onstop = () => {
  // 모든 chunks를 하나의 Blob으로 합치기
  const blob = new Blob(recordedChunks, {
    type: 'video/webm'
  });

  // Blob URL 생성
  const url = URL.createObjectURL(blob);

  // 다운로드 링크 생성
  const a = document.createElement('a');
  a.style.display = 'none';
  a.href = url;
  a.download = `recording_${Date.now()}.webm`;

  // 다운로드 실행
  document.body.appendChild(a);
  a.click();

  // 메모리 정리
  setTimeout(() => {
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }, 100);
};

설명

이것이 하는 일: 위 코드는 녹화가 종료되면 자동으로 파일을 다운로드하는 완전한 프로세스를 보여줍니다. 데이터 수집부터 파일 생성, 다운로드, 메모리 정리까지 모든 단계를 포함합니다.

첫 번째로, ondataavailable 이벤트 핸들러가 녹화 중에 생성되는 데이터 조각들을 recordedChunks 배열에 수집합니다. MediaRecorder는 녹화를 여러 개의 작은 조각으로 나누어 생성하는데, 이는 메모리 효율성을 위한 것입니다.

각 조각은 Blob 형태의 데이터이며, size가 0보다 큰 경우에만 저장하여 빈 데이터를 필터링합니다. 그 다음으로, onstop 이벤트가 발생하면 수집한 모든 조각들을 new Blob(recordedChunks, { type: 'video/webm' })으로 하나의 완전한 비디오 파일로 합칩니다.

Blob은 Binary Large Object의 약자로, 파일과 유사한 바이너리 데이터 객체입니다. type 속성은 브라우저에게 이것이 webm 포맷의 비디오 파일임을 알려줍니다.

세 번째로, URL.createObjectURL(blob)이 메모리의 Blob 데이터를 가리키는 특별한 URL을 생성합니다. 이 URL은 'blob:http://...' 형식이며, 브라우저만 접근할 수 있는 임시 URL입니다.

그 다음 <a> 태그를 동적으로 생성하고, href에 Blob URL을, download 속성에 파일 이름을 설정합니다. Date.now()를 사용하여 각 녹화마다 고유한 파일 이름을 만듭니다.

마지막으로, a.click()을 프로그래밍 방식으로 호출하여 다운로드를 실행합니다. 사용자는 브라우저의 기본 다운로드 동작(다운로드 폴더에 저장)을 경험하게 됩니다.

다운로드가 시작되면 100ms 후에 <a> 태그를 제거하고 URL.revokeObjectURL()로 Blob URL을 해제하여 메모리 누수를 방지합니다. 이것은 매우 중요한 정리 작업입니다.

여러분이 이 코드를 사용하면 사용자는 녹화 종료 즉시 자동으로 파일을 다운로드할 수 있습니다. 별도의 서버나 스토리지 없이도 완벽하게 작동하며, 사용자 경험이 매우 부드럽습니다.

실전 팁

💡 파일 이름에 타임스탬프와 함께 사용자가 이해하기 쉬운 제목을 추가하세요. 예: meeting_recording_2024-01-20.webm 형식이 recording_1234567890.webm보다 훨씬 친절합니다.

💡 대용량 파일 다운로드 시 브라우저가 느려질 수 있습니다. 다운로드 버튼을 별도로 제공하여 사용자가 원할 때 다운로드하도록 하세요. onstop에서 바로 다운로드하지 말고, 다운로드 버튼 클릭 시 실행되게 변경할 수 있습니다.

💡 Blob URL은 반드시 해제해야 합니다. URL.revokeObjectURL()을 호출하지 않으면 메모리 누수가 발생하여 장시간 사용 시 브라우저가 느려집니다. 다운로드 완료 후 즉시 해제하세요.

💡 모바일 환경에서는 파일 다운로드가 다르게 동작할 수 있습니다. iOS Safari는 직접 다운로드 대신 새 탭에서 비디오를 재생할 수 있습니다. 이 경우 사용자에게 "길게 눌러서 저장" 안내를 제공하세요.

💡 파일 포맷을 변환하고 싶다면 FFmpeg.js 같은 라이브러리를 사용할 수 있습니다. webm을 mp4로 변환하면 더 많은 플레이어에서 재생 가능하지만, 브라우저에서 변환 시 시간이 오래 걸릴 수 있으니 로딩 UI를 제공하세요.


5. 서버 측 녹화

시작하며

여러분이 클라이언트 측 녹화를 구현했는데 이런 문제를 겪어본 적 있나요? "사용자의 인터넷이 불안정해서 녹화 중간에 연결이 끊기면 어떻게 하죠?" 또는 "여러 참가자의 화면을 동시에 녹화하고 싶어요"라는 요구사항 말입니다.

이런 문제는 대규모 화상 회의나 중요한 웨비나에서 심각한 이슈가 됩니다. 클라이언트 측 녹화는 사용자의 디바이스 성능과 네트워크 상태에 의존하기 때문에, 브라우저 크래시나 연결 끊김으로 인해 녹화가 손실될 수 있습니다.

또한 모든 참가자의 화면을 하나로 합치는 것은 클라이언트에서 매우 어렵습니다. 바로 이럴 때 필요한 것이 서버 측 녹화입니다.

WebSocket이나 WebRTC를 통해 미디어 스트림을 서버로 전송하고, 서버에서 안정적으로 녹화하여 저장하는 방식입니다.

개요

간단히 말해서, 서버 측 녹화는 클라이언트에서 캡처한 미디어 스트림을 실시간으로 서버로 전송하고, 서버에서 녹화 및 저장을 담당하는 아키텍처입니다. 이 기능이 필요한 이유는 안정성과 확장성 때문입니다.

예를 들어, 100명이 참가하는 웨비나를 녹화할 때 발표자의 컴퓨터에서 녹화하면 브라우저가 크래시될 위험이 있지만, 서버에서 녹화하면 안정적으로 처리할 수 있습니다. 또한 녹화 파일을 바로 데이터베이스에 저장하거나 클라우드에 업로드하는 등 후처리가 쉽습니다.

기존에는 클라이언트에서 녹화 후 대용량 파일을 서버로 업로드해야 했다면, 이제는 실시간으로 스트림을 전송하여 서버에서 바로 녹화하므로 업로드 시간이 필요 없고, 네트워크 대역폭도 효율적으로 사용할 수 있습니다. 서버 측 녹화의 핵심 특징은 첫째, 클라이언트 성능과 무관하게 안정적으로 녹화하고, 둘째, 여러 스트림을 서버에서 합성(mixing)할 수 있으며, 셋째, 녹화 실패 시 서버에서 자동 재시도나 백업이 가능하다는 점입니다.

이러한 특징들이 엔터프라이즈급 녹화 솔루션을 가능하게 합니다.

코드 예제

// 클라이언트 측: 스트림을 서버로 전송
const socket = io('https://your-server.com');
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

// MediaRecorder로 데이터를 서버로 스트리밍
const mediaRecorder = new MediaRecorder(stream);

mediaRecorder.ondataavailable = (event) => {
  if (event.data.size > 0) {
    // 실시간으로 녹화 데이터를 서버로 전송
    socket.emit('recording-data', event.data);
  }
};

// 1초마다 데이터 전송
mediaRecorder.start(1000);

// 녹화 시작 알림
socket.emit('start-recording', { userId: 'user123', roomId: 'room456' });

// 녹화 종료
function stopServerRecording() {
  mediaRecorder.stop();
  socket.emit('stop-recording');
}

// 서버 측 (Node.js + Socket.io 예시)
const fs = require('fs');
const io = require('socket.io')(server);

io.on('connection', (socket) => {
  let writeStream;

  socket.on('start-recording', (data) => {
    // 파일 스트림 생성
    const filename = `recordings/${data.roomId}_${Date.now()}.webm`;
    writeStream = fs.createWriteStream(filename);
  });

  socket.on('recording-data', (chunk) => {
    // 받은 데이터를 파일에 쓰기
    if (writeStream) {
      writeStream.write(Buffer.from(chunk));
    }
  });

  socket.on('stop-recording', () => {
    if (writeStream) {
      writeStream.end();
      console.log('녹화 완료');
    }
  });
});

설명

이것이 하는 일: 위 코드는 클라이언트-서버 간 실시간 녹화 스트리밍 시스템을 구현합니다. 클라이언트는 녹화 데이터를 생성하여 전송하고, 서버는 이를 받아서 파일로 저장합니다.

첫 번째로, 클라이언트 측에서 Socket.io를 사용하여 서버와 실시간 양방향 통신 연결을 맺습니다. getUserMedia()로 미디어 스트림을 가져온 후, MediaRecorder를 생성합니다.

여기서 중요한 점은 start(1000)으로 1초마다 데이터를 생성하도록 설정한 것입니다. 이렇게 하면 네트워크를 통해 작은 조각으로 나누어 전송할 수 있어 대용량 파일도 안정적으로 처리됩니다.

그 다음으로, ondataavailable 이벤트가 1초마다 발생할 때마다 socket.emit('recording-data', event.data)로 녹화 데이터를 서버로 전송합니다. event.data는 Blob 객체인데, Socket.io는 자동으로 Blob을 직렬화하여 네트워크를 통해 전송합니다.

'start-recording' 이벤트도 함께 전송하여 서버에게 어떤 사용자가 어떤 방에서 녹화를 시작했는지 메타데이터를 제공합니다. 세 번째로, 서버 측에서는 Socket.io로 클라이언트의 연결을 받습니다.

'start-recording' 이벤트를 받으면 fs.createWriteStream()으로 파일 쓰기 스트림을 생성합니다. 파일 이름은 roomId와 타임스탬프를 조합하여 고유하게 만듭니다.

이 스트림은 데이터를 받을 때마다 디스크에 바로 쓰기 때문에 메모리를 많이 사용하지 않습니다. 마지막으로, 'recording-data' 이벤트를 받을 때마다 writeStream.write()로 받은 Blob 데이터를 파일에 씁니다.

Buffer.from(chunk)는 Blob을 Node.js의 Buffer 형식으로 변환하는 과정입니다. 'stop-recording' 이벤트를 받으면 writeStream.end()로 파일을 닫아서 녹화를 완료합니다.

여러분이 이 코드를 사용하면 클라이언트가 중간에 연결이 끊겨도 서버에는 이미 전송된 데이터가 저장되어 있어 부분적으로라도 녹화를 복구할 수 있습니다. 또한 여러 클라이언트의 스트림을 서버에서 받아서 FFmpeg 같은 도구로 합성할 수도 있습니다.

실전 팁

💡 네트워크 재연결 로직을 반드시 추가하세요. Socket.io의 'disconnect' 이벤트를 감지하여 자동으로 재연결을 시도하고, 녹화 중단 지점부터 다시 시작할 수 있도록 구현하세요.

💡 서버 부하를 줄이기 위해 스트림 데이터를 압축하세요. MediaRecorder의 videoBitsPerSecond와 audioBitsPerSecond 옵션을 조절하여 품질과 파일 크기의 균형을 맞추세요. 예: { videoBitsPerSecond: 2500000 } (2.5Mbps)

💡 서버 측에서 디스크 공간을 모니터링하세요. 녹화 시작 전 사용 가능한 디스크 용량을 확인하고, 부족하면 클라이언트에게 에러를 반환하여 녹화 실패를 방지하세요. fs.statfs()를 활용할 수 있습니다.

💡 보안을 위해 인증된 사용자만 녹화를 시작할 수 있도록 JWT 토큰 검증을 추가하세요. Socket.io의 미들웨어를 사용하여 연결 시 토큰을 검증하고, 유효하지 않으면 연결을 거부하세요.

💡 대규모 서비스에서는 녹화 서버를 별도로 분리하세요. 메인 웹 서버와 녹화 전용 서버를 나누어 부하를 분산하고, 녹화 서버는 오토스케일링을 적용하여 동시 녹화 수에 따라 서버를 자동으로 증설하세요.


6. 클라우드 스토리지 업로드

시작하며

여러분이 서버에 녹화 파일을 저장했는데 이런 고민을 해본 적 있나요? "서버 디스크가 금방 꽉 차서 관리가 힘들어요" 또는 "녹화 파일을 전 세계 사용자가 빠르게 다운로드할 수 있게 하고 싶어요"라는 요구사항 말입니다.

이런 문제는 녹화 서비스가 성장하면서 필연적으로 발생합니다. 로컬 서버 디스크는 용량이 제한적이고, 백업과 관리가 어려우며, 서버가 다운되면 모든 파일이 위험에 처합니다.

또한 전 세계 사용자에게 빠른 다운로드를 제공하려면 CDN이 필요한데, 로컬 서버로는 한계가 있습니다. 바로 이럴 때 필요한 것이 클라우드 스토리지 업로드입니다.

AWS S3, Google Cloud Storage, Azure Blob Storage 같은 클라우드 서비스에 녹화 파일을 업로드하면 무제한 확장성, 자동 백업, 글로벌 CDN을 모두 활용할 수 있습니다.

개요

간단히 말해서, 클라우드 스토리지 업로드는 녹화된 비디오 파일을 로컬 서버 대신 AWS S3 같은 클라우드 객체 스토리지에 저장하는 방식입니다. 이 기능이 필요한 이유는 확장성, 안정성, 비용 효율성 때문입니다.

예를 들어, 매일 수천 개의 녹화 파일이 생성되는 교육 플랫폼에서는 로컬 서버 디스크로는 감당할 수 없지만, S3는 무제한 확장이 가능하고 사용한 만큼만 비용을 지불합니다. 또한 자동으로 여러 지역에 백업되어 데이터 손실 위험이 거의 없습니다.

기존에는 파일 서버를 직접 구축하고 용량을 미리 프로비저닝해야 했다면, 이제는 클라우드 스토리지에 업로드만 하면 자동으로 확장되고, CloudFront나 CloudFlare 같은 CDN과 연동하여 전 세계 어디서든 빠른 다운로드를 제공할 수 있습니다. 클라우드 스토리지 업로드의 핵심 특징은 첫째, 사실상 무제한 용량을 제공하여 스토리지 걱정이 없고, 둘째, 99.999999999%(11 nines)의 내구성으로 데이터 손실 위험이 극히 낮으며, 셋째, Presigned URL이나 Multipart Upload 같은 고급 기능으로 대용량 파일도 안전하게 업로드할 수 있다는 점입니다.

이러한 특징들이 프로덕션 환경에 적합한 녹화 스토리지 솔루션을 제공합니다.

코드 예제

// 클라이언트 측: Presigned URL 방식으로 직접 S3에 업로드
async function uploadToS3(blob) {
  // 서버에서 Presigned URL 받기
  const response = await fetch('/api/get-upload-url', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      filename: `recording_${Date.now()}.webm`,
      contentType: 'video/webm'
    })
  });

  const { uploadUrl, fileUrl } = await response.json();

  // S3에 직접 업로드 (서버를 거치지 않음)
  await fetch(uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': 'video/webm' },
    body: blob
  });

  console.log('업로드 완료:', fileUrl);
  return fileUrl;
}

// 녹화 완료 후 업로드
mediaRecorder.onstop = async () => {
  const blob = new Blob(recordedChunks, { type: 'video/webm' });
  const url = await uploadToS3(blob);
  alert(`녹화가 저장되었습니다: ${url}`);
};

// 서버 측 (Node.js + AWS SDK v3)
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

const s3Client = new S3Client({ region: 'us-east-1' });

app.post('/api/get-upload-url', async (req, res) => {
  const { filename, contentType } = req.body;

  // S3 업로드 명령 생성
  const command = new PutObjectCommand({
    Bucket: 'my-recordings-bucket',
    Key: `recordings/${filename}`,
    ContentType: contentType
  });

  // Presigned URL 생성 (15분 유효)
  const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 900 });

  // 업로드 후 접근할 파일 URL
  const fileUrl = `https://my-recordings-bucket.s3.amazonaws.com/recordings/${filename}`;

  res.json({ uploadUrl, fileUrl });
});

설명

이것이 하는 일: 위 코드는 서버 대역폭을 소비하지 않고 클라이언트에서 직접 AWS S3로 파일을 업로드하는 현대적인 방식을 보여줍니다. Presigned URL을 활용한 안전하고 효율적인 업로드 패턴입니다.

첫 번째로, 클라이언트는 업로드할 파일의 메타데이터(파일 이름, 콘텐츠 타입)를 서버의 /api/get-upload-url 엔드포인트로 전송합니다. 이 요청은 "S3에 파일을 업로드하고 싶은데, 어디로 업로드해야 하나요?"라고 묻는 것과 같습니다.

Date.now()로 파일 이름을 고유하게 만들어 덮어쓰기를 방지합니다. 그 다음으로, 서버는 AWS SDK의 getSignedUrl() 함수를 사용하여 임시 업로드 권한이 부여된 Presigned URL을 생성합니다.

이 URL은 특정 파일을 15분 동안만 업로드할 수 있는 임시 키와 같습니다. 일반적인 S3 URL과 달리 서명(signature)이 포함되어 있어, AWS 자격증명 없이도 업로드가 가능하지만 시간 제한이 있어 보안이 유지됩니다.

세 번째로, 클라이언트는 받은 uploadUrl로 직접 S3에 PUT 요청을 보냅니다. 이때 body에 Blob 객체를 그대로 전달하는데, 이것이 핵심입니다.

데이터가 클라이언트 → S3로 직접 전송되기 때문에 서버의 네트워크 대역폭과 CPU를 전혀 사용하지 않습니다. 대용량 파일을 여러 사용자가 동시에 업로드해도 서버는 영향을 받지 않습니다.

마지막으로, 업로드가 완료되면 서버가 제공한 fileUrl을 사용하여 나중에 파일에 접근할 수 있습니다. 이 URL을 데이터베이스에 저장하면 사용자가 언제든지 녹화 파일을 다운로드하거나 스트리밍할 수 있습니다.

필요하다면 CloudFront CDN을 연동하여 전 세계 사용자에게 빠른 전송 속도를 제공할 수도 있습니다. 여러분이 이 코드를 사용하면 서버 인프라 비용을 크게 절감하면서도 무제한 확장 가능한 스토리지 시스템을 구축할 수 있습니다.

또한 S3의 Lifecycle Policy를 설정하여 오래된 녹화 파일을 자동으로 Glacier로 이동시켜 스토리지 비용을 더욱 줄일 수 있습니다.

실전 팁

💡 대용량 파일(100MB 이상)은 Multipart Upload를 사용하세요. 파일을 여러 조각으로 나누어 병렬로 업로드하면 속도가 빠르고, 중간에 실패해도 실패한 부분만 재업로드할 수 있습니다. AWS SDK의 createMultipartUpload() API를 활용하세요.

💡 업로드 진행률을 사용자에게 보여주려면 XMLHttpRequest를 사용하세요. Fetch API는 업로드 진행률을 추적할 수 없지만, XMLHttpRequest의 upload.onprogress 이벤트로 실시간 진행률을 표시할 수 있습니다.

💡 S3 버킷에 CORS 정책을 반드시 설정하세요. 클라이언트에서 직접 업로드하려면 S3 버킷의 CORS 설정에서 허용할 도메인과 메서드(PUT, POST)를 명시해야 합니다. 그렇지 않으면 브라우저에서 CORS 에러가 발생합니다.

💡 비용 최적화를 위해 S3 Intelligent-Tiering 스토리지 클래스를 사용하세요. 자주 접근하는 파일은 빠른 스토리지에, 오래된 파일은 저렴한 스토리지에 자동으로 이동시켜 비용을 30% 이상 절감할 수 있습니다.

💡 보안 강화를 위해 업로드된 파일 URL을 공개하지 말고, 별도의 Presigned Download URL을 생성하여 제공하세요. 파일에 접근할 때마다 서버에서 임시 다운로드 링크를 생성하면 권한 관리가 쉽고, 링크 공유로 인한 무단 접근을 방지할 수 있습니다.


#WebRTC#MediaRecorder#화면녹화#오디오녹화#클라우드업로드#WebRTC,녹화

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Docker 배포와 CI/CD 완벽 가이드

Docker를 활용한 컨테이너 배포부터 GitHub Actions를 이용한 자동화 파이프라인까지, 초급 개발자도 쉽게 따라할 수 있는 실전 배포 가이드입니다. AWS EC2에 애플리케이션을 배포하고 SSL 인증서까지 적용하는 전 과정을 다룹니다.

보안 강화 및 테스트 완벽 가이드

웹 애플리케이션의 보안 취약점을 방어하고 안정적인 서비스를 제공하기 위한 실전 보안 기법과 테스트 전략을 다룹니다. XSS, CSRF부터 DDoS 방어, Rate Limiting까지 실무에서 바로 적용 가능한 보안 솔루션을 제공합니다.

Redis 캐싱과 Socket.io 클러스터링 완벽 가이드

실시간 채팅 서비스의 성능을 획기적으로 향상시키는 Redis 캐싱 전략과 Socket.io 클러스터링 방법을 배워봅니다. 다중 서버 환경에서도 안정적으로 작동하는 실시간 애플리케이션을 구축하는 방법을 단계별로 알아봅니다.

반응형 디자인 및 UX 최적화 완벽 가이드

모바일부터 데스크톱까지 완벽하게 대응하는 반응형 웹 디자인과 사용자 경험을 개선하는 실전 기법을 학습합니다. Tailwind CSS를 활용한 빠른 개발부터 다크모드, 무한 스크롤, 스켈레톤 로딩까지 최신 UX 패턴을 실무에 바로 적용할 수 있습니다.

React 채팅 UI 구현 완벽 가이드

실시간 채팅 애플리케이션의 UI를 React로 구현하는 방법을 다룹니다. Socket.io 연동부터 컴포넌트 설계, 상태 관리까지 실무에 바로 적용할 수 있는 내용을 담았습니다.