Opkle(옵클) - 창작자를 위한 앱과 시스템
옵클(Opkle)은 창작자를 위한 다양한 앱과 시스템을 제공하는 개발사입니다. 전자책 에디터 앱 'Opkle editor'를 출시했고, 관련 전자책 클래스를 제공하고 있습니다.
EDITOR
CLASS
BLOG
LOGIN
표현한다는 것의
무한한 가능성,
새로운 형태로
담아내다.
새로운 형태의 콘텐츠
Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.
텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.
옵클 에디터 개발기: 표 이미지를 만들기 위한 스프레드시트
dev
17
옵클 에디터 개발기: 같은 데이터를 표와 보드, 캘린더로
dev
18
옵클 에디터 개발기: 책을 웹에서 그대로 읽게 하는 퍼블리셔
dev
19
옵클 에디터 개발기: 편집 화면을 그대로 웹에 게시하기
dev
20
옵클 에디터 개발기: EPUB 에디터가 모든 작업을 연결하는 방식
dev
21
9
10
11
12
13
...
17
옵클 에디터 개발기: 표 이미지를 만들기 위한 스프레드시트
레이아웃 에디터까지 만들고 나니 표를 다루는 방식도 다시 생각하게 되었습니다. 출판물에 들어가는 표는 결과만 보면 이미지 한 장이지만 제작 과정에서는 값과 행·열, 병합, 정렬, 글꼴과 테두리가 계속 바뀝니다. 픽셀은 마지막 표본일 뿐이고 그 뒤에는 관계를 가진 데이터와 조판 규칙이 있어야 했습니다.
표 이미지를 그리는 기능부터 붙이지 않았습니다. 먼저 셀과 범위, 타입과 수식을 가진 계산 모델을 만들고 그 상태에서 출판용 장면을 다시 생성하도록 했습니다. 표의 원본은 화면 DOM도 이미지도 아니고, 좌표와 값 사이의 관계를 설명하는 희소한 문서였습니다. 화면과 출력은 그 문서의 서로 다른 projection이 됩니다.
이 구조를 잡으면서 일반적인 HTML table 하나로는 충분하지 않다는 것도 분명했습니다. 편집에는 빠른 국소 접근이 필요하고, 수식은 의존 graph를 만들며, 저장은 transaction과 durability를 요구하고, 출력은 브라우저 화면과 독립된 geometry를 계산해야 합니다. 표 에디터는 격자 UI보다 관계형 모델, 타입 시스템과 희소 행렬이 만나는 작은 계산 환경에 가까웠습니다.
Sparse relation
표를 이미지 오브젝트처럼만 다루면 셀이라는 개념이 금방 무너집니다. 글자 하나를 바꿀 때도 전체 픽셀을 다시 해석해야 하고 정렬이나 필터는 색상 배열 위에서 의미를 갖지 않습니다. 행과 열은 단순한 선이 아니라 레코드와 속성의 직교 좌표계입니다. 데이터가 먼저이고 geometry는 그 데이터를 보여주는 한 가지 방법이었습니다.
수학적으로 시트는 행 집합 R과 열 집합 C의 곱 `R × C` 위에 정의된 부분 함수로 볼 수 있습니다. 대부분의 셀이 비어 있으므로 모든 좌표를 물질화한 dense matrix보다 값이 정의된 좌표만 보존하는 sparse relation이 자연스럽습니다. 값뿐 아니라 style이나 수식이 있는 셀도 support set에 포함됩니다. 화면 크기와 실제 데이터의 크기가 분리되는 이유입니다.
열은 단순 좌표가 아니라 같은 domain을 공유하는 attribute입니다. 한 행은 여러 attribute를 가진 tuple이고, title이나 date처럼 역할이 있는 열은 schema의 일부가 됩니다. 행 순서와 열 순서는 사용자에게 중요하지만 relation의 정체성과는 별개입니다. 순서를 바꾸어도 같은 tuple과 attribute가 유지되어야 다양한 projection이 같은 데이터를 읽을 수 있습니다.
병합 셀도 값을 여러 칸에 복제하는 것이 아니라 하나의 anchor가 사각 영역을 소유하는 관계로 보았습니다. 병합 영역들은 서로 겹치지 않아야 하고 anchor는 영역 안에 정확히 하나 존재해야 합니다. 이런 불변식을 geometry가 아니라 데이터 모델에 두면 화면과 출력, 붙여넣기가 모두 같은 규칙을 따를 수 있습니다.
Relational document
살아 있는 표 문서는 관계형 모델과 잘 맞았습니다. 셀 값과 열 schema, 병합과 style은 서로 다른 relation이지만 key를 통해 같은 문서를 설명합니다. 하나의 거대한 중첩 객체를 매번 다시 쓰는 대신 변경된 tuple만 갱신할 수 있고, 범위 선택과 정렬도 selection과 order 연산으로 표현됩니다.
schema에서는 functional dependency를 명확히 해야 했습니다. 셀 좌표 `(sheet, row, column)`가 값과 수식을 결정하고, column identity가 이름과 type을 결정합니다. 같은 candidate key를 가진 tuple이 두 개 존재하면 어느 값이 권위적인지 알 수 없습니다. key와 uniqueness는 저장 기술의 옵션이 아니라 문서 의미의 일부였습니다.
정규화만 지나치게 밀어붙이면 편집할 때 join 비용이 커질 수 있습니다. 반대로 모든 정보를 셀 하나에 중복하면 열 이름이나 style을 바꿀 때 수많은 tuple이 서로 다른 값을 갖게 됩니다. update anomaly와 읽기 locality 사이에서 relation을 나누고, 자주 함께 읽는 파생값은 cache projection으로 두는 편이 맞았습니다.
자산은 relation 밖의 큰 값이지만 참조 graph 안에서는 같은 정점입니다. 문서가 소유한 자산과 외부에서 공유받은 자산은 생명주기가 다르고, 삭제 가능성은 root에서의 도달성으로 판단해야 합니다. 관계형 무결성과 graph reachability를 함께 보면서 표 하나의 변화가 다른 문서의 자산을 손상시키지 않도록 했습니다.
Gradual type system
열은 단순한 A, B, C 좌표보다 더 많은 의미를 갖습니다. 숫자와 날짜, 논리값, 범주형 값은 가능한 연산과 정렬 순서가 다릅니다. 그러나 사용자가 처음부터 모든 type을 선언하게 하면 표의 자유로운 입력 감각이 사라집니다. 동적 입력과 정적 schema 사이를 연결하는 gradual typing이 필요했습니다.
아직 type이 정해지지 않은 값은 top type에 가까운 unknown으로 두고, 관측된 입력에서 더 구체적인 후보를 추론할 수 있습니다. 여러 값의 type을 합칠 때는 모두를 수용하는 least upper bound를 찾습니다. 정수와 실수는 number로 합쳐질 수 있지만 날짜와 임의 문자열은 안전하게 하나로 좁혀지지 않습니다. 첫 셀 하나의 우연한 모양이 전체 열을 결정해서는 안 됩니다.
타입 변경은 annotation 변경보다 migration입니다. 변환 함수 `c: A -> B`가 모든 기존 값에 정의되는 total function인지, 일부 값에서 실패하는 partial function인지 구분해야 합니다. partial coercion을 조용히 적용하면 열 안에 B와 변환되지 않은 A가 섞입니다. 실패 집합을 먼저 계산하고 사용자가 선택할 수 있게 해야 schema와 실제 값의 관계가 정직하게 유지됩니다.
범주형 열은 유한 집합과 같습니다. 단일 선택은 집합의 원소 하나, 다중 선택은 power set의 원소입니다. 옵션 이름을 바꾸는 일은 단순 문자열 교체가 아니라 domain의 원소를 다른 이름으로 재명명하는 bijection이어야 합니다. 표시 색은 값의 identity와 분리된 projection입니다. 타입과 표현을 구분하면서 grid와 다른 view가 같은 값을 공유할 수 있었습니다.
Selection geometry
셀 선택은 정수 격자 `Z²` 위의 영역입니다. 단일 셀은 한 점이고 사각 범위는 두 끝점으로 정의되는 폐구간의 곱입니다. 화면의 pixel rect와 데이터의 정수 좌표를 분리하면 zoom과 scroll이 달라져도 선택의 의미는 유지됩니다. UI overlay는 정수 영역을 현재 viewport에 사상한 결과일 뿐입니다.
병합 영역이 있으면 단순한 사각 선택은 closure 연산이 됩니다. 선택 S가 병합 영역 M과 교차하지만 M 전체를 포함하지 않으면 `S <- bbox(S ∪ M)`으로 확장하고, 새 경계가 다른 병합과 만나는 동안 반복합니다. 유한한 시트에서는 매 반복마다 S가 단조롭게 커지므로 fixed point에 도달합니다. 최종 선택은 모든 교차 병합에 대해 닫힌 최소 사각형입니다.
숨겨진 행과 열은 데이터 공간에는 존재하지만 표시 공간에서는 measure가 0인 구간처럼 취급할 수 있습니다. 누적 열 너비와 행 높이의 prefix sum을 만들면 셀 좌표에서 pixel 위치를 빠르게 구할 수 있고, 반대로 pointer 위치에서는 이 누적 함수의 lower bound를 찾아 셀을 얻습니다. 가변 크기 grid의 hit test도 결국 단조 배열 위의 탐색 문제였습니다.
키보드 이동은 활성 점과 anchor가 있는 상태기계입니다. 단순 이동은 두 값을 함께 옮기고 범위 확장은 anchor를 고정한 채 active point만 바꿉니다. 텍스트 편집에 들어가면 방향키의 정의역이 grid에서 문자열로 전환됩니다. 같은 키가 어떤 공간의 연산인지 mode를 명시하면서 선택 geometry와 문자 caret이 충돌하지 않게 했습니다.
Expression state
셀에는 표시값과 원본 표현식이 동시에 존재할 수 있습니다. 수식 `=A1+B1`은 source이고 화면의 숫자는 현재 의존 graph에서 평가한 value입니다. source를 value로 덮으면 다음 재계산의 의미가 사라집니다. 편집 상태는 committed expression, transient input과 evaluated result를 분리해야 했습니다.
수식 parser는 문자열을 abstract syntax tree로 바꾸고 셀 참조를 symbol로 해석합니다. 각 수식 셀을 정점으로 두고 참조를 edge로 만들면 계산 순서는 dependency graph의 topological order가 됩니다. cycle이 있으면 순서를 정의할 수 없으므로 오류로 다루거나 명시적인 iterative semantics를 선택해야 합니다. 일반 표 계산에서는 비순환 graph가 가장 예측 가능했습니다.
한글 입력기의 조합 문자열도 transient expression과 비슷합니다. 아직 확정되지 않은 입력을 문서 source로 커밋하면 같은 자모가 두 번 들어가거나 중간 문자열이 수식 parser로 넘어갈 수 있습니다. composing state에서는 입력기가 소유한 값을 화면에만 보여주고, 확정 전이 뒤에 하나의 transaction으로 source를 갱신해야 합니다.
재계산은 dirty subgraph만 대상으로 할 수 있습니다. 셀 x가 바뀌면 x에서 역방향으로 도달 가능한 dependent set만 다시 평가하면 되고, 서로 독립적인 component는 영향을 받지 않습니다. 입력 commit과 graph evaluation, 화면 projection을 하나의 revision 경계로 묶으면서 수식과 다국어 입력이 같은 상태 모델 안에 들어왔습니다.
Sequence algebra
복사와 채우기는 2차원 부분행렬을 다른 위치로 사상하는 연산입니다. 값만 복사하는 경우에는 translation으로 충분하지만 수식의 상대 참조는 이동 벡터에 따라 바뀌어야 합니다. 절대 참조와 상대 참조를 구분하면 수식 AST의 좌표 항을 affine하게 변환할 수 있습니다.
붙여넣기는 외부 행렬을 typed relation에 합치는 과정입니다. 각 열 domain에 대해 값이 속하는지 검사하고 필요한 coercion이 total한지 확인해야 합니다. 일부 셀이 실패했을 때 전체 transaction을 거부할지, 성공 가능한 부분만 적용할지는 atomicity 정책입니다. 조용한 부분 성공보다 실패 집합과 적용 범위를 명확히 하는 편이 데이터 의미를 지킵니다.
자동 채우기는 짧은 sequence에서 생성 규칙을 추론하는 문제입니다. 숫자열 `a_n`의 1차 차분이 일정하면 arithmetic progression, 비율이 일정하면 geometric progression을 의심할 수 있습니다. 2차 차분이 일정한 sequence는 quadratic polynomial로 설명할 수 있습니다. 그러나 유한한 표본에는 무한히 많은 생성 함수가 맞으므로 가장 단순한 규칙을 선택하는 Occam의 기준이 필요합니다.
날짜와 텍스트 suffix도 각자의 algebra를 가집니다. 날짜는 달력의 가변 월 길이 때문에 단순 정수 덧셈과 다르고, 문자열은 prefix와 숫자 부분을 분해해야 합니다. 규칙을 추론하지 못하면 반복이라는 가장 보수적인 변환으로 돌아갑니다. fill은 작은 자동화 기능이지만 type과 sequence model을 함께 이해해야 예측 가능한 도구가 되었습니다.
Sparse style tensor
셀 서식도 값과 같은 데이터지만 대부분 기본값입니다. 행 R, 열 C, 속성 P의 곱 `R × C × P` 위에 정의된 희소 tensor로 볼 수 있습니다. 특정 셀에서 기본값과 다른 속성만 저장하면 빈 표의 비용이 셀 수에 비례하지 않습니다. 스타일 전체 객체를 복제하는 것보다 부분 함수의 support만 유지하는 편이 자연스럽습니다.
최종 style은 문서 기본값, 열과 행의 style, 셀 override를 합성해 얻습니다. 이때 각 계층은 property별 partial map이고 가까운 범위의 정의가 우선합니다. 동일한 style 집합이 반복되면 flyweight처럼 공유할 수 있지만, 공유 객체를 직접 mutate하면 여러 셀이 함께 바뀝니다. immutable style value와 structural sharing이 잘 맞았습니다.
숫자 format은 원본 값에서 표시 문자열로 가는 함수입니다. `format: Value × Locale × Pattern -> String`으로 두면 정렬과 수식은 원본 value를 사용하고 화면과 출력만 같은 formatter를 공유합니다. 표시 문자열을 다시 숫자로 해석하면 통화 기호와 소수점 locale 때문에 정보가 손실됩니다. 값과 representation을 분리하는 전형적인 사례였습니다.
테두리는 셀의 속성보다 격자 edge의 속성으로 보는 편이 수학적으로 정확합니다. 인접한 두 셀이 공유하는 변은 하나뿐인데 양쪽 셀이 각자 stroke를 가지면 충돌과 이중선이 생깁니다. 격자를 planar graph로 보고 edge에 style을 부여하면 병합과 외곽선도 동일한 topology에서 설명할 수 있습니다.
Ownership graph
이미지와 폰트는 셀 안에 보이지만 값 자체와는 다른 생명주기를 가집니다. 큰 binary를 모든 셀에 복제하기보다 자산 정점을 만들고 셀이 참조 edge를 갖는 편이 맞습니다. 같은 이미지를 여러 셀이 사용할 수 있고, style과 출력도 같은 font 정점을 공유할 수 있습니다. 문서는 관계형 표와 자산 graph의 결합이 됩니다.
소유권과 참조는 다릅니다. 문서 A가 만든 자산을 문서 B가 읽는다고 해서 B가 삭제 권한까지 얻는 것은 아닙니다. strong ownership edge와 borrowed reference를 구분해야 root 문서를 지울 때 어느 자산이 함께 사라질지 판단할 수 있습니다. 자산 생명주기는 단순한 파일 목록보다 capability와 reachability 문제였습니다.
undo가 과거 revision을 가리키는 동안 현재 화면에서 사라진 자산도 여전히 도달 가능합니다. 현재 revision만 mark하면 redo에 필요한 값이 조기에 수집됩니다. live roots에는 현재 문서뿐 아니라 보존된 history와 export 중인 snapshot도 포함되어야 합니다. garbage collection의 정확성은 무엇을 root로 정의하느냐에 달려 있었습니다.
폰트는 geometry에도 영향을 줍니다. 자산이 준비되지 않은 상태에서 fallback metric으로 측정하면 행 높이와 줄바꿈이 달라집니다. 따라서 font reference의 해소는 시각적 장식의 준비가 아니라 layout input의 확정입니다. 자산 graph의 특정 정점들이 ready 상태가 된 뒤에만 typography projection을 권위적인 결과로 볼 수 있었습니다.
Serializability
표 편집의 undo는 셀 값만 되돌려서는 부족했습니다. 열 type이 바뀌면 값과 수식, view가 함께 영향을 받고 행 삽입은 병합과 참조 좌표를 이동시킵니다. 하나의 사용자 명령이 여러 relation을 바꾸므로 ACID transaction과 같은 원자성이 필요합니다. 일부 write만 보이는 상태는 유효한 표가 아닙니다.
두 action이 같은 이전 revision을 읽고 각자 결과를 쓰면 lost update가 생길 수 있습니다. 실행 결과가 어떤 순차 순서와도 동등하도록 serializability를 보장해야 합니다. 가장 단순한 방법은 명령을 한 번에 하나씩 commit하는 것이고, 더 많은 병렬성을 원하면 read set과 write set이 충돌하는지 검사할 수 있습니다.
수식 재계산과 view projection은 base transaction에서 파생된 작업입니다. base revision r에 대해 계산한 결과가 r+1 위에 늦게 도착하면 stale result입니다. 모든 파생 결과에 source revision을 붙이고 현재 revision과 다르면 버려야 화면이 과거 값으로 돌아가지 않습니다. 비동기 렌더도 데이터베이스의 snapshot isolation과 같은 시간 모델을 필요로 했습니다.
history는 committed revision 사이의 이동입니다. snapshot은 복원이 빠르지만 크고, operation log는 작지만 역연산과 schema 변화에 민감합니다. 표의 복합 변경에는 의미 명령과 부분 snapshot을 섞는 방식이 맞았습니다. 화면 반응성과 durable history를 분리하되 둘이 같은 revision graph를 가리키게 했습니다.
저장이 끝나는 시점
저장이 끝난 시점은 write 함수가 호출된 순간이 아니라 특정 revision이 durable해졌다는 증명이 있는 순간입니다. 화면의 transient input과 계산 중인 파생값을 제외하고, committed base state와 필요한 자산 graph가 하나의 일관된 snapshot을 이뤄야 합니다. durability boundary는 UI lifecycle과 독립적으로 정의되어야 했습니다.
저장 중 새 revision이 생길 수 있으므로 시작할 때의 revision r과 완료 시점의 현재 revision을 비교해야 합니다. r이 durable해졌더라도 현재가 r+1이라면 최신 상태까지 저장되었다고 말할 수 없습니다. 동시 write를 경쟁시키기보다 r을 완료하고 가장 최신 revision을 다시 checkpoint하는 편이 순서를 설명하기 쉽습니다.
닫기는 단순히 화면을 없애는 이벤트가 아니라 마지막 입력 transaction과 durability barrier를 통과하는 상태 전이입니다. 확정되지 않은 composition과 expression이 남아 있다면 먼저 commit 또는 rollback되어야 하고, 저장이 성공하지 않았다면 이전 durable revision은 계속 유효해야 합니다. UI가 사라지는 것과 데이터 수명이 끝나는 것을 분리했습니다.
셀 입력, 여러 문자 체계, 수식 dependency와 cycle, type migration, 대량 행렬 연산, 병합 closure, 자산 graph, 연속 undo와 저장 중 새 revision을 반복해서 검수했습니다. 관계형 원본 상태와 화면 projection, durable snapshot이 같은 revision을 가리키는 것을 모두 확인했습니다. 직접 편집하면서 출판용 표의 원본을 관리하는 스프레드시트 엔진은 완전하게 구현되고 안정화되었습니다.
이전글
목록으로
다음글
저작권 고시
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에게 있습니다.