고양이hyebin
새로고침을 했는데 영상이 통째로 사라졌어요
March 2, 2025

“녹화 중 새로고침을 하니 영상이 통째로 사라졌어요”

이 문제는 저에게 항상 골칫거리였습니다. 녹화 중에 새로고침을 하거나 캠 연결이 끊기면, 촬영한 영상이 그대로 사라지는 문제가 있었습니다. 통째로 사라지게 된 영상으로 중요한 기록이 남아 있지 않게 되었다면, 사용자의 PC문제일지라도 개발자의 잘못이라고 생각합니다. 😭 이를 해결하기 위해 여러 방안을 고민해왔습니다…. 아직 완벽한 해결책을 찾지는 못했지만, 프론트에서 스토리지를 활용해 영상 손실을 최소화하고 최대한 복구할 수 있도록 고민한 과정을 공유하려고 합니다.

회사에서는 프로그램 오류 발생 시 정확한 확인을 위해 녹화 영상을 검토하는 경우가 많습니다. 하지만 현재 녹화 방식은 특정 트리거에 의해 영상이 녹화되고 저장되기 때문에, 저장 과정에서 문제가 발생하면 영상이 통째로 사라지는 문제가 있었습니다. 게다가 네트워크 불안정이나 프로그램 튕김 현상으로 인해 영상이 손실되는 경우도 많았습니다. 이러한 문제를 반드시 개선해야 했지만, 개발 비용을 고려했을 때 쉽지 않은 과제였습니다.

일부 회사에서는 서버에서 실시간 스트리밍 방식으로 영상을 저장해 손실을 줄이는 방식을 사용하고 있었습니다. 하지만 우리 시스템은 사용자의 로컬 환경에서 녹화를 진행하는 구조였기 때문에 영상 손실에 취약했습니다. 서버에서 실시간으로 영상을 저장하는 방법은 안정성이 높았지만, 높은 비용이 들어 현실적으로 어려운 선택지였어요.

영상 데이터를 일정 단위로 쪼개어 서버에 저장하는 방법도 고려했지만, 트래픽 부담을 줄여야 했고, 영상 확인 과정이 복잡해질 우려가 있어 적절한 해결책이 아니라고 판단했습니다.

결국 비용을 최소화하면서도 영상을 최대한 복원할 수 있는 방법을 고민했고, 응시자의 로컬 스토리지를 활용하는 방안을 떠올리게 되었습니다.

본격적인 해결 과정을 알아보기 전에, 먼저 비디오 데이터에 대해 간략히 살펴보겠습니다.

비디오 데이터

비디오 데이터는 단순히 이미지가 빠르게 넘어가는 것이 아니라, 효율적으로 저장하고 전송할 수 있도록 압축된 데이터입니다.

비디오 파일이 만들어 지는 과정

비디오는 프레임이라는 여러 장의 사진이 빠르게 연속되며 만들어집니다. 하지만 비디오 파일을 그냥 저장하면 파일 크기가 너무 커서 다루기 어려워집니다. 그래서 우리는 압축 기술(코덱)을 사용해서 파일 크기를 줄입니다.

프레임

비디오는 프레임이라고 불리는 정지된 이미지들이 빠르게 연속되며 만들어집니다.

예를 들어 1초에 30장의 사진이 재생되면 자연스러운 영상이 됩니다.

🚀 FPS

FPS는 Frames Per Second으로 초당 프레임 수를 나타냅니다.

  • 1초에 30장의 사진이 재생되면 30 FPS라고 합니다.
  • 24~30 FPS → 일반적인 영화, 방송 콘텐츠
  • 60 FPS 이상 → 게임, 스포츠 영상, 실시간 스트리밍

코덱

비디오 데이터를 효율적으로 저장하고 압축하는 기술입니다.

비디오 파일이 너무 크면 저장과 전송이 어렵기 때문에, 코덱을 사용해 데이터를 압축(인코딩)하고 해제(디코딩)합니다.

주요 코덱 종류

  • H.264 : 가장 많이 사용되는 표준 코덱 (유튜브, 스트리밍 플랫폼에서 주로 사용)
  • H.265 (HEVC) → H.264보다 높은 압축률을 제공하여 고화질 영상 저장 가능
  • VP8 / VP9 → 구글이 개발한 웹 최적화 코덱 (WebM 포맷에서 사용)
  • AV1 → 차세대 오픈소스 코덱, 고효율 압축 제공

비디오 파일 포맷

비디오 파일은 단순한 영상이 아니라, 영상 + 소리 + 자막을 담고 있는 컨테이너입니다.

즉, 파일 형식에 따라 저장되는 방식이 달라지기 때문에 비디오 파일을 담는 그릇이라고 생각하면 됩니다.

  • MP4 → 가장 대중적인 포맷, 모든 기기에서 재생 가능
  • WebM → 웹 최적화 포맷, 빠른 로딩
  • MKV → 고화질 지원, 여러 오디오·자막 포함 가능

