옵클(Opkle)은 창작자를 위한 다양한 앱과 시스템을 제공하는 개발사입니다. 전자책 에디터 앱 'Opkle editor'를 출시했고, 관련 전자책 클래스를 제공하고 있습니다.
EDITORCLASSBLOGLOGIN표현한다는 것의무한한 가능성,새로운 형태로 담아내다.새로운 형태의 콘텐츠Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.옵클 에디터 개발기: Valkey 큐 서버의 기본 구조dev7옵클 에디터 개발기: Worker loop와 AI 결과 검증dev8옵클 에디터 개발기: 미리보기가 그대로 코드로dev9옵클 에디터 개발기: 그래픽 에디터도 결국 직접 만들기로 했다dev10옵클 에디터 개발기: Rust 공장을 그래픽 엔진으로 다듬다dev117891011...9 옵클 에디터 개발기: 미리보기가 그대로 코드로큐 서버가 안정적으로 돌아가게 된 다음에는, AI가 옵클 에디터의 작업 방식을 익히게 해야 했습니다. 모델이 문장을 잘 쓰는 것과 에디터 안에서 제대로 일하는 것은 다른 문제입니다. 에디터 안에서는 사용자가 렌더링된 EPUB 미리보기를 보고 있고, 실제 저장되는 것은 XHTML과 CSS입니다. 사용자가 눈앞의 문단을 고쳤는데 뒤의 코드가 다르게 남으면 안 됩니다. AI도 마찬가지였습니다. 화면에서 한 일과 EPUB 안에 들어가는 코드가 같은 방향으로 움직여야 했습니다.제가 만들고 싶었던 것은 “HTML을 써주는 AI”가 아니었습니다. 사용자는 코드 편집기를 보고 있지 않습니다. 문단을 선택하고, 이미지를 누르고, 서식을 바꾸고, 미리보기 위에서 바로 명령을 내립니다. 그러면 에디터는 선택된 대상의 현재 clean HTML, 적용 중인 general CSS, chapter local CSS, target id, 사용자 명령을 묶어 AI에게 넘깁니다. AI는 그 상태를 읽고, 실제 EPUB에 들어갈 수 있는 outerHTML과 stylesheet patch를 돌려줘야 합니다.여기서 중요한 것은 그럴듯한 화면을 만드는 게 아니었습니다. 다시 적용할 수 있는 수정 결과를 만드는 것이었습니다. 미리보기에서 바뀐 모습과 저장되는 XHTML/CSS가 같아야 하고, CM6 문자열과 document state, IndexedDB 저장 상태까지 같은 변경으로 이어져야 합니다. 그래서 AI에게 자유롭게 설명하게 하지 않고, `edits`와 `commands`를 가진 JSON 결과만 받도록 했습니다. AI가 할 수 있는 일은 선택 대상의 HTML/CSS를 고치거나, 에디터가 아는 UI command를 실행하는 것뿐입니다.Preview as source옵클 에디터에서 미리보기는 단순히 결과를 보는 화면이 아닙니다. 실제로 편집이 일어나는 표면입니다. 사용자는 EPUB 코드를 직접 보지 않아도 렌더링된 문단과 이미지와 표를 보면서 작업합니다. 그렇지만 EPUB은 결국 XHTML과 CSS로 만들어집니다. 그래서 미리보기에서 일어난 편집은 반드시 코드로 되돌아갈 수 있어야 합니다. 이 연결이 느슨하면 미리보기는 편집기가 아니라 미리보기로만 남습니다.그래서 선택된 block은 단순 DOM node가 아니라 코드 주소를 가진 대상이어야 했습니다. 사용자가 어떤 문단을 선택하면, 에디터는 그 문단의 id와 현재 outerHTML, 적용 중인 CSS context를 같이 잡습니다. AI에게도 똑같이 넘깁니다. “이 문장을 조금 더 크게 해줘”라는 말만 보내면 부족합니다. 지금 대상의 HTML이 무엇인지, 어떤 general rule과 local rule이 영향을 주고 있는지까지 같이 줘야 합니다.이렇게 해두면 미리보기와 코드 편집기의 역할이 분명해집니다. 미리보기는 사람이 만지는 표면이고, XHTML/CSS는 저장과 export의 기준입니다. AI는 미리보기 DOM을 직접 휘젓는 것이 아니라, 미리보기에서 선택된 대상에 적용할 수 있는 코드 patch를 만들어야 합니다. 그래야 AI로 바꾼 결과가 화면에만 남지 않고 EPUB 파일 안에도 남습니다.Shadow DOM 기반 미리보기와도 이 방식은 잘 맞았습니다. Shadow DOM 안의 렌더링 결과는 독립적인 스타일 환경을 갖지만, 에디터는 그 안에서 선택된 block을 document state의 node와 연결할 수 있습니다. AI에게 전달되는 것은 화면에 보이는 텍스트 한 조각이 아니라, 다시 저장 가능한 target context입니다. 미리보기는 사람에게는 편집 화면이고, AI에게는 구조화된 작업 대상이 됩니다.Selector modeAI가 미리보기 편집에 들어오려면 일반 writer mode와 다른 경로가 필요했습니다. writer mode는 책에 들어갈 문장을 쓰거나 다듬습니다. selector mode는 선택된 대상의 HTML/CSS를 고칩니다. 그래서 selector system prompt는 처음부터 선택 대상에 대한 action engine으로 잡았습니다. 사용자가 문서 안에서 하나 이상의 target을 선택했고, 그 target들의 현재 clean HTML과 적용 CSS가 주어졌다는 전제로 움직입니다.selector mode의 입력은 대상별 outerHTML과 CSS context입니다. HTML은 현재 node를 통째로 대체할 수 있는 전체 outerHTML이어야 하고, CSS는 book-wide stylesheet인 general과 chapter stylesheet에 가까운 local로 나뉩니다. AI는 이 정보를 보고 사용자의 요청을 해석합니다. 결과는 선택 대상 자체를 바꾸는 `edits`와 에디터 UI를 움직이는 `commands`로 나뉩니다.이 구분이 생각보다 중요했습니다. 사용자가 “이 문단을 파란색으로 바꿔줘”라고 하면 selector는 HTML inline style이나 general CSS override를 반환해야 합니다. 그런데 “서식 팝업 열어줘”라고 하면 HTML을 건드리면 안 됩니다. command만 반환하면 됩니다. 또 “이 문단을 강조하고 서식 팝업도 열어줘”처럼 두 일이 섞이면 `edits`와 `commands`가 같이 나올 수 있습니다. 한 입력 안에서도 코드 수정과 UI 제어가 동시에 존재할 수 있는 구조입니다.그래서 selector output은 JSON 하나로 고정했습니다. `{"edits":[{"id":"...","html":"...","css":{"general":"...","local":""}}],"commands":["..."]}` 형태입니다. 설명도, markdown도, code fence도 없어야 합니다. 에디터가 이 결과를 그대로 parse해서 적용하기 때문입니다. AI가 친절하게 설명하는 것보다, 에디터가 바로 실행할 수 있는 결과를 주는 것이 더 중요했습니다.OuterHTML contractselector mode에서 HTML은 항상 full outerHTML로 받게 했습니다. 부분 HTML이나 innerHTML만 받으면 적용 위치가 애매해집니다. 선택된 node를 통째로 교체할 수 있어야 하므로, AI는 해당 target의 전체 element를 반환해야 합니다. 요청이 tag 변경을 요구하지 않는다면 기존 tag 구조를 유지하고, 필요한 attribute와 inline style만 바꾸는 것이 기본입니다.id는 절대 바꾸면 안 됩니다. target id는 에디터가 patch를 다시 꽂는 주소입니다. AI가 id를 빼먹거나 새 id를 만들면 결과를 적용할 수 없습니다. 그래서 system prompt에서도 id를 정확히 유지하라고 고정했고, sanitize 단계에서도 id가 없는 edit은 버리게 했습니다. 되돌려 꽂을 주소가 없는 patch는 아무리 좋아 보여도 쓸 수 없습니다.CSS만 바꾸는 경우에도 html field는 필요했습니다. general stylesheet에 override rule만 추가하는 작업이어도, selector 결과의 shape는 같아야 합니다. 그래야 적용 경로가 단순해집니다. 대신 실제로 바꾼 target만 `edits`에 포함합니다. 바꾸지 않은 대상까지 매번 echo하면 불필요한 DOM replace가 일어나고, selection state나 undo stack이 괜히 흔들릴 수 있습니다.이 contract를 두면 AI가 만든 HTML은 미리보기 DOM과 document state 사이에서 안정적인 patch 단위가 됩니다. 사용자는 “이 문단 좀 더 잘 보이게 해줘”라고 말하지만, 서버는 target id와 outerHTML을 받고, AI는 수정된 outerHTML을 반환하고, 에디터는 해당 node를 교체합니다. 그 다음 serialize된 XHTML, CM6 문자열, IndexedDB 저장 상태가 같은 변경으로 따라옵니다.CSS channelsCSS는 더 조심해서 다뤘습니다. AI에게 stylesheet를 마음대로 다시 쓰게 하면 EPUB 전체가 흔들릴 수 있습니다. 그래서 selector mode에서 스타일을 바꾸는 채널은 두 개로 제한했습니다. 하나는 inline style이고, 다른 하나는 general stylesheet append입니다. local CSS는 읽기 전용 context로만 줍니다. AI가 local CSS를 수정해서 반환하는 것은 막았습니다.inline style은 해당 element 하나에만 적용되는 변경에 씁니다. 선택된 문단 하나의 글자색을 바꾸거나, 특정 이미지 하나의 margin을 조정하는 경우에는 outerHTML 안의 style attribute에 직접 넣는 것이 안정적입니다. patch 범위가 분명합니다. target 하나에만 영향을 주기 때문에 다른 문단이나 서식이 예상치 않게 바뀌지 않습니다.general stylesheet는 공유 서식이나 공통 selector를 바꿔야 할 때 씁니다. 모든 `
`에 적용되는 규칙이나 특정 class의 스타일을 바꿔야 한다면 `css.general`에 새 rule만 넣습니다. 여기서 중요한 것은 append-only입니다. 기존 stylesheet를 다시 쓰거나 그대로 echo하지 않습니다. 뒤에 붙는 rule이 cascade에서 이기므로, 필요한 override rule만 반환하면 됩니다. 기존 CSS를 부수지 않고 변경을 쌓는 방식입니다.