Opkle(옵클) - 창작자를 위한 앱과 시스템
옵클(Opkle)은 창작자를 위한 다양한 앱과 시스템을 제공하는 개발사입니다. 전자책 에디터 앱 'Opkle editor'를 출시했고, 관련 전자책 클래스를 제공하고 있습니다.
EDITOR
CLASS
BLOG
LOGIN
표현한다는 것의
무한한 가능성,
새로운 형태로
담아내다.
새로운 형태의 콘텐츠
Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.
텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.
옵클 에디터 개발기: 그래픽 엔진 안의 선형대수
dev
12
옵클 에디터 개발기: 인디자인처럼 편집하는 레이아웃 편집기
dev
13
옵클 에디터 개발기: 원고가 다음 페이지로 흐르는 편집기
dev
14
옵클 에디터 개발기: EPUB과 IDML을 편집 가능한 페이지로
dev
15
옵클 에디터 개발기: 한 번 만든 레이아웃을 EPUB과 인쇄물로
dev
16
8
9
10
11
12
...
13
옵클 에디터 개발기: 인디자인처럼 편집하는 레이아웃 편집기
벡터 에디터와 그 뒤의 Rust 그래픽 엔진까지 만들고 나니 다음에 필요한 도구는 자연스럽게 정해졌습니다. 책 안에 들어갈 이미지와 도형을 직접 만들 수 있게 되었지만, 그것들을 실제 페이지 위에 배치하는 일은 전혀 다른 종류의 편집이었습니다. 그래픽 에디터가 하나의 평면 위에서 오브젝트를 다룬다면, 레이아웃 에디터는 여러 페이지와 펼침면, 도련, 마진, 마스터 페이지와 인쇄 규격을 동시에 이해해야 합니다.
처음부터 이 에디터를 페이지 테두리가 추가된 벡터 캔버스로 생각하지는 않았습니다. 실제 책에서는 같은 좌표를 가진 오브젝트라도 어느 페이지에 속했는지에 따라 전혀 다른 대상이 되고, 왼쪽 면과 오른쪽 면은 책등을 기준으로 서로 다른 마진을 가집니다. 사진은 책등을 넘어 펼침면 전체에 걸칠 수 있어야 하고, 공통 머리말과 쪽번호는 개별 페이지가 아니라 마스터에서 관리되어야 합니다.
그렇다고 종이책의 규칙만 복제할 생각도 없었습니다. 옵클의 레이아웃은 나중에 EPUB과 웹으로 다시 나가야 하므로 자유 좌표와 흐름형 구조를 함께 품을 수 있어야 했습니다. 우선은 이 모든 작업의 바닥이 되는 페이지 공간부터 정확하게 만들기로 했습니다. 문서 상태를 어떻게 나눌지, 물리 단위를 무엇으로 계산할지, 화면에서 일어난 조작을 어느 페이지의 좌표로 저장할지부터 순서대로 정리했습니다.
벡터 다음의 빈자리
벡터 에디터의 중심은 오브젝트입니다. 사각형, 타원, path, text, image와 group이 하나의 좌표계 위에서 움직이고 selection과 transform이 그 오브젝트를 따라갑니다. 레이아웃 에디터에도 이 기능은 그대로 필요했지만, 그 위에 페이지라는 상위 소유권이 추가됩니다. 선택된 오브젝트가 같아도 현재 페이지가 바뀌면 활성 layer와 저장 위치, clip 영역, 적용되는 마스터가 모두 바뀌어야 합니다.
그래서 프로젝트 상태를 문서 메타, 페이지 문자열, 마스터 문자열, 바이너리 자산으로 분리했습니다. 문서 메타에는 판형과 도련, 마진, 단면·펼침 설정, 페이지 순서, 마스터 연결, 텍스트 스타일 정의가 들어갑니다. 각 페이지의 실제 SVG 구조는 독립된 문자열 상태로 두고, 이미지와 폰트는 별도의 blob 자산으로 보관했습니다. 페이지 수가 늘어나도 하나의 거대한 HTML 문자열이 프로젝트 전체를 독점하지 않도록 한 구조입니다.
페이지에는 표시 순서와 별개인 안정적인 식별자를 주었습니다. 페이지를 중간에 삽입하거나 삭제하면 index는 계속 달라지지만, 저장된 DOM과 history, 마스터 연결은 같은 페이지를 계속 가리켜야 하기 때문입니다. 화면에서는 index로 빠르게 순회하되 영속 상태의 참조는 page id를 기준으로 맞췄습니다. 마스터도 왼쪽 면과 오른쪽 면의 DOM을 각각 따로 가지고, 페이지는 자신에게 적용할 master id만 기억합니다.
이 분리 덕분에 기존 벡터 오브젝트 모델도 유지할 수 있었습니다. 대부분의 이동·회전·리사이즈 함수는 여전히 하나의 objects layer를 다루는 것처럼 동작합니다. 페이지를 바꿀 때 활성 layer를 가리키는 참조만 교체하고, 같은 편집 연산을 새 페이지의 로컬 공간에 적용했습니다. 바깥에서는 수백 페이지짜리 문서지만 오브젝트 편집기는 항상 명확한 하나의 작업 공간만 바라보게 만들었습니다.
Physical unit
레이아웃 에디터에서는 좌표 단위부터 다시 정리해야 했습니다. 화면에서 익숙한 CSS pixel과 인쇄 규격에서 쓰는 point, 사용자가 판형과 여백을 입력할 때 쓰는 millimeter는 같은 숫자가 아닙니다. 저는 문서 규격과 출력 좌표를 point 계열로 유지하고, 사용자에게 보이는 판형과 마진 입력은 millimeter로 변환해 보여주도록 잡았습니다. PDF와 IDML이 point 공간을 중심으로 움직이기 때문에 내부 기준을 여기에 맞추는 편이 정확했습니다.
변환의 정준 단위는 inch로 두었습니다. `1 inch = 25.4 mm`, `1 inch = 72 pt`이고 CSS pixel은 기본 96 DPI를 기준으로 환산됩니다. 임의의 길이는 먼저 inch 비율로 정규화한 뒤 목적 단위로 돌려보냈습니다. mm와 pt, px와 pt 사이에 서로 다른 변환 상수를 흩어놓는 대신 하나의 단위 경로를 공유하면서 입력창, 문서 초기값, 프리셋, SVG 좌표와 PDF 출력이 같은 값을 사용하게 했습니다.
A4도 단순히 `595 × 842`처럼 반올림한 값으로 저장하지 않았습니다. `210 × 297 mm`를 정확히 point로 환산한 값을 기본으로 두어 다시 mm로 표시했을 때 210과 297이 깨끗하게 돌아오게 했습니다. 작은 반올림 차이는 한 페이지에서는 눈에 띄지 않지만 페이지 박스와 도련, 인쇄 마크를 반복 계산하면 누적될 수 있습니다. 판형 preset과 새 문서, reset, 과거 프로젝트 fallback까지 같은 기준값을 쓰도록 맞췄습니다.
인쇄 해상도가 필요한 시점에는 문서 좌표와 raster 좌표를 다시 분리했습니다. 페이지에서 1 unit이 의미하는 물리 길이를 먼저 구하고, 출력 DPI에 따라 pixel 수를 결정합니다. 편집 화면의 zoom은 사용자가 보는 배율일 뿐 문서의 실제 크기에 영향을 주지 않습니다. 이 원칙을 고정한 뒤부터 화면 확대, PDF 출력, 고해상도 이미지 출력이 서로 다른 크기를 만드는 일이 없어졌습니다.
세 개의 좌표 공간
단위가 정리된 다음에는 좌표 공간을 분리했습니다. 마우스 이벤트가 주는 값은 브라우저 화면의 client 좌표이고, SVG stage는 pan과 zoom이 적용된 viewport 좌표를 사용합니다. 실제 오브젝트는 다시 각 페이지의 왼쪽 위를 `(0, 0)`으로 삼는 page local 좌표에 저장됩니다. 같은 점이 화면, 캔버스, 페이지에서 서로 다른 숫자를 가지는 구조입니다.
화면의 점 `p_screen`에서 stage의 translation을 빼고 zoom으로 나누면 world 좌표가 나옵니다. 여기서 대상 페이지 group의 translation을 빼야 page local 좌표가 됩니다. 식으로 줄이면 `p_local = (p_screen - t_view) / zoom - t_page`입니다. 마스터 편집 공간은 별도의 page translation을 사용하므로 현재 모드와 편집 중인 면에 따라 마지막 변환 행렬을 선택했습니다.
이 변환은 단순한 pointer 계산을 넘어 모든 삽입 경로에 적용했습니다. 사용자가 다른 페이지 위로 이미지를 드롭하면 먼저 screen point와 각 page rect의 교차를 계산해 대상 페이지를 찾고, 그 페이지를 활성화한 뒤 local point로 변환합니다. 오브젝트를 펼침면 반대쪽으로 옮길 때도 출발 페이지 좌표를 root 좌표로 올린 다음 도착 페이지의 local 좌표로 내립니다. 중간에 직접 x 차이만 더하면 zoom이나 spread gap이 바뀌는 순간 위치가 어긋납니다.
선택 영역과 resize handle도 같은 원칙을 따랐습니다. 오브젝트 geometry는 page local에 남겨두고 selection overlay만 root layer에 그렸습니다. 그래서 여러 오브젝트를 함께 선택하거나 서로 다른 page translation을 가진 면으로 이동해도 실제 저장 좌표는 오염되지 않습니다. 화면에 맞게 보이는 좌표와 문서에 맞게 저장되는 좌표를 분리한 것이 멀티페이지 편집의 기준점이 되었습니다.
펼침면의 기하
페이지 배치는 단면과 펼침면을 서로 다른 규칙으로 계산했습니다. 단면에서는 페이지를 세로로 나열하면 되지만, 펼침면에서는 첫 페이지가 오른쪽에 단독으로 놓이고 그다음부터 왼쪽과 오른쪽 페이지가 한 쌍을 이룹니다. 페이지 parity로 면을 판정하고, 한 쌍이 완성될 때마다 다음 spread row로 이동시켰습니다. 페이지 추가와 삭제로 parity가 바뀌면 이후 면의 마진과 마스터도 다시 계산합니다.
도련이 들어가면 단순히 페이지 폭만큼 떨어뜨려서는 안 됩니다. 책등 쪽의 두 bleed 영역이 겹쳐 보이거나 반대로 불필요한 틈이 생길 수 있으므로 spread gap에는 양쪽 bleed와 실제 작업 간격을 함께 반영했습니다. 페이지 배경, 도련 가이드, 마진 가이드, 재단 경계는 본문 오브젝트보다 아래에 있는 전용 layer로 분리했습니다. 활성 페이지를 앞으로 올려도 흰 배경이 옆 페이지의 펼침 이미지를 덮지 않게 하기 위한 배치입니다.
책등을 가로지르는 오브젝트를 위해 clip도 비대칭으로 만들었습니다. 모든 페이지에 자기 width만큼의 clip을 주면 왼쪽 페이지에서 시작한 사진이 오른쪽으로 넘어가는 순간 잘립니다. 서로 마주 보는 면에서는 clip 영역을 책등 방향으로만 이웃 페이지까지 확장하고, 바깥쪽과 위아래는 계속 page box 기준으로 잘리게 했습니다. 한 오브젝트가 어느 페이지에 소속됐는지 유지하면서도 실제 펼침면에서는 자연스럽게 이어져 보입니다.
마진은 left와 right보다 inside와 outside의 의미를 먼저 가집니다. 오른쪽 홀수 면에서는 inside가 물리적인 왼쪽이고, 왼쪽 짝수 면에서는 inside가 오른쪽입니다. 문서 설정을 적용할 때 논리 마진을 페이지 parity에 맞는 물리 마진으로 바꾸고 본문 가이드와 자동 텍스트 프레임에 반영했습니다. 비대칭 제본 여백과 좌우 마스터가 페이지 순서에 따라 정확하게 뒤집히도록 만들었습니다.
Smart guide
레이아웃 작업에서는 정확한 숫자만큼 손으로 움직이는 감각이 중요했습니다. 오브젝트를 끌었을 때 페이지 중앙과 모서리, 마진선, 다른 오브젝트의 중심과 경계에 자연스럽게 붙어야 합니다. rect와 ellipse, image, foreignObject, path와 group은 geometry 저장 방식이 다르기 때문에 먼저 실제 bounds를 공통 형태 `(x, y, w, h)`로 정규화했습니다.
선택 영역의 왼쪽·가운데·오른쪽과 위·가운데·아래를 스냅 축으로 만들고, 페이지와 주변 오브젝트의 같은 축을 후보로 수집했습니다. 각 후보와의 delta를 비교해 임계값 안에서 가장 가까운 값만 이동량에 반영했습니다. zoom이 커져도 화면에서 느껴지는 스냅 거리가 지나치게 달라지지 않도록 threshold는 문서 좌표와 화면 좌표 사이에서 현재 배율에 맞게 환산했습니다.
임계값 하나만 사용하면 경계에서 오브젝트가 붙었다 떨어지기를 반복합니다. 그래서 스냅이 시작된 마우스 위치와 적용한 delta를 기억하고, 진입보다 넓은 해제 임계값을 가진 hysteresis를 넣었습니다. 사용자가 스냅 축을 분명하게 벗어날 때까지 기존 결정을 유지하므로 느리게 움직여도 오브젝트가 떨리지 않습니다. x축과 y축은 독립적으로 잠글 수 있게 해 한쪽 정렬을 유지한 채 다른 방향으로 계속 움직일 수 있습니다.
등간격 스냅은 별도의 후보군으로 계산했습니다. 선택 오브젝트와 좌우 이웃 사이의 간격 `g_left`, `g_right`를 비교하고 두 값이 같아지는 이동량을 찾았습니다. 세로축도 같은 방식으로 위아래 간격을 계산했습니다. 일반 정렬이 이미 걸린 축에서는 등간격 판단이 개입하지 않게 우선순위를 두었고, 실제 간격은 치수선과 숫자 배지로 표시했습니다. 가이드는 page local에서 계산하지만 root overlay에 그려 pan과 zoom 중에도 정확히 따라오도록 했습니다.
룰러도 현재 viewport가 보고 있는 world 범위를 역산해 눈금을 만들었습니다. zoom에 따라 major tick과 minor tick 간격을 바꾸고, 화면 밖의 눈금은 생성하지 않았습니다. pointermove마다 stage의 layout 값을 다시 읽으면 강제 reflow 비용이 생기므로 bounding rect와 변환값을 캐시하고 pan, zoom, resize 시점에만 무효화했습니다. 조작의 부드러움은 결국 좌표 계산과 브라우저 layout 비용을 함께 관리한 결과였습니다.
Object hierarchy
페이지 안의 오브젝트는 모두 같은 깊이에만 놓이지 않습니다. 여러 요소를 묶은 group은 하나의 transform 대상으로 움직여야 하지만, 필요할 때는 안쪽 요소를 다시 선택하고 편집할 수 있어야 합니다. 일반 선택 모드에서는 group을 하나의 객체로 취급하고, 더블 클릭하면 그 group만 편집하는 isolation 상태로 들어가도록 만들었습니다.
격리 상태에서는 선택 탐색의 root를 전체 page layer가 아니라 현재 group으로 제한했습니다. 마우스 아래에 group 바깥의 요소가 함께 겹쳐 있어도 선택 후보로 승격하지 않고, group의 직계 자식만 선택합니다. 새 도형이나 텍스트를 만들면 현재 page가 아니라 격리 중인 group 안에 삽입됩니다. Tab 순회와 marquee selection도 같은 root를 사용해서 화면상의 격리와 실제 DOM 소유권이 일치하도록 했습니다.
현재 편집 범위는 점선 outline으로 보여주되 이 overlay는 문서 데이터가 아니므로 transient 요소로 분리했습니다. 저장 문자열과 history snapshot을 만들기 전에 제외하고, zoom이 바뀌면 stroke와 dash 간격을 다시 맞췄습니다. 바깥 배경을 클릭하거나 페이지를 전환하면 격리를 끝내고 group 자체를 다시 선택합니다. 편집 중 group이 삭제되거나 DOM에서 분리된 경우에도 상태를 자동으로 정리해 죽은 참조가 selection에 남지 않게 했습니다.
페이지 배경과 가이드, 마스터 DOM, 개별 오브젝트, selection overlay도 같은 이유로 서로 다른 계층에 두었습니다. 저장되는 layer와 화면에만 존재하는 layer를 구분하니 undo에 resize handle이 남거나 출력물에 안내선이 섞이는 일을 막을 수 있었습니다. 레이아웃 에디터의 page는 단순한 SVG 한 장이 아니라 소유권과 편집 수명을 구분한 여러 layer의 조합으로 완성되었습니다.
Master page
책 전체에서 반복되는 요소를 매 페이지에 복사해 관리할 수는 없습니다. 머리말과 꼬리말, 장식선, 배경, 쪽번호는 마스터 페이지의 왼쪽 면과 오른쪽 면에 각각 만들고 일반 페이지는 어떤 마스터를 적용할지만 저장하게 했습니다. 마스터 DOM은 페이지 본문과 독립적으로 보관하고, 실제 화면을 만들 때 현재 면에 맞는 내용을 전용 하부 layer에 복제합니다.
쪽번호는 고정된 문자열로 굽지 않고 현재 페이지 번호와 전체 페이지 수를 나타내는 변수로 남겼습니다. 마스터를 적용할 때 페이지 context를 주입해 표시값을 만들고, 페이지가 추가되거나 삭제되면 번호를 다시 계산했습니다. 첫 페이지를 오른쪽에 두는 펼침 규칙과 페이지 시작 번호도 같은 단계에서 처리했습니다. 공통 템플릿은 하나지만 각 페이지에서 보이는 숫자는 자신의 순서에 맞게 달라집니다.
마스터 편집 중에는 일반 페이지 history와 선택 상태를 격리했습니다. 왼쪽과 오른쪽 면을 전환할 때 각각의 DOM을 보존하고, 편집을 끝낼 때 한 번만 마스터 상태를 확정한 뒤 적용된 페이지에 다시 배포합니다. 페이지 수가 많을 때는 현재 페이지 주변부터 먼저 반영하고 나머지는 idle 작업으로 넘겨 편집 재개가 늦어지지 않게 했습니다.
개별 페이지에서는 마스터 요소를 일반 본문 오브젝트처럼 잘못 선택하지 않도록 했습니다. 공통 요소는 본문보다 아래에 렌더되지만 출력에는 포함되고, 마스터 편집 모드에서만 직접 선택됩니다. 적용과 해제, 좌우 면 전환, 페이지 재정렬까지 반복해도 본문 DOM과 섞이지 않는 구조를 완성했습니다.
페이지 엔진 완성
레이아웃 에디터의 첫 번째 완성점은 도구의 개수가 아니었습니다. 문서 메타와 페이지·마스터·자산이 안정적인 식별자로 연결되고, point로 보관한 물리 규격이 millimeter 입력과 정확히 왕복하며, 화면의 조작이 올바른 page local 좌표로 저장되는 것이 먼저였습니다. 이 바닥이 정확해야 이후의 텍스트 조판과 문서 변환도 같은 위치를 믿고 작동할 수 있습니다.
그 위에 첫 페이지가 오른쪽에 놓이는 펼침면, 책등 방향으로 확장되는 비대칭 clip, inside와 outside 마진의 미러링, 도련과 page box의 계층을 올렸습니다. 서로 다른 SVG 요소를 하나의 bounds 모델로 정규화해 정렬과 등간격 스냅을 만들었고, group isolation과 transient overlay로 복잡한 오브젝트 계층도 안전하게 편집할 수 있게 했습니다. 마스터 페이지와 쪽번호 변수까지 본문과 독립된 상태로 연결했습니다.
페이지 순서 변경, 단면과 펼침면 전환, 비대칭 마진, 서로 다른 도련, 고배율과 저배율, 페이지 간 오브젝트 이동, 중첩 group 편집, 좌우 마스터 적용을 반복하며 전체 좌표와 저장 결과를 검수했습니다. 화면에서 맞은 위치가 다시 열었을 때도 같은 물리 좌표로 복원되고, PDF용 page box와도 일치하는 것을 모두 확인했습니다. 벡터 에디터에서 만든 자산을 실제 책의 지면 위에 조립할 수 있는 페이지 엔진은 이 단계에서 완성도 있게 안정화되었습니다.
이전글
목록으로
다음글
저작권 고시
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에게 있습니다.