인코딩 & 디코딩

비디오는 용량이 크기 때문에 저장·전송을 위해 압축(인코딩)하고, 재생할 때 다시 복원(디코딩)해야 합니다.

  • 인코딩(Encoding): 비디오 파일을 압축해 크기를 줄이는 과정
  • 디코딩(Decoding): 압축된 비디오를 원래대로 복원하는 과정

인코딩: 영상을 압축하는 과정

녹화된 원본 영상은 크기가 커서 그대로 저장·전송하기 어렵습니다. 코덱을 이용해 불필요한 데이터를 제거하고 크기를 줄일 수 있습니다.

  • 손실 압축: 화질을 일부 희생하고 용량을 줄임 (MP4, WebM)
  • 비손실 압축: 화질 유지, 파일 크기 큼 (ProRes 등)

예) 원본 영상(10GB) → H.264 코덱으로 인코딩 → 1GB로 압축(MP4 저장)

디코딩: 영상을 해독하여 재생하는 과정

압축된 비디오는 바로 재생할 수 없으므로, 비디오 플레이어가 코덱을 사용해 원래 영상으로 복원해야 합니다.

예) MP4/WebM 파일 불러오기 → 코덱이 압축을 해제하고 프레임 복원 → 화면에 영상 출력

비디오 데이터를 정리하자면

  • 비디오는 프레임(정지된 이미지들)이 빠르게 연속되며 만들어짐
  • FPS가 높을수록 영상이 더 부드러움
  • 코덱은 비디오 데이터를 압축하고 해제하는 기술
  • 포맷은 비디오 파일을 담는 그릇과 같다 (MP4, WebM 등)
  • 인코딩은 파일을 압축하는 과정, 디코딩은 파일을 다시 복원하는 과정

현재 녹화 방식의 문제점과 데이터 손실 원인

새로고침, 네트워크 오류, 캠 연결 문제 등 다양한 이유로 녹화된 데이터가 손실될 가능성이 크기 때문에 단순히 영상을 기록하고 저장하는 것만으로는 충분하지 않았습니다.

현재 녹화 방식이 어떻게 동작하는지 살펴보고, 기존 방식의 한계와 데이터 손실이 발생하는 원인을 분석해보고 개선할 수 있는 방안을 고민해보았습니다.

현재 녹화 프로세스 흐름 (트리거 → 녹화 시작 → 저장)

현재 녹화 방식은 특정 이벤트가 발생하면 녹화를 시작하고, 중지 되면서 저장되는 구조입니다.

  1. 녹화 시작 (트리거 발생)

    • 사용자의 특정 이벤트가 발생하면 녹화가 시작됨

    • MediaRecorder API 또는 RecordRTC 등을 이용해 웹캠 스트림을 녹화

  2. 녹화 진행

    • 비디오 데이터를 실시간으로 인코딩 (보통 WebM, MP4 포맷 사용)

    • 일반적으로 녹화가 끝날 때까지 메모리에 임시 저장됨

  3. 녹화 중지 & 저장

    • 사용자가 녹화 중지 버튼을 클릭하거나 트리거에 의해 최종적으로 파일이 생성됨

    • 브라우저에서 Blob 객체로 데이터를 변환하여 서버 업로드 방식

현재 방식에서 개선 방안 고민

지금 방식은 브라우저는 녹화 데이터를 메모리에 저장하고 있다가, 녹화 종료 후 파일로 변환하는 방식이기 때문에 녹화가 끝나지 않으면 파일이 생성되지 않기 때문에 데이터가 남아있지 않았습니다.

새로고침 후에도 복원할 수 있도록 브라우저에 데이터 임시 저장 기능이 필요했고, 스토리지를 활용한 해결 접근 방식 아이디어를 얻었습니다.

  1. 로컬 스토리지를 활용
  2. 세션 스토리지 왈용
  3. IndexedDB 또는 File API를 활용한 저장 방식

아이디어 방향

→ 녹화 데이터를 10초마다 조각으로 저장

→ 스토리지를 활용하여 임시 저장

→ 녹화 종료 후, 저장된 조각을 합쳐서 재생 가능하도록 구현

비디오 데이터를 변환하면서 발생한 문제들

