Sangcho.log
dev

OpenCut 탐구 2 — 자동 저장은 누가, 언제, 무엇을 저장하는가

8 min read|

1편에서 지도를 그렸으니, 이번에는 가장 기본적인 흐름부터 따라가 봅니다. 에디터에서 클립을 옮기면 저장 버튼을 누르지 않아도 알아서 저장됩니다. 이 "알아서"를 뜯어보면 생각보다 많은 역할 분담이 숨어 있습니다.

비유하자면 회사의 문서 관리와 같습니다. "언제 정리할지"를 판단하는 비서(SaveManager), "무엇을 어떻게 묶어 넣을지"를 아는 담당자(ProjectManager), 그리고 실제 서류 캐비닛(storageService). 저장이라는 한 단어 뒤에 세 명이 있는 셈이죠.

등장인물 네 명

구성 요소 역할
SaveManager 자동 저장의 타이밍 담당 — dirty 상태, 디바운스, pause/resume, flush
ProjectManager 프로젝트 생성·로드·저장·복제·삭제의 오케스트레이터
storageService 실제 영속 저장소 접근 — IndexedDB, OPFS, 스토리지 마이그레이션을 캡슐화
EditorProvider 화면 진입 시 projectId를 받아 로드를 시작하는 UI 진입점

핵심은 SaveManager가 "무엇을 저장할지"를 모른다는 점입니다. 무엇을 저장할지는 ProjectManager가, 어디에 저장할지는 storageService가 압니다. SaveManager는 오직 "지금 저장할 때가 됐는가"만 판단하는 스케줄러입니다.

자동 저장 — 비서의 일과

scenes / timeline 상태 변경
        ↓
SaveManager.markDirty()        # "정리할 게 생겼다" 메모
        ↓
디바운스 타이머 (일정 시간 대기)
        ↓
saveNow()                      # 추가 변경이 없으면 실행
        ↓
editor.project.saveCurrentProject()
        ↓
storageService.saveProject()   # IndexedDB에 기록

변경이 생길 때마다 즉시 저장하면 드래그 한 번에 수십 번의 I/O가 발생합니다. 그래서 비서는 "마지막 변경 후 잠잠해질 때까지" 기다렸다가 한 번에 정리합니다(디바운스).

왜 scenes와 timeline만 구독하나

SaveManager는 열 개 매니저 중 딱 둘, scenestimeline만 구독합니다. 에디터의 본문 편집이 대부분 이 경로로 흐르기 때문입니다.

timeline (트랙/엘리먼트 편집의 진입점)
    → scenes (활성 씬이 트랙을 실제 보유)
        → active project

나머지 매니저가 빠진 이유는 둘 중 하나입니다.

  • 저장 대상이 아닌 런타임 상태playback(재생 위치), selection(선택 상태)은 새로고침하면 사라져도 되는 값
  • 별도 경로로 스스로 저장media는 파일을 직접 저장소에 씀

그런데 project 매니저에는 저장해야 할 필드(썸네일, 프로젝트 설정 등)가 있는데도 구독 대상이 아닙니다. 대신 변경이 일어나는 지점에서 editor.save.markDirty()를 직접 호출합니다. 철학이 보이는 대목입니다 — 공통 편집 본문은 구독으로 자동 처리하고, 예외적인 영속 상태는 변경 지점에서 수동으로 dirty 처리한다.

불러오기 — 단순한 읽기가 아니라 복원 공정

화면 진입점은 EditorProvider입니다. projectId를 받아 editor.project.loadProject({ id })를 호출하고, 실패하면 에러 처리 또는 새 프로젝트 생성으로 빠집니다.

진짜 일은 ProjectManager.loadProject()에서 벌어집니다. "JSON 하나 읽기"가 아니라 12단계 복원 공정입니다.

1. 로딩 상태 설정
2. autosave 일시 정지          ← 주목
3. 저장소 마이그레이션 보장
4. 기존 미디어/씬 상태 초기화
5. storageService.loadProject()로 프로젝트 구조 읽기
6. active project 설정
7. 씬 초기화
8. 프로젝트별 미디어 로드 (메타데이터는 IndexedDB, 파일은 OPFS)
9. 트랙 기반 폰트 로드
10. 썸네일 없으면 생성 후 저장
11. 로딩 종료
12. autosave 재개               ← 주목

storageService.loadProject()는 IndexedDB에서 직렬화된 프로젝트를 읽어 날짜 문자열을 Date로 되살리고, legacy 필드를 보정해 TProject로 반환합니다. 저장 포맷 → 런타임 포맷의 역직렬화 계층입니다.

로드 중 autosave를 끄는 이유

로드 중에는 씬 초기화, 미디어 초기화, active project 교체처럼 내부 상태가 대량으로 바뀝니다. 이건 사용자의 편집이 아니라 복원 과정입니다. autosave가 켜져 있으면 복원 중간의 어중간한 상태가 저장될 수 있죠. 그래서 로드 시작 전 save.pause(), 완료 후 save.resume(). 비서에게 "지금은 이사 중이니까 정리하지 마세요"라고 말해두는 겁니다.

