Opkle(옵클) - 창작자를 위한 앱과 시스템
옵클(Opkle)은 창작자를 위한 다양한 앱과 시스템을 제공하는 개발사입니다. 전자책 에디터 앱 'Opkle editor'를 출시했고, 관련 전자책 클래스를 제공하고 있습니다.
EDITOR
CLASS
BLOG
LOGIN
표현한다는 것의
무한한 가능성,
새로운 형태로
담아내다.
새로운 형태의 콘텐츠
Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.
텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.
옵클 에디터 개발기: 첫 번째 구현은 코드 편집기였습니다
dev
2
옵클 에디터 개발기: 미리보기는 곧 또 하나의 편집기였습니다
dev
3
옵클 에디터 개발기: 미디어쿼리까지 편집하는 EPUB 에디터
dev
4
옵클 에디터 개발기: CSS 애니메이션으로 영상을 흡수하는 방식
dev
5
옵클 에디터 개발기: 책을 만들기 위한 LLM을 설계하다
dev
6
6
7
8
9
10
...
3
옵클 에디터 개발기: 미리보기는 곧 또 하나의 편집기였습니다
1차 구현을 지나면서 분명해진 것이 있었습니다. CodeMirror 6 위에서 HTML과 CSS를 조금 더 편하게 다루는 것만으로는 부족했습니다. 창작자는 코드가 아니라 책의 화면을 보고 싶어 했고, 문단과 이미지와 여백을 실제 읽히는 모습에 가깝게 만지고 싶어 했습니다. 그래서 다음 단계는 자연스럽게 미리보기였습니다. 다만 이 미리보기는 단순한 preview panel이 아니었습니다. 제가 만들고 싶었던 것은 보는 화면이면서 동시에 편집 가능한 화면이었습니다.
그 순간부터 설계해야 할 층이 많아졌습니다. 미리보기에서 문단을 수정하면 뒤에 있는 CodeMirror 6의 XHTML 문자열이 바뀌어야 했고, 스타일을 바꾸면 CSS 문자열도 갱신되어야 했습니다. 그리고 그 결과는 다시 IndexedDB에 저장되어야 했습니다. 반대로 CM6에서 코드를 수정하면 미리보기 DOM이 다시 렌더링되어야 했습니다. 화면, 코드 문자열, 내부 document state, IndexedDB 저장 데이터가 모두 같은 문서를 가리키게 만드는 구조가 필요했습니다.
처음부터 이 부분은 일반적인 양방향 동기화보다 더 정교하게 설계해야 한다고 봤습니다. DOM은 살아 있는 객체이고, HTML/CSS는 직렬화된 문자열이며, IndexedDB는 저장된 스냅샷입니다. 각각의 시간 감각이 다릅니다. 사용자가 지금 화면에서 블록 하나를 드래그하는 순간, 그 변경을 어느 단위로 HTML에 반영할지, CSS는 어디까지 다시 계산할지, 저장은 언제 commit할지 정해 두면 됩니다. 이때부터 옵클 에디터는 단순한 코드 편집기에서 상태 동기화 시스템으로 확장되기 시작했습니다.
iframe과 Shadow DOM 사이
미리보기 화면을 만들 때 가장 먼저 고민한 것은 격리였습니다. EPUB 본문은 독립적인 CSS 환경에서 렌더링되어야 합니다. 에디터 자체의 UI 스타일과 책 본문의 CSS가 섞이면 안 됩니다. 버튼, 패널, 툴바, 팝업에 쓰는 클래스와 책 안의 클래스가 충돌하면 미리보기는 신뢰할 수 없는 화면이 됩니다. 그래서 처음에는 iframe과 Shadow DOM을 모두 고민했습니다.
iframe은 격리라는 면에서는 강력합니다. 별도의 document context를 갖고, CSS와 JavaScript 실행 환경도 부모와 분리할 수 있습니다. 실제 reading system에 가까운 독립 문서처럼 다룰 수도 있습니다. 하지만 편집기로 쓰기 시작하면 비용이 커집니다. 부모 에디터와 iframe 내부 DOM 사이의 selection, drag event, context menu, resource path, focus, undo/redo를 계속 연결해야 합니다. postMessage나 직접 contentWindow 접근으로 다룰 수는 있지만, 편집 이벤트의 밀도가 높아질수록 경계가 무겁게 느껴졌습니다.
Shadow DOM은 다른 성격의 선택지였습니다. 같은 document 안에 있으면서도 style scoping을 얻을 수 있고, 에디터의 상태 관리와 이벤트 시스템 안에 더 가까이 둘 수 있습니다. shadowRoot 안에 EPUB 본문 DOM을 렌더링하면, 책의 CSS를 그 안으로 주입하고 외부 UI 스타일과 어느 정도 분리할 수 있었습니다. 동시에 부모 쪽 TypeScript 코드에서 shadowRoot 내부의 block node를 직접 추적하고 이벤트를 붙이기도 쉬웠습니다.
결국 저는 Shadow DOM을 선택했습니다. 완벽한 reading system sandbox라기보다, 편집 가능한 preview surface를 만들기 위한 선택이었습니다. 이 선택은 이후 구조를 크게 결정했습니다. EPUB 본문은 더 이상 단순 문자열이 아니라 shadowRoot 안에 살아 있는 DOM tree가 되었고, 그 DOM tree의 각 요소는 편집 가능한 block으로 취급되기 시작했습니다.
미리보기 DOM과 block 단위로 쪼개기
Shadow DOM을 선택한 뒤에는 본문을 어떻게 편집 단위로 볼 것인지가 중요해졌습니다. HTML 전체를 하나의 큰 문자열로 다루면 사용자는 여전히 코드 편집기의 논리 안에 갇힙니다. 그래서 미리보기 안에서는 문단, 제목, 이미지, 컨테이너, 열 구조 같은 요소를 block 단위로 다루기 시작했습니다. 각 block은 DOM node이면서 동시에 원본 XHTML로 되돌아갈 수 있어야 했습니다.
이 구조에서는 태그 하나하나가 단순한 HTML 요소가 아니라 편집 가능한 객체가 됩니다. `p`, `h1`, `img`, `section`, `div` 같은 요소를 shadowRoot 안에 렌더링하고, 각 요소에 block id나 data attribute를 부여했습니다. 그리고 pointer event, click, double click, contextmenu, dragstart, dragover, drop 같은 이벤트를 block 단위로 걸었습니다. 사용자가 화면에서 문단을 클릭하면 그 문단이 선택되고, 드래그하면 순서가 바뀌고, 이미지 파일을 떨어뜨리면 해당 위치에 image block이 삽입되는 흐름을 만들고 싶었습니다.
여기서부터 Notion 같은 block editor의 감각을 많이 의식했습니다. 사용자가 글의 순서를 바꾸기 위해 HTML 문자열을 잘라 붙이는 것이 아니라, 화면에 보이는 블록을 끌어 옮기면 내부 DOM 순서가 바뀌고, 그 결과가 XHTML 문자열로 다시 직렬화되는 방식입니다. 열을 나누거나 이미지 배치를 바꾸는 작업도 CSS를 직접 입력하는 것이 아니라, block의 layout state를 바꾸고 그 state가 class나 style rule로 반영되는 식으로 가져가려 했습니다.
block 기반 편집에서는 serialization 경로가 중요했습니다. DOM을 조작한 뒤 그 상태를 원본 XHTML로 되돌리는 규칙이 필요합니다. 사용자가 문단 순서를 바꾸면 부모 node의 child order가 바뀌고, 그 순서를 문자열에 반영해야 합니다. 이미지 block을 추가하면 리소스 저장소, OPF manifest, 본문 XHTML, preview DOM이 모두 같이 바뀌어야 합니다. 열 레이아웃을 만들면 단순히 DOM을 감싸는 것인지, class를 붙이는 것인지, CSS rule을 생성하는 것인지 결정하면 됩니다.
미리보기에서 편집과 CM6, IndexedDB
미리보기 편집을 넣으면서 CodeMirror 6와의 동기화 규칙도 다시 잡았습니다. 1차 구현에서는 CM6가 원본에 가까웠습니다. 사용자가 CM6에서 XHTML/CSS 문자열을 수정하면 내부 상태가 바뀌고, 저장하면 IndexedDB에 반영되는 구조였습니다. 미리보기 편집이 들어오면 사용자는 CM6가 아니라 shadowRoot 안의 DOM을 직접 바꾸고 있으므로, 그 변경을 다시 CM6 문자열로 돌려보내는 흐름이 필요했습니다.
이때 CM6를 단순히 숨겨진 저장소처럼 쓰면 안 됐습니다. CM6의 EditorState와 undo history, 문서 문자열도 실제 상태와 맞아야 했습니다. 그래서 preview DOM에서 변경이 발생하면, 해당 변경을 XHTML string patch로 변환하고, CM6의 dispatch를 통해 document를 갱신하는 방향을 생각했습니다. DOM에서 바로 IndexedDB로 저장해 버리면 CM6는 낡은 문자열을 들고 있게 되고, 이후 사용자가 코드 탭을 열었을 때 상태가 어긋납니다.
예를 들어 미리보기에서 문단 하나를 수정하면, shadowRoot의 textContent만 바꾸는 것으로 끝나지 않습니다. 그 문단에 연결된 block id를 통해 원본 XHTML의 range를 찾고, 새 text를 escape한 뒤, 대응하는 문자열 범위를 교체해야 합니다. CM6에는 transaction으로 변경을 넣어야 하고, 그 transaction은 다시 update listener를 타며 preview rerender를 유발할 수 있습니다. 여기서 루프를 막아야 했습니다. preview에서 온 변경인지, CM6에서 직접 온 변경인지 source를 구분하지 않으면 같은 수정이 두 번 적용되거나 렌더가 반복될 수 있습니다.
CSS도 비슷했습니다. 미리보기에서 어떤 block의 margin, width, column layout을 바꾸면 그것은 DOM style만 바꾸는 문제가 아닙니다. EPUB으로 내보낼 때도 살아 있어야 하므로 CSS 문자열이나 inline style, 혹은 class 기반 rule 중 하나로 남아야 합니다. preview에서 보이는 변경이 export와 reading system에서도 재현되려면, 화면 조작을 CSS state로 직렬화하는 규칙이 필요했습니다.
마지막 단계가 아니라 계속 움직이는 층
미리보기 편집이 들어오면서 IndexedDB 저장도 더 세밀하게 나누었습니다. 1차 구현에서는 CM6 문자열 변경을 저장하면 됐습니다. 하지만 이제는 변경의 출처가 여러 곳이 되었습니다. CM6에서 직접 수정한 XHTML/CSS, shadowRoot에서 발생한 block reorder, 이미지 드롭으로 생긴 리소스 추가, layout 조작으로 생긴 CSS state, 그리고 responsive preview 간의 동기화가 모두 저장 대상이 되었습니다.
저장 시점을 너무 늦추면 사용자는 불안합니다. 브라우저 기반 에디터에서 작업이 날아가는 경험은 치명적입니다. 반대로 모든 이벤트마다 전체 프로젝트를 IndexedDB에 다시 쓰면 성능이 무너집니다. 특히 이미지와 폰트 blob이 들어간 프로젝트에서는 전체 snapshot 저장이 부담이 됩니다. 그래서 저장은 문서 문자열, 리소스 blob, 메타데이터, UI state를 나누어 생각해야 했습니다.
preview DOM에서 작은 편집이 일어나면 먼저 내부 document state를 갱신하고, 그 결과를 CM6 document에 반영한 뒤, 변경된 XHTML 문자열만 저장합니다. 이미지 파일이 추가되면 blob store에 리소스를 넣고, OPF manifest state를 갱신하고, 본문 XHTML에도 참조를 삽입합니다. 레이아웃 변경은 CSS state를 갱신하고 CSS 문자열을 다시 직렬화한 뒤 저장합니다. 이렇게 변경 타입별로 저장 경로를 나누지 않으면, 작은 편집 하나가 프로젝트 전체 저장으로 번지고 에디터가 무거워집니다.
이때부터 자동 저장은 단순한 debounce 함수가 아니었습니다. 어떤 변경을 어느 store에 저장할지, 저장 중에 또 다른 변경이 들어오면 어떻게 queueing할지, IndexedDB transaction이 실패했을 때 UI 상태를 어떻게 되돌릴지 생각해야 했습니다. 사용자는 그냥 문단 하나를 옮겼을 뿐인데, 내부에서는 DOM reorder, XHTML serialization, CM6 dispatch, IndexedDB write가 한 흐름으로 이어져야 했습니다.
모바일과 태블릿 미리보기를 동시에
EPUB은 미디어쿼리가 아주 중요한 형식입니다. 웹페이지처럼 하나의 화면 폭에서만 맞으면 되는 것이 아니라, 리더기와 태블릿, 휴대폰, 사용자의 글자 크기와 여백 설정까지 고려해야 합니다. 그래서 미리보기 화면도 하나만으로는 부족하다고 생각했습니다. 한쪽에서는 태블릿에 가까운 폭을 보고, 다른 한쪽에서는 모바일에 가까운 폭을 동시에 볼 수 있어야 했습니다.
처음에는 이것도 단순히 같은 HTML을 두 개의 viewport에 렌더링하면 된다고 생각했습니다. 하지만 편집 가능한 preview가 되면 이야기가 달라집니다. 태블릿 shadowRoot와 모바일 shadowRoot가 각각 존재하고, 둘 다 같은 XHTML/CSS 상태를 바라봐야 합니다. 사용자가 태블릿 쪽에서 문단을 수정하면 모바일 쪽 DOM도 즉시 바뀌어야 하고, 모바일에서 block 순서를 바꾸면 태블릿 쪽에서도 같은 순서로 반영되어야 합니다.
결국 여러 preview DOM은 서로 독립된 원본이 아니라, 같은 document state의 서로 다른 projection이어야 했습니다. source of truth는 하나여야 하고, shadowRoot들은 그 상태를 각 viewport 조건에 맞게 렌더링한 결과여야 했습니다. 그렇지 않으면 태블릿 DOM과 모바일 DOM이 서로 다른 책이 되어 버립니다. 어느 쪽에서 편집했는지에 따라 결과가 달라지는 순간, 에디터는 신뢰를 잃습니다.
그래서 편집 이벤트는 특정 preview에서 발생하더라도 바로 공통 document state로 올라가야 했습니다. 그 state가 XHTML/CSS 문자열로 반영되고, CM6와 IndexedDB를 갱신한 뒤, 다시 모든 preview target에 내려갑니다. 태블릿에서 편집하든 모바일에서 편집하든 흐름은 같아야 했습니다. 화면은 여러 개지만 문서는 하나여야 했습니다.
실시간 동기화
이 단계에서 옵클 에디터의 구조는 “EPUB 파일을 만드는 도구”에서 “하나의 책 상태를 여러 표현 층에 동시에 반영하는 시스템”으로 확장되었습니다. CodeMirror 6의 문자열, Shadow DOM의 block tree, 모바일/태블릿 preview, CSSOM에 가까운 style state, IndexedDB에 저장된 프로젝트 데이터가 모두 같은 상태를 가리키도록 설계했습니다.
실시간 동기화는 보기에는 즉각 반응하는 UI처럼 보이지만, 내부적으로는 순서와 출처를 잘 정해 두는 작업입니다. 사용자가 미리보기에서 문단을 드래그하는 동안 CM6 쪽 update가 들어오면 어떤 source를 기준으로 처리할지, CSS 수정이 preview rerender를 유발하는 동안 selection을 어떻게 유지할지, 저장 중인 IndexedDB transaction이 끝나기 전에 또 다른 block edit이 들어오면 어떤 순서로 commit할지를 정해 두었습니다. 이런 설계가 있어야 UI가 가볍게 움직이면서도 내부 상태가 흐트러지지 않습니다.
이 구조는 어렵게 돌아가는 방식이 아니라, 기준만 정확히 두면 꽤 명쾌하게 움직입니다. 저는 에디터를 “문자열 편집기”가 아니라 “문서 상태를 여러 렌더링 표면에 투영하는 시스템”으로 보고, 공통 document state를 기준에 두었습니다. HTML과 CSS는 최종 산출물의 핵심으로 남기되, 사용자가 직접 만지는 표면은 block과 layout과 preview로 옮겼습니다. CM6는 사라지지 않았습니다. 내부 문자열의 정확성을 유지하는 계층으로 두고, Shadow DOM 편집 결과가 CM6 dispatch와 IndexedDB 저장 흐름을 통과하도록 만들었습니다.
결과적으로 이 동기화 구조는 완성도 있게 잡혔습니다. 미리보기에서 block을 옮기면 DOM 순서가 바뀌고, 그 변경이 XHTML 문자열과 CM6 상태에 반영되고, IndexedDB에는 변경된 문서와 리소스 상태가 저장되며, 모바일과 태블릿 preview도 같은 document state를 기준으로 다시 맞춰졌습니다. iframe 대신 Shadow DOM을 선택한 것도 이 구조에서는 맞는 판단이었습니다. 편집 표면, 코드 문자열, 저장 데이터, responsive preview가 서로 다른 방향으로 어긋나지 않도록 흐름을 정리했고, 반복 검수와 테스트에서도 안정적으로 통과했습니다. 미리보기는 더 이상 보조 화면이 아니라 옵클 에디터의 핵심 편집 엔진이 되었습니다.
이전글
목록으로
다음글
저작권 고시
Copyright Notice
본 웹사이트의 모든 디자인 결과물 및 영상에 대한 저작권은 Abstract Cloud에 있으며, 저작권법 및 관련 법령에 의해 보호받습니다. 웹, 영상, 본문, 표지, 내지 디자인을 포함한 모든 콘텐츠는 저작권자의 자산으로, 사전 동의 없이 무단 복제, 배포, 2차 저작물 제작, 온라인 공유 등을 금지합니다. 이를 위반할 시, 저작권법에 따라 민형사상 책임을 질 수 있습니다. 정당한 구매와 저작권 보호는 창작자의 권리를 지키며, 더 나은 작품으로 보답할 힘이 됩니다.
저작권자: Abstract Cloud | 대표자: 배창규(uragen)
© Abstract Cloud. All Rights Reserved.
HOME
FAQ
이용 약관
개인정보 이용방침
help@opkle.app
010-2747-3403
상호 :
추상적 형상 디자인(Abstract cloud) |
대표자 :
배창규
사업자등록번호 :
249-74-00533
통신판매업 신고번호 :
2025-의정부송산-0634
주소 :
경기도 의정부시 부용로 49, 108동 402호
웹의 모든 콘텐츠, 디자인, 소스 코드에 대한
저작권은 Opkle에게 있습니다.