비디오 데이터 저장을 위해 단순하게 Base64로 변환하여 저장한다면 된다고 생각했어요. 하지만 Base64 저장방식은 적절하지 못했고, 이로 인해 발생할 수 있는 오류들이 있었습니다.

  1. atob() 디코딩 에러 (Base64 → 바이너리 변환 문제)

    • Base64로 데이터를 저장하면 텍스트 문자열로 변환되기 때문에, 재사용하기 위해서는 디코딩 과정이 필요합니다. (이는 속도 저하의 원인이 됩니다.)

    • Base64는 문자열이 깨지면 원본 데이터를 복원할 수 없습니다.

    • 스토리지에서 저장한 후 불러올 때, 일부 문자열이 잘리거나 변형되면 atob() 디코딩 오류 발생하게 됩니다.

  2. Base64 변환으로 인한 파일 크기 증가 문제

    • Base64는 파일 크기를 약 33% 증가시켜 대용량 비디오 데이터 저장 시 비효율적입니다.

이러한 문제들은 Base64 대신 ArrayBuffer 사용하여 해결하였습니다.

ArrayBuffer를 저장하면, 불필요한 디코딩 과정 없이 바로 Blob으로 변환 가능하였습니다.

Base64 vs ArrayBuffer

저장 방식저장 형태장점단점
Base64문자열텍스트 기반 → 어디서나 사용 가능변환 과정에서 파일 크기가 약 33% 증가
ArrayBuffer바이너리 데이터 (바이트 배열)원본 크기를 유지 → 용량 최적화문자열처럼 다루기 어려움

ArrayBuffer란?

ArrayBuffer는 고정된 크기의 바이너리 데이터 버퍼를 저장하는 객체입니다. 즉, 데이터를 숫자 배열처럼 저장하는 방법입니다. 데이터를 숫자 배열(0과 1의 조합)처럼 다룰 수 있어 파일을 직접 조작하거나, 숫자로 변환해야 할 때 사용됩니다. 따라서 파일처럼 바로 사용은 어렵고, 변환 과정이 필요합니다.

let buffer = new ArrayBuffer(8); // 8바이트 크기의 빈 메모리 공간 생성
let view = new Uint8Array(buffer); // 숫자로 조작 가능하도록 변환
view[0] = 255; // 첫 번째 바이트에 255 저장
console.log(view); // Uint8Array(8) [255, 0, 0, 0, 0, 0, 0, 0]

Blob이란?

Blob은 데이터를 파일처럼 저장하는 방식으로 데이터를 즉시 다운로드하거나, 비디오/이미지로 재생할 때 사용합니다.  파일처럼 URL을 생성해서 다운로드하거나, 비디오 재생 가능하며, 메모리에 직접 올리지 않고, 브라우저에서 저장 가능합니다.

let blob = new Blob(["Hello, Blob!"], { type: "text/plain" }); // 텍스트 데이터를 Blob으로 저장
let link = document.createElement("a");
link.href = URL.createObjectURL(blob); // Blob을 URL로 변환
link.download = "example.txt"; // 파일로 다운로드 가능
document.body.appendChild(link);
link.click();
document.body.removeChild(link);

ArrayBuffer와 Blob 변환 핵심 로직

ArrayBuffer → Blob 변환 (비디오 저장 시 유용!)

const arrayBufferToBlob = (buffer, mimeType = "video/webm") => {
  return new Blob([buffer], { type: mimeType });
};

Blob → ArrayBuffer 변환 (파일 데이터를 메모리에서 조작할 때)

const blobToArrayBuffer = async (blob) => {
  return await blob.arrayBuffer();
};

세션 스토리지를 활용한 녹화 데이터 저장 - 첫 번째 개선 ⭐️

녹화 중 새로고침을 하거나 브라우저가 튕기면 데이터가 사라지는 문제를 해결하기 위해, 먼저 세션 스토리지를 활용하여 녹화 데이터를 저장하는 방안을 시도해 보았습니다.

첫 번째 개선안, 주요 로직 정리

RecordRTC를 사용하여 녹화 데이터를 10초마다 자동으로 저장

recorder = new RecordRTC(mediaStream, {
  type: "video",
  mimeType: "video/webm",
  timeSlice: 10000, //  timeSlice: 1000 옵션 사용하여 10초마다 저장
  ondataavailable: (blob) => saveChunkToStorage(blob),
});

녹화 데이터를 조각 단위로 세션 스토리지에 저장

const saveChunkToStorage = async (blob) => {
  const reader = new FileReader();
  reader.readAsArrayBuffer(blob);
  reader.onloadend = () => {
    let chunks = JSON.parse(sessionStorage.getItem("recordedChunks") || "[]");
    chunks.push([...new Uint8Array(reader.result)]); // ArrayBuffer 변환 후 저장
    sessionStorage.setItem("recordedChunks", JSON.stringify(chunks));
  };
};

녹화된 조각을 합쳐서 하나의 영상으로 변환 후 재생

