OpenCut 탐구 2 — 자동 저장은 누가, 언제, 무엇을 저장하는가
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는 열 개 매니저 중 딱 둘, scenes와 timeline만 구독합니다. 에디터의 본문 편집이 대부분 이 경로로 흐르기 때문입니다.
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을 원자적으로 만들려면
이런 이중 장부 문제를 다루는 고전적인 방법이 몇 가지 있습니다.
- 삭제 순서 뒤집기 + tombstone — 파일을 바로 지우지 않고 "삭제 예정" 표시(tombstone)만 남긴 뒤, 프로젝트 저장이 완료된 다음에 실제 파일을 지웁니다. 실패해도 "참조는 없는데 파일만 남은" 상태가 되어, 유령 참조보다 훨씬 안전합니다(다음 로드 때 청소하면 그만).
- 로드 시 정합성 복구 — 프로젝트 로드 시 존재하지 않는
mediaId를 참조하는 요소를 감지해 제거하거나 사용자에게 알립니다. OpenCut의 ScenesManager가 Main 씬 누락을 자동 복구하는 것과 같은 "외부 데이터를 신뢰하지 않는" 방어 패턴의 연장입니다. - 단일 저장 경로로 합류 — 미디어 삭제도 dirty → flush 경로에 태우고, flush 시점에 "프로젝트 저장 + 파일 삭제"를 순서대로 수행합니다. 구현은 단순해지지만 대용량 파일 삭제가 디바운스에 묶이는 단점이 있습니다.
어느 쪽이든 원칙은 하나입니다. 두 저장소를 갱신할 때는 "실패해도 덜 위험한 쪽이 먼저" — 파일이 남는 건 용량 낭비지만, 참조가 남는 건 버그입니다.
한 줄 요약 — OpenCut의 자동 저장은 "언제(SaveManager) / 무엇을(ProjectManager) / 어디에(storageService)"로 역할이 분리된 다층 구조이며, 프로젝트 저장(디바운스)과 미디어 파일 저장(즉시)이라는 두 경로의 타이밍 차이가 유일한 일관성 구멍이다.
스스로 점검할 질문
- SaveManager가
project매니저를 구독하지 않는데도 프로젝트 설정 변경이 저장되는 이유는 무엇인가요? - 프로젝트 로드 중에 autosave를 멈추지 않으면 구체적으로 어떤 데이터가 저장될 위험이 있나요?
- 여러분의 앱에서 "두 저장소를 함께 갱신"하는 코드가 있다면, 중간에 페이지가 닫혔을 때 어떤 상태가 남나요?
참고 링크