갈라진 저장 경로 — 미디어 삭제의 함정

여기부터가 이 구조에서 가장 흥미로운 부분입니다. 미디어 삭제는 프로젝트 저장과 같은 경로를 타지 않습니다.

변경 종류 저장 방식
타임라인/씬 편집 dirty 표시 → 디바운스 → 나중에 프로젝트 저장
미디어 파일 삭제 storageService.deleteMediaAsset() 즉시 실행

미디어 하나를 삭제하면 실제로는 이런 순서로 일이 벌어집니다.

1. 메모리에서 media asset 제거
2. 그 media를 참조하는 타임라인 요소 제거
3. (2)의 변경으로 SaveManager가 dirty 상태가 됨 → 디바운스 대기
4. 별도로 storageService.deleteMediaAsset()가 즉시 실행됨
5. 프로젝트 구조 저장은 디바운스가 끝난 뒤에야 일어남

즉 "미디어 파일 삭제"와 "삭제가 반영된 프로젝트 저장"은 하나의 트랜잭션으로 커밋되지 않습니다. 두 개의 장부가 서로 다른 시점에 갱신되는 구조입니다.

삭제 직후 새로고침하면?

타임라인 요소가 지워지면서 dirty 상태이므로 브라우저가 beforeunload 경고를 띄울 수 있습니다. 하지만 사용자가 경고를 무시하면 세 가지 경우가 생깁니다.

  • 경우 A (가장 위험) — 미디어 파일 삭제는 완료, 프로젝트 autosave는 미완료. reload 후 프로젝트에는 옛 타임라인 요소가 남아 있는데 실제 파일은 없음
  • 경우 B — 둘 다 미완료. 삭제가 부분 반영되거나 아예 반영 안 됨
  • 경우 C — 둘 다 완료. 정상

경우 A가 되면 orphaned media reference 상태입니다 — 프로젝트 트리에는 mediaId를 참조하는 요소가 있는데 IndexedDB/OPFS에는 실체가 없죠. 다행히 렌더러는 미디어를 못 찾으면 그 노드를 건너뛰므로 크래시 대신 "타임라인에 빈 껍데기만 남고, 프리뷰와 export에서 빠지는" 증상으로 나타납니다. UX 차원에서는 beforeunload로 막고 있지만, 데이터 일관성을 원자적으로 보장하는 구조는 아니라는 것이 현재 구현의 정직한 한계입니다.

더 깊이: 이 race condition을 원자적으로 만들려면

이런 이중 장부 문제를 다루는 고전적인 방법이 몇 가지 있습니다.

  1. 삭제 순서 뒤집기 + tombstone — 파일을 바로 지우지 않고 "삭제 예정" 표시(tombstone)만 남긴 뒤, 프로젝트 저장이 완료된 다음에 실제 파일을 지웁니다. 실패해도 "참조는 없는데 파일만 남은" 상태가 되어, 유령 참조보다 훨씬 안전합니다(다음 로드 때 청소하면 그만).
  2. 로드 시 정합성 복구 — 프로젝트 로드 시 존재하지 않는 mediaId를 참조하는 요소를 감지해 제거하거나 사용자에게 알립니다. OpenCut의 ScenesManager가 Main 씬 누락을 자동 복구하는 것과 같은 "외부 데이터를 신뢰하지 않는" 방어 패턴의 연장입니다.
  3. 단일 저장 경로로 합류 — 미디어 삭제도 dirty → flush 경로에 태우고, flush 시점에 "프로젝트 저장 + 파일 삭제"를 순서대로 수행합니다. 구현은 단순해지지만 대용량 파일 삭제가 디바운스에 묶이는 단점이 있습니다.

어느 쪽이든 원칙은 하나입니다. 두 저장소를 갱신할 때는 "실패해도 덜 위험한 쪽이 먼저" — 파일이 남는 건 용량 낭비지만, 참조가 남는 건 버그입니다.


한 줄 요약 — OpenCut의 자동 저장은 "언제(SaveManager) / 무엇을(ProjectManager) / 어디에(storageService)"로 역할이 분리된 다층 구조이며, 프로젝트 저장(디바운스)과 미디어 파일 저장(즉시)이라는 두 경로의 타이밍 차이가 유일한 일관성 구멍이다.

스스로 점검할 질문

  1. SaveManager가 project 매니저를 구독하지 않는데도 프로젝트 설정 변경이 저장되는 이유는 무엇인가요?
  2. 프로젝트 로드 중에 autosave를 멈추지 않으면 구체적으로 어떤 데이터가 저장될 위험이 있나요?
  3. 여러분의 앱에서 "두 저장소를 함께 갱신"하는 코드가 있다면, 중간에 페이지가 닫혔을 때 어떤 상태가 남나요?

참고 링크