const playRestoredVideo = () => {
  const storedData = JSON.parse(
    sessionStorage.getItem("recordedChunks") || "[]",
  );
 
  if (!storedData.length) {
    alert("⚠ 저장된 녹화 데이터가 없습니다!");
    return;
  }
 
  const restoredBlobs = storedData.map((data) =>
    arrayBufferToBlob(new Uint8Array(data)),
  );
  const finalBlob = new Blob(restoredBlobs, { type: "video/webm" });
 
  const videoURL = URL.createObjectURL(finalBlob);
  document.getElementById("restoredVideo").src = videoURL;
  document.getElementById("restoredVideo").play();
};

ArrayBuffer → Blob 변환

const arrayBufferToBlob = (buffer) => {
  return new Blob([buffer], { type: "video/webm" });
};

세션 스토리지를 활용하여 세가지가 개선되었습니다.

  1. 새로고침 후에도 녹화 데이터 유지 가능
  2. 실시간으로 녹화 데이터를 저장하여 데이터 손실 방지
  3. 서버 없이 로컬에서 빠르게 녹화 및 복원 가능

하지만 세션 스토리지는 브라우저를 닫으면 데이터가 삭제되는 점과, 한정된 용량으로 대용량 비디오 데이터를 저장하는데는 적합하지 않았습니다.

Uncaught QuotaExceededError: Failed to execute 'setItem' on 'Storage': Setting the value of 'recordedChunks' exceeded the quota. at reader.onloadend

1분도 안돼 저장 실패 에러가 발생하였기 때문에 다른 방법을 찾아야 했습니다.

IndexedDB를 활용한 녹화 데이터 저장 - 두 번째 개선 ⭐️⭐️

세션 스토리지를 활용하여 데이터 손실을 줄이는 첫 번째 개선 작업을 진행했지만, 세션 스토리지는 브라우저를 닫으면 데이터가 사라지는 단점과 용량 제한의 문제를 개선하기 위해 indexdDB를 사용하여 개선하였습니다.

IndexedDB와 세션 스토리지의 저장 용량 차이

  • IndexedDB는 세션 스토리지보다 훨씬 많은 데이터를 저장할 수 있습니다. GB단위로 대용량 비디오 저장 가능합니다.
  • 세션 스토리지는 브라우저 탭을 닫으면 데이터가 사라지며, 용량도 작기 때문에 대용량 데이터를 저장하기 어렵습니다.

두 번째 개선안, 주요 로직 정리

녹화 조각을 IndexedDB에 저장

const saveChunkToIndexedDB = (blob) => {
  const reader = new FileReader();
  reader.readAsArrayBuffer(blob);
  reader.onloadend = () => {
    const transaction = db.transaction(["chunks"], "readwrite");
    transaction.objectStore("chunks").add(reader.result);
  };
};

저장된 녹화 데이터를 합쳐서 재생

const playRestoredVideo = () => {
  const transaction = db.transaction(["chunks"], "readonly");
  const request = transaction.objectStore("chunks").getAll();
  request.onsuccess = () => {
    const finalBlob = new Blob(request.result.map(arrayBufferToBlob), {
      type: "video/webm",
    });
    document.getElementById("restoredVideo").src =
      URL.createObjectURL(finalBlob);
  };
};

IndexedDB 데이터 삭제 (초기화)

const clearIndexedDB = () => {
  db.transaction(["chunks"], "readwrite").objectStore("chunks").clear();
};

ArrayBuffer → Blob 변환

const arrayBufferToBlob = (buffer) =>
  new Blob([buffer], { type: "video/webm" });

IndexedDB 기반으로 녹화 데이터를 더욱 안전하게 저장할 수 있었습니다.

개선된 점

  1. 일정 한 초마다 녹화 데이터를 IndexedDB에 저장하여 데이터 손실 방지
  2. 새로고침 후에도 저장된 데이터 유지 가능
  3. 브라우저를 닫아도 녹화된 영상 보존 가능
  4. 데이터 초기화 기능 추가하여 필요시 삭제 가능

마치며

처음에는 이 문제가 서버에서 실시간 저장을 하는 방식으로만 해결 가능하다고 생각했지만, 로컬 저장 방식을 활용하여 안정적인 녹화 시스템을 구축할 수 있었습니다. 클라이언트에서도 충분히 많은 작업을 수행할 수 있다는 점이 정말 흥미로웠어요.

불가능할 것 같았던 아이디어가 현실이 되어 재밌는 경험이었어요. 비교적 단순한 접근 방식이 문제를 해결할 수 있었던 것 같아요.

아직은 간단한 로직이지만 해당 로직을 더욱 구체화하여 실무에 적용할 계획입니다. 하지만 저장 용량 관련해서 더 효율 적인 방안이 없는지, 로컬 저장 방식의 안정성을 더 높일 수 없는지 계속 고민해 볼 필요가 있는 것 같아요. 좋은 의견이 있다면 언제든지 편하게 공유해주시면 감사하겠습니다 :)