Automerge로 멀티플레이어 팟캐스트 에디터 만들기
요약
Automerge를 활용하여 실시간 멀티플레이어 협업이 가능한 브라우저 기반 오디오 에디터 'Ducking'을 구축한 사례를 소개합니다. CRDT 기반의 데이터 모델링을 통해 복잡한 동기화 문제를 해결하고 협업 워크플로우를 구현하는 방법을 다룹니다.
핵심 포인트
- Automerge를 통한 로컬 우선 및 멀티플레이어 데이터 동기화 구현
- CRDT 연산에 최적화된 데이터 모델 설계의 중요성
- React 훅 패턴을 활용한 직관적인 데이터 관리 방식
- 정규 데이터와 파생 데이터의 명확한 분리 필요성
- 몇년 전만 해도 실시간 멀티플레이어 데이터 동기화는 전문 인력과 기업 수준의 투자가 필요한 가장 어려운 문제였으나, 이제는
npm install
한 번으로 취미 프로젝트에도 멀티플레이어 UI 구현 가능
Automerge는 로컬 우선·멀티플레이어 안전·버전 관리를 갖춘 데이터 모델 구축 도구로, React의 useState
패턴과 유사한 방식으로 데이터 영속화·이력 관리·협업자 브로드캐스트·충돌 해소를 UI가 신경 쓰지 않아도 자동 처리
- 브라우저 기반 멀티플레이어 오디오 에디터
Ducking 사례에서, 데이터 모델을 CRDT 연산에 자연스럽게 매핑되도록 설계하는 것이 핵심 - 리스트 재정렬처럼 Automerge가 보장하지 않는 경우에는
애플리케이션 계층 코드로 더 강한 불변식 직접 구현 - 과거 산업용 수준의 마법이었던 실시간 협업 편집이 이제 소수를 위한 작은 앱에도 자유롭게 적용 가능해진 점이 핵심 의미
배경 — Ducking 프로젝트
-
지난 몇 달간 파트너의 팟캐스트를 위해
브라우저 기반 멀티플레이어 오디오 에디터 Ducking 제작 -
오디오 편집이 20년 된 단일 사용자 데스크톱 앱과 파일을 주고받는 방식에 머물러 있는 현실이 이상했음
-
한 명이 클립을 편집하는 동안 다른 한 명은 트랜스크립트를 고치거나 EQ 설정을 조정하는,
Google Docs나 Figma 같은 협업 워크플로우 필요 -
댓글, 이력, 변경 추적 같은 현대적 협업 도구도 함께 요구됨
-
이전 글에서 다룬 독특한 UI 디자인과 오디오 레이아웃 모델은 단일 편집기를 더 효과적으로 만들었으나, 진짜 원했던 것은
더 협업적인 워크플로우
Automerge 작업 방식
- 오디오 blob을 제외한 Ducking의 모든 데이터는
Automerge 문서에 저장 - 핵심 패턴은 React 개발자에게 익숙한 형태로, 훅으로 데이터를 가져와 렌더링하고 비동기 변경 요청을 디스패치하면 데이터 변경 후 훅이 리렌더 트리거
useDocument
훅 사용 예시: const [doc, changeDoc] = useDocument<Episode>(docUrl)
형태로 문서를 받아 입력값 변경 시 changeDoc((d) => { d.title = e.target.value })
로 갱신
-
데이터 갱신 연산은 명령형처럼 보이지만 네이티브 JS 객체·배열과 다름
-
메서드가 더 적고 즉시 변형(mutate)하지 않으며, 자체적으로 변형을 가로채
문서 이력의 changelist 항목으로 전환 -
Automerge는 단순 용도에서는 필요한 것을 처리하지만 마법은 아니며, 불변식이 원하는 의미와 항상 일치하지는 않아
신중한 데이터 모델 설계 중요 -
대부분의 의미 단위 사용자 동작이 Automerge가 제공하는 단일 연산에 대응되도록 함
-
관련 데이터에 대한 별개의 사용자 동작이 해당 Automerge 연산의 불변식 관점에서 자연스럽게 해소되도록 함
-
저장되는 정규 데이터(canonical data)와 계산으로 도출되는 파생 데이터(derived data)를 명확히 분리
멀티플레이어를 위한 데이터 모델링
-
Ducking의 데이터 모델에서
clip(클립) 은 변경 불가능한 기저 오디오 소스의 일부를 재생하는 창(window)으로, 재생 구간·효과 적용·타임라인 공간 점유 담당 -
가장 흔한 효과는 클립이 기저 오디오의 볼륨을 시간에 따라 조절해 크로스페이드하거나 잡음을 제거하는 것
-
초기에는 각 클립이
클립 시작 기준의 시간 인덱스 볼륨 레벨 목록을 가졌으나, 대부분의 볼륨 변경은 클립이 아닌 기저 오디오에 관한 것이라 문제 발생 -
클립 시작 시간을 조금 앞당기면 모든 볼륨 변경이 오디오의 다른 부분에 적용되는 현상
-
클립 시작 시간이 바뀔 때 모든 볼륨 타임스탬프를 갱신하는 코드를 작성하는 방식은
나쁜 선택 -
두 협업자가 동시에 클립 시작 시간을 편집하면, 각 편집이 시작 시간과 모든 볼륨 자동화 타임스탬프 변경을 함께 묶음
-
Automerge는 그 변경들 사이의
인과 관계(causal relationship) 를 알 수 없어 병합 시 뒤죽박죽으로 해소될 수 있음 -
하나의 의미 단위 동작이 CRDT가 이해하지 못하는 인과적 방식으로 여러 영속 데이터를 갱신하려 할 때 발생하는 전형적 문제
-
해결책은 오디오 효과 데이터를 클립이 아닌
기저 오디오의 타임프레임 기준으로 이전(migrate)하는 것 -
클립 시작·길이 변경 시 갱신이 불필요해져, 여러 편집자가 시작 시간·볼륨 자동화·기타 효과를 바꿔도 서로 독립적이라 올바르게 병합될 가능성 높음
-
단일 사용자 UI와 멀티플레이어 UI의 차이
-
단일 사용자 UI에서는 기존 데이터 모델을 두고 쓰기 시점에 추가 계산을 더해 동작시키기도 함
-
멀티플레이어 UI에서는 모든 영속 데이터를
직교(orthogonal) 상태로 유지하기 위해 데이터 모델 이전이 훨씬 흔함
쓰기 시점 단순화와 읽기 시점 계산을 강하게 선호하게 됨으로써 Automerge의 자동 병합 활용 극대화
- 데이터 형태 이전(migration)에 대한 조언
- 빌드 과정에서 데이터 형태를 이전해야 함을 받아들이고, 첫 큰 이전을 두려워하지 않도록 초반에 시간을 들여 미리 연습
- 클라이언트의 읽기 시점 처리, 서버의 일괄 업그레이드 등 다양한 패턴 존재
- 이전 전후가 동일한지 확인할 편리한 불변식을 찾으면 작업이 훨씬 수월
- Ducking에서는 이전 전후 모든 프로젝트의 오디오를 내보내
오디오 지문(audio fingerprint) 으로 변경 여부를 확인, 큰 스키마 변경도 두렵지 않게 배포
리스트 재정렬 구현
-
때로는 Automerge가 제공하지 않는 보장을 위해
애플리케이션 계층 코드로 더 강한 불변식 직접 작성 필요 -
Ducking의
magnetic timeline(재생할 클립의 정렬된 목록) 구현 시 문제 발생 -
Automerge는 인덱스로 항목을 삭제·삽입하는 배열 연산은 제공하나, 기존 항목을
원자적으로 재정렬(atomic re-order) 하는 연산은 미제공 -
알려진 해법 존재
-
Martin Kleppmann이 원자적 리스트 재정렬 연산에 관한 논문 발표
-
Liangrun Da와 함께 "Extending JSON CRDTs with Move Operations" 논문도 발표
-
Automerge에 추가하는
draft PR도 있으나 아직 병합되지 않음 -
단순한 재정렬 방식의 문제
-
현재 인덱스에서 객체를 삭제하고 목적지 인덱스에 다시 추가하는 방식
-
이 두 연산의 불변식을 결합해도 "동시 재정렬이 많을 때 객체가 목록에 정확히 한 번만 존재한다"는 원하는 불변식이 보장되지 않음
-
동시 삭제·추가가 여러 개면 객체가 목록 여러 위치에 존재할 수 있음 (Alice와 Bob이 각각 B를 delete+insert로 이동하면 두 삭제는 하나의 tombstone으로 합쳐지나 두 삽입은 각기 새 요소를 만들어 둘 다 살아남아
B가 두 번 등장) -
"정확히 한 번" 불변식을 애플리케이션 계층에서 제공하는 직접 구현
-
클립이 타임라인에 삽입될 때
semantic id 부여 -
재정렬 시 위와 같이 삭제·삽입 연산 트리거
-
읽기 시점에 애플리케이션이 동일 semantic id의 중복을 스캔해, 삭제되지 않은 첫 항목을 임의로 선택하고 나머지는 무시
-
이로써 객체가 목록에 한 번만 존재하고 여러 독자가 항상 동일한 최종 상태에 도달
-
리스트 재정렬은 Ducking에서 Automerge가 제공하지 않은 유일한 연산으로, PR 병합 시 애플리케이션 레벨 로직 불필요해질 전망
문서 이력 (Document history)
-
좋은 멀티플레이어 UI는
이력 관리 도구 필요로, 협업자는 떠난 사이의 변경 확인·diff 댓글·구버전 비교 및 롤백 원함 -
Automerge는 문서 버전 이력을 추적하고 이력·비교를 다루는 훌륭한 기본 요소(primitive) 제공
-
단, 그 정보를 어떻게 노출하고 어떤 개념을 사용자에게 제시할지는 애플리케이션 개발자가 결정
-
Ink & Switch의
Patchwork lab notes 권장 -
사용자에게 브랜치를 노출하는 작업과 universal comments 작업이 특히 흥미로움
-
Ducking이 정착한 비교적 단순한 협업·이력 모델
-
사용자 정의 이름의
checkpoint가 있는 선형 버전 이력으로, checkpoint가 변경의 그룹화 단위이자 논의·diff·롤백의 단위 -
오디오의 특정 지점, 트랜스크립트의 영역, 버전 checkpoint에 연결 가능한
댓글 스레드(comment thread) -
아직 브랜치 도입의 충분한 이유는 없었으나 향후 유용할 가능성 언급
텍스트와 marks
-
리치 텍스트 작업은 편집 가능한 텍스트 위에 커스텀 로직을 얹으려 할 때 특히 까다로운 문제
-
리치 텍스트와 멀티플레이어 소프트웨어 전반의 난점을 설명하는
Peritext 논문 권장 -
Automerge의 리치 텍스트 스키마는
marks(텍스트 범위에 적용되며 텍스트 편집 중에도 일관성 유지되는 주석) 포함 -
가장 흔히 굵게·기울임 같은 서식에 사용되나, 애플리케이션 고유의 커스텀 mark 생성도 가능
-
Ducking의 커스텀 mark 활용 두 가지
-
댓글 스레드의 대상이 된 트랜스크립트 영역 추적
-
트랜스크립트 단어의 타임스탬프 추적, 편집은 그대로 허용
-
트랜스크립션 서비스가 각 단어에 타이밍 정보 mark를 단 richtext 객체로 트랜스크립트를 Automerge에 저장
-
작은 오타로 단어 하나만 고치면 mark가 유지돼 모든 타이밍 정보 보존
-
문장 전체를 고치면 중간 mark 일부는 제거되나 문장 시작·끝의 mark는 유지돼 최소한의 대략적 타이밍 정보 확보
-
marks의 한 가지 제약은 datum이 단순 값(일반적으로 문자열)이어야 하고 멀티플레이어 병합이 되지 않는다는 점
-
트랜스크립트 타이밍 정보처럼 작고 불변인 데이터는
JSON을 문자열로 직렬화 -
댓글 스레드처럼 더 복잡하거나 가변적인 데이터는 mark에
id만 저장하고 실제 데이터는 문서 내 다른 곳에 보관 -
marks는 멀티플레이어 리치 텍스트 위에 애플리케이션 기능을 쌓는 훌륭한 토대 제공
다음 글 — 시리즈 구성
-
본 글은 Ducking 제작에 관한 3부작 중 2부
-
1부: 소프트웨어의 독특한 UI 디자인 설명
-
2부(본 글): Automerge 검토를 권하고 취미용 멀티플레이어 프로젝트 구축 가능성 제시
-
최종 3부 예정: Ducking 제작 경험 회고
-
최종 3부 관련 언급
LLM 지원을 작업 강화가 아닌 더 많은 스케치·해먹 시간 확보 목적으로 사용 -
소수만을 만족시키면 되는
narrowcast 소프트웨어 제작의 즐거움
예상 질문
오디오 데이터는?
-
모든 멀티플레이어 데이터는 Automerge에 저장되나, 기저
오디오 blob은 빠른 재생을 위해 Automerge에 두지 않고 별도 처리 필요 -
목표는 새 협업자가 페이지 로드 후
4초 이내 청취·편집 시작으로, 데스크톱 앱 실행보다 빠르고 전체 프로젝트 파일 다운로드보다 훨씬 빠름 -
1시간 분량 에피소드는 고품질 스튜디오 녹음 4시간에 효과·배경음악을 더해
약 1기가바이트 오디오에 의존할 수 있음 -
빠른 콜드 스타트를 위한 업로드 시 오디오 서비스 작업
-
원본 오디오 백업
-
음성을 트랜스크립트 뷰용으로 전사(transcribe)
-
타임라인 뷰용 파형(waveform) 생성
-
40분 녹음 중 1분만 사용하면 대부분의 클라이언트가 한두 개 작은 조각만 받도록 짧은 창(window)으로 분할(slice)
-
조각을 압축 포맷으로 트랜스코딩해, 고품질 오디오가 백그라운드에서 다운로드되는 중에도 즉시 재생 가능한 lossy 버전 제공
-
UI 데이터 계층은 사용자 의도를 따라 즉시 필요한 데이터의 빠른 버전과 실제 사용된 전체 오디오의 고품질 버전 로딩 관리
-
브라우저의
IndexedDB API가 다단계 캐싱과 content-addressable 저장에 유용하며, 자동 eviction 관리로 사용하면 남고 안 쓰면 사라짐 -
이 모든 처리와 로컬 캐싱이 끝나면 나머지 UI는 오디오에 대한 빠른 무작위 접근을 가정하고 편집 워크플로우에 집중 가능
로컬 우선 앱이 아닌 서버+브라우저 UI를 만든 이유
-
서버 없이 완전히 동작하는 Obsidian 같은
local-first 앱 선호, 특히 신뢰할 만한 탈출 경로를 제공하면서 클라우드 기반 유료 경험을 함께 갖춘 형태 선호 -
초기에는 로컬 파일시스템 저장과 선택적 서버 동기화를 갖춘
Tauri 앱 옵션으로 시작 -
서버나 로컬 앱 어느 쪽이든 공급 가능한 데이터 인터페이스 기준으로 UI 구축
-
향후 어떤 자금도 lock-in으로 앱을 더 수익화하도록 유혹하지 못하게 하는 안전장치
-
이후 이것이 SaaS가 아니라 파트너 및 소수 친구와 쓰고 싶은 것이라 판단
-
잘못 다룰 유인이 사라지고 영구 운영 비용이 낮아져 가장 쉬운 방식으로 제작 결정
-
약 3초 콜드 스타트를 달성하자 누구도 네이티브 앱 다운로드·설치에 시간 낭비하길 원치 않게 됨
-
오디오 앱이 현재의 데스크톱 전용 세계에서 동기화 옵션을 갖춘
local-first 세계로 곧장 건너뛰어, 중간의 10~20년 SaaS lock-in을 피할 수 있기를 희망
Automerge는 안전하고 web-scale인가? 스타트업에 써야 하나?
-
모른다고
즐겁게 답할수 있음, 이는 거부가 아니라 말 그대로 알 수 없다는 의미 -
입사 당시 충돌 없는 실시간 멀티플레이어 편집은 마법이었고, 10년 전에는 특정 문제의 알려진 해법이 있었으나 자금 지원 팀과 여러 분야 전문성 필요
-
오늘날에는 의존성 하나를 받아 대체로 직관적으로 UI를 만들고 친구들과 실시간 협업 가능
-
보안 측면에서 현재 Ducking은 제한된 네트워크 접근과 Automerge 서버 websocket 연결 생성 시
인가(authorization) 단계로 보호 -
사용자는 초대받지 않은 프로젝트를 발견하거나 편집할 수 없음
-
편집·댓글의 사용자 귀속은 일부만 안전하며 친구들이 못된 짓을 안 한다는 전제에 의존
-
comment만 가능/edit 불가, 프로젝트 일부만 편집, 발견 가능성 제어 같은
세분화된 권한은 신중한 설계 작업 필요 -
Ink & Switch가 개발 중인
Keyhive는 암호학적으로 안전한 capability 기반 접근 제어 모델 제공 -
신뢰할 수 없는 사용자에게도 Automerge 앱을 공개 공유하기 쉽게 하나 아직 준비되지 않음
Automerge가 더 나은가?
-
이 분야의 다른 해법으로
Yjs가 존재하나, 무엇이 적합한지는 대신 평가해 줄 수 없음 -
변치 않는 조언
-
문제를 깊이 고민하고, 마주칠 한계에 대해 대략적 계산(back-of-the-napkin)을 해 보며, 여러 대안으로 프로토타입을 만들어 보고, 어쩌면 자신의 문제가 그리 어렵지 않아 최신·최고급 해법이 필요 없을 수도 있음을 정직하게 인정
-
Ducking의 경우 빠른 프로토타입과 문서 탐색으로 Automerge가 해당 용도에
충분히 성숙하고 성능 좋음 확인 -
더 중요하게는
Ink & Switch 생태계가 미학적으로 끌림 -
Automerge가 단순 동기화·버전 관리 엔진이 아니라 소프트웨어를 더 안전·협업적·유연·즐겁고 개인적으로 만드는 더 큰 비전의 일부라는 점
-
Keyhive 등의 성공을 바라며, 소수만을 위한 작지만 마법 같은 소프트웨어의 확산을 기대
댓글과 토론
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기