EDITORCLASSBLOGLOGINimg default descriptionimg default descriptionimg default description표현한다는 것의무한한 가능성,새로운 형태로
담아내다.
img default descriptionimg default descriptionimg default descriptionimg default descriptionimg default descriptionimg default descriptionimg default description새로운 형태의 콘텐츠Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.
옵클 에디터 개발기: 표 이미지를 만들기 위한 스프레드시트dev17옵클 에디터 개발기: 같은 데이터를 표와 보드, 캘린더로dev18옵클 에디터 개발기: 책을 웹에서 그대로 읽게 하는 퍼블리셔dev19옵클 에디터 개발기: 편집 화면을 그대로 웹에 게시하기dev20텍스트힙이란 무엇인가texthip1910111213...17 옵클 에디터 개발기: 표 이미지를 만들기 위한 스프레드시트레이아웃 에디터까지 만들고 나니 표를 다루는 방식도 다시 생각하게 되었습니다. 출판물에 들어가는 표는 대개 엑셀이나 구글 시트에서 만든 뒤 화면을 캡처하고, 그 이미지를 페이지에 올리는 식으로 처리됩니다. 결과만 보면 표 이미지 한 장이지만, 제작 과정에서는 셀 값과 행·열, 병합, 정렬, 글꼴, 테두리를 계속 수정해야 합니다. 이미지가 된 뒤에는 그 구조가 전부 사라집니다.나는 표 이미지를 그리는 기능부터 붙이지 않았습니다. 먼저 구글 시트처럼 값을 입력하고 범위를 선택하며, 행과 열을 조절하고, 타입이 있는 데이터를 관리할 수 있는 표 편집기를 따로 만들었습니다. 그다음 같은 데이터에서 출판용 이미지를 다시 조판하도록 했습니다. 표의 원본은 픽셀이 아니라 데이터베이스이고, 이미지는 언제든 다시 생성할 수 있는 결과물이 되어야 했습니다.이 구조를 잡으면서 일반적인 HTML ``만으로는 충분하지 않다는 것도 분명했습니다. 화면 편집에는 빠른 셀 접근과 키보드 동작이 필요하고, 저장에는 관계가 보존되는 문서 모델이 필요하며, 출력에는 브라우저 화면과 독립된 정확한 기하 계산이 필요합니다. 그래서 편집용 그리드, 로컬 문서 데이터베이스, 이미지 조판기를 서로 다른 계층으로 두고 표 에디터를 만들기 시작했습니다.
데이터가 먼저인 표표를 이미지 편집기의 오브젝트처럼만 다루면 셀이라는 개념이 금방 무너집니다. 글자 하나를 바꿀 때도 전체 이미지를 다시 만들어야 하고, 한 행을 추가하면 아래쪽 모든 요소의 좌표를 직접 밀어야 합니다. 정렬이나 필터는 픽셀 위에서 수행할 수 없고, 같은 데이터를 모바일용으로 재배치하는 일도 거의 불가능합니다.그래서 표의 권위적인 상태는 행과 열을 가진 문서 데이터로 두었습니다. 셀은 행·열 좌표와 원본 값, 값의 종류를 가지며, 열은 이름과 타입, 안정적인 식별자를 가집니다. 별도의 테이블에는 셀 서식과 병합 범위, 열 너비와 행 높이, 시트 순서와 뷰 설정을 저장했습니다. 화면의 DOM은 이 상태를 보여주고 입력을 받는 projection일 뿐입니다.새 문서는 기본적으로 충분한 크기의 빈 그리드를 가지지만, 실제로 살아 있는 범위는 데이터와 서식, 병합, 이름이나 타입이 지정된 열을 합친 범위로 계산했습니다. 사용자는 넓은 시트를 자유롭게 쓸 수 있으면서도 출력 엔진은 의미 없는 빈 셀까지 이미지로 만들지 않습니다. 편집 공간의 크기와 실제 콘텐츠 범위를 분리한 것입니다.첫 번째 열에는 문서의 레코드를 대표하는 타이틀 성질을 주었습니다. 날짜와 상태 열도 함께 만들어 새 표를 열자마자 레코드형 데이터를 사용할 수 있게 했습니다. 기존 문서를 불러왔는데 타이틀 열이 없다면 앞쪽에 새 열을 삽입하고 기존 데이터를 오른쪽으로 이동했습니다. 시트마다 타이틀 열이 정확히 하나는 남도록 보장해 이후 칸반과 캘린더에서도 같은 행을 안정적으로 설명할 수 있게 했습니다.Local SQLite document살아 있는 편집 문서는 브라우저 안의 SQLite로 만들었습니다. OPFS에 연결된 하나의 문서 파일 안에 시트, 셀, 열과 행, 병합, 서식, 뷰를 함께 보관했습니다. 셀을 바꿀 때마다 거대한 JSON 전체를 다시 직렬화하지 않고 필요한 row만 갱신할 수 있고, 범위 읽기와 정렬, 값 재계산도 데이터베이스 연산으로 처리할 수 있습니다.새 표를 만들 때는 이전 작업에 쓰이던 로컬 문서 파일을 먼저 비우고 새로운 시트를 생성했습니다. 같은 파일명을 재사용하는 구조에서 초기화 없이 연결만 하면 직전 프로젝트의 시트가 새 표에 남을 수 있기 때문입니다. 반대로 기존 프로젝트를 열 때는 저장된 SQLite 바이트를 현재 OPFS 문서로 import하고 스키마를 다시 확인한 뒤 활성 시트와 문서 제목을 복원했습니다.프로젝트 이식도 이 경계를 이용했습니다. 표 안의 이미지가 다른 장치에서 새 데이터베이스 ID를 받으면 셀에 저장된 자산 참조도 함께 바뀌어야 합니다. import 과정에서 old-to-new map을 보존하고, SQLite가 열린 뒤 셀 참조를 새 ID로 멱등 치환했습니다. 치환이 끝나기 전에는 고아 이미지 정리를 실행하지 않아 아직 옛 ID를 가진 정상 자산이 삭제되지 않게 했습니다.편집 데이터와 미디어 소유권도 분리했습니다. SQLite 스냅샷, 사용자가 넣은 이미지와 폰트는 표 프로젝트 전용 자산으로 저장하고, EPUB 부모 프로젝트의 폰트는 필요할 때 참조만 따라가도록 했습니다. 자식 편집기끼리 자산 저장소를 섞지 않았고, 포인터를 따라갈 때는 순환 참조를 막는 방문 집합과 탐색 상한을 두었습니다. 표 하나를 옮기거나 지워도 다른 편집기의 자산이 같이 손상되지 않습니다.Property column열은 단순한 A, B, C 좌표보다 더 많은 의미를 갖게 했습니다. 타이틀, 텍스트, 숫자, 날짜, 단일 선택, 다중 선택, 체크박스, URL 타입을 지정할 수 있고, 타입을 정하지 않은 새 열은 첫 입력을 보고 숫자와 날짜, 텍스트를 추론합니다. 다만 이미 데이터가 있는 열에는 첫 셀만 보고 타입을 강제하지 않았습니다. 자동 추론이 기존 값과 충돌하지 않도록 빈 열에서만 실행했습니다.열 타입 변경은 metadata 한 칸을 바꾸는 것으로 끝내지 않았습니다. 숫자 열로 바꾸면 쉼표와 공백이 들어간 숫자를 실제 number로 정규화하고, 날짜 열은 여러 구분자로 입력된 값을 `YYYY-MM-DD`로 맞췄습니다. 체크박스는 참과 거짓으로 해석할 수 있는 입력을 정규화했습니다. 변환할 수 없는 값은 새 타입에 맞지 않는 상태로 조용히 남겨두지 않고 명확하게 정리했습니다.선택과 다중 선택 열은 옵션 목록과 색을 열의 format 데이터로 보관했습니다. 새로운 라벨을 입력하면 파스텔 팔레트에서 색을 할당해 옵션으로 등록하고, 옵션 이름을 바꾸거나 삭제할 때는 그 값을 쓰는 모든 셀을 같이 갱신했습니다. 단일 선택은 하나의 라벨, 다중 선택은 쉼표로 정규화된 여러 라벨을 저장하며 화면에서는 같은 색의 chip으로 표시했습니다.새 행이 실제 데이터 행으로 활성화되면 날짜 열의 빈칸은 오늘 날짜로, 선택 열의 빈칸은 첫 기본 옵션으로 채웠습니다. 열을 나중에 날짜나 선택 타입으로 바꿨을 때도 현재 활성 범위 전체를 다시 훑어 기본값을 맞췄습니다. 어떤 입력 경로로 행이 생겼는지에 따라 기본값이 빠지는 일이 없도록 셀 편집, 수식줄, 붙여넣기와 카드 생성이 같은 규칙을 사용하게 했습니다.Cell editing geometry그리드의 기본 좌표는 익숙한 A1 체계를 사용했습니다. 열 번호를 26진법에 가까운 문자 체계로 변환해 A부터 Z, AA 이후까지 만들고, 단일 셀과 사각 선택 범위를 같은 주소로 표현했습니다. 주소 입력으로 셀이나 범위를 바로 선택할 수 있고, 행·열 헤더를 누르면 해당 축 전체가 선택됩니다.범위 선택은 단순히 시작점과 끝점 사이의 셀에 class를 붙이는 방식으로 끝내지 않았습니다. 병합 셀이 선택과 일부만 겹치면 병합 전체를 포함할 때까지 사각형을 반복 확장했습니다. 실제 강조 영역은 렌더된 셀의 `getBoundingClientRect()` 합집합으로 계산해 하나의 overlay로 그렸습니다. 필터로 숨은 행이나 병합 때문에 DOM이 없는 셀도 자동으로 계산에서 제외됩니다.드래그가 그리드 가장자리에 닿으면 최신 포인터 좌표를 기준으로 `requestAnimationFrame` 자동 스크롤을 돌렸습니다. 선택 overlay와 렌더 영역 표시도 스크롤 콘텐츠 좌표로 변환해 셀과 함께 이동합니다. 열 너비나 행 높이를 드래그하는 동안에는 데이터베이스를 매 순간 쓰지 않고 DOM geometry만 갱신하고, mouseup에서 최종 크기를 하나의 변경으로 저장했습니다.키보드 동작은 스프레드시트의 리듬을 그대로 따랐습니다. 방향키와 Tab, Enter로 활성 셀을 이동하고, Shift는 범위를 확장하며, Cmd나 Ctrl과 방향키를 함께 누르면 현재 데이터 블록의 경계로 이동합니다. 아무 문자를 입력하면 기존 값을 지우고 바로 편집을 시작하지만, 더블클릭이나 F2로 연 편집에서는 방향키가 텍스트 caret을 움직입니다. 같은 contenteditable이라도 진입 방식에 따라 셀 이동과 문자열 편집을 구분했습니다.IME와 수식 입력한글 입력은 contenteditable 그리드에서 별도로 다뤘습니다. 조합이 시작될 때 문자열을 직접 대입하면 첫 자모가 유실되거나 caret이 이동할 수 있으므로 브라우저의 IME composition을 셀에 그대로 맡겼습니다. 조합 중 Enter는 셀 커밋이 아니라 글자 조합 확정일 수 있어 무시하고, 조합이 끝난 뒤에만 저장과 이동을 실행했습니다.열 이름 입력도 같은 문제를 가집니다. 헤더의 input에서 발생한 composition 이벤트가 그리드까지 버블링하면 활성 셀의 덮어쓰기 편집이 시작되며 포커스를 빼앗습니다. input, select, textarea에서 올라온 이벤트는 셀 편집 진입 경로에서 제외했습니다. 헤더 이름은 blur 또는 Enter에서 커밋하고 Escape에서는 데이터베이스 값을 다시 표시하도록 했습니다.수식줄에는 활성 셀의 계산 결과가 아니라 원본 값을 표시했습니다. `=`로 시작한 식은 그대로 편집할 수 있고, 셀 화면에는 재계산된 결과가 들어갑니다. 수식줄에서 입력하는 동안에는 활성 셀에 임시 미리보기를 보여주되 Enter를 누른 순간에만 타입 검사, 값 저장, 재계산과 렌더를 한 번에 실행했습니다. 포커스만 잃고 떠난 경우에는 원본 값으로 되돌렸습니다.셀 안에서 Shift+Enter는 줄바꿈으로 남기고 Enter는 값만 확정했습니다. 타이핑으로 진입한 셀에서 방향키를 누르면 먼저 현재 값을 커밋한 뒤 옆 셀로 이동하고, F2로 진입한 셀에서는 caret 이동을 유지했습니다. 편집 모드 하나에 모든 키 규칙을 밀어 넣지 않고 사용자가 어떤 의도로 편집을 시작했는지를 상태로 보존한 결과입니다.Clipboard and fill복사와 붙여넣기는 TSV를 공통 교환 형식으로 사용했습니다. 선택 범위를 행과 열의 2차원 배열로 만든 뒤 탭과 줄바꿈으로 직렬화하면 구글 시트, 엑셀, 일반 텍스트 편집기와 자연스럽게 오갈 수 있습니다. 수식 셀은 계산 결과가 아니라 원본 식을 복사해 다른 셀에서도 다시 계산되게 했습니다.붙여넣기는 활성 셀을 왼쪽 위 기준으로 삼아 그리드 범위 안에 배치했습니다. 이때 열 metadata를 먼저 한 번 읽고 각 셀을 타입 규칙에 맞춰 검사했습니다. 숫자나 날짜는 코어션하고 선택 열의 새 라벨은 옵션 목록에 모아 등록했습니다. 타입이 맞지 않는 셀은 전체 붙여넣기를 실패시키지 않고 해당 셀만 제외한 뒤 제외 개수를 알려주었습니다.자동 채우기는 시작 셀에서 포인터가 더 멀리 이동한 축을 주축으로 정해 가로나 세로 한 줄을 확장했습니다. 숫자는 offset만큼 증가시키고, 날짜는 하루 단위로 이동하며, 끝에 숫자가 붙은 텍스트는 접두어를 유지한 채 번호만 늘렸습니다. 일반 문자열과 수식은 그대로 복사했습니다. 채우기 전체는 하나의 범위 쓰기와 하나의 undo 기록으로 처리했습니다.CSV와 TSV 파일은 따옴표 안의 구분자와 줄바꿈, 이중 따옴표 escape, BOM까지 처리하는 parser로 읽었습니다. XLSX는 바이너리 문서를 서버의 문서 판독 경로에서 TSV로 변환한 뒤 같은 matrix import를 사용했습니다. 가져온 데이터가 현재 시트보다 크면 행과 열을 상한 안에서 확장하고, 셀과 서식, 병합을 하나의 변경 단위로 교체했습니다.서식도 데이터로셀 서식은 DOM의 inline style만 바꾸지 않고 별도의 스타일 row로 저장했습니다. 굵게, 기울임, 밑줄, 가로·세로 정렬, 숫자 형식, 채움색과 글자색, 글꼴, 크기와 굵기, 줄바꿈, 네 방향 패딩, 행간과 자간, 네 변의 테두리를 하나의 셀 스타일로 관리했습니다. 모든 값이 기본값으로 돌아오면 빈 스타일 row를 삭제해 문서가 불필요하게 커지지 않게 했습니다.숫자 형식은 원본 number를 바꾸지 않고 표시 단계에서 적용했습니다. 일반 숫자와 원화, 백분율은 각기 다른 기본 소수 자릿수와 천 단위 묶음을 가지며, 수식 결과가 숫자인 경우에도 같은 형식을 사용합니다. 화면 그리드와 SVG 출력은 같은 값 포맷 함수를 호출해 한쪽만 다른 문자열을 보여주지 않습니다.테두리는 셀 하나의 사각형이 아니라 위·오른쪽·아래·왼쪽 변의 상태로 저장했습니다. 전체, 바깥쪽, 특정 방향 같은 preset을 선택 범위에 적용할 때 각 셀의 위치를 보고 필요한 변만 켰습니다. 굵기와 색은 별도로 바꿀 수 있고, 병합 셀은 앵커의 큰 경계를 기준으로 표시했습니다.화면 그리드는 빠른 입력을 위해 언제나 한 줄 높이를 유지하고 긴 문자열을 셀 안에서 잘랐습니다. 줄바꿈과 자동 행 높이는 아래쪽 출판 미리보기에서만 계산했습니다. 입력 DOM이 콘텐츠에 따라 출렁이지 않으면서도 최종 이미지에서는 패딩과 행간, 여러 줄 조판이 정확히 반영됩니다. 편집의 ergonomics와 출력의 typography를 같은 규칙 아래에서 다른 방식으로 표현했습니다.이미지와 폰트 자산셀에는 텍스트뿐 아니라 이미지도 넣을 수 있게 했습니다. JPEG, PNG, WebP를 드롭하면 먼저 브라우저에서 디코드하고 긴 변이 지나치게 큰 원본은 캔버스로 축소했습니다. PNG의 투명도는 유지하고 나머지는 적절한 JPEG 품질로 다시 인코딩한 뒤 표 프로젝트 전용 blob으로 저장했습니다.셀 값에는 바이너리 자체가 아니라 blob을 가리키는 토큰만 넣었습니다. 그리드에서는 object URL로 작은 미리보기를 만들고, 출력 SVG에서는 data URL로 인라인했습니다. 이미지의 종횡비와 현재 열 너비를 이용해 필요한 행 높이를 계산하되 세로로 긴 이미지가 표 전체를 과도하게 늘리지 않도록 상한을 두었습니다. 병합 영역에 드롭한 경우에는 실제 앵커 셀로 좌표를 보정했습니다.이미지 삭제와 undo가 함께 작동하려면 고아 자산을 너무 일찍 지우면 안 됩니다. 편집 세션 중에는 셀 참조만 바꾸고 blob은 남겨 두었습니다. 프로젝트를 새로 열었거나 iframe을 닫아 undo 이력이 더 이상 필요하지 않은 경계에서 모든 시트의 이미지 참조를 조회하고, 현재 표 프로젝트가 소유한 목록 안에서만 고아 blob을 정리했습니다.폰트도 파일명만 CSS에 적지 않았습니다. TTF, OTF, WOFF의 내부 name table을 읽어 실제 family를 찾고 `FontFace`를 주입했습니다. 표 프로젝트에 드롭한 폰트와 부모 EPUB이 가진 폰트를 함께 사용할 수 있지만 저장 소유권은 구분했습니다. 폰트 load가 끝난 뒤 렌더를 다시 예약해 fallback font로 측정된 미리보기가 최종 결과로 남지 않게 했습니다.History transaction표 편집의 undo는 셀 값만 되돌려서는 부족했습니다. 열 타입을 바꾸면 기존 값이 변환되고 선택 옵션이 추가될 수 있으며, 행을 삽입하면 병합과 열·행 좌표가 함께 이동합니다. 그래서 변경 전후에 셀, 행·열 크기, 셀 스타일, 병합, 열 metadata와 뷰 정의를 통합 스냅샷으로 만들었습니다.모든 변경은 하나의 promise chain에 직렬화했습니다. 빠르게 입력하거나 크기를 바꾸는 동안 두 액션이 동시에 before snapshot을 읽으면 undo 순서가 뒤섞일 수 있습니다. 앞 액션이 DB 쓰기와 재계산, DOM 갱신을 끝낸 뒤 다음 액션이 시작되게 하고, 실제 전후 상태가 같은 경우에는 이력과 자동저장을 만들지 않았습니다.undo와 redo도 같은 큐에 들어갑니다. 선택 상태까지 함께 기록해 복원 후 사용자가 보던 셀과 범위가 돌아오고, 열의 안정 ID도 원래 값으로 복구되어 칸반과 캘린더의 열 참조가 끊기지 않습니다. 복원 뒤에는 그리드 geometry, 계산값, 스타일, 헤더 metadata와 아래쪽 렌더를 순서대로 다시 만들었습니다.자동저장과 즉시 렌더는 분리했습니다. 화면 이미지는 다음 animation frame에 한 번만 갱신해 입력 결과를 바로 보여주고, SQLite 전체 스냅샷은 짧은 debounce 뒤에 만들었습니다. 타이핑마다 큰 DB blob을 다시 쓰지 않으면서도 사용자가 보는 표는 지연 없이 따라옵니다.저장이 끝나는 시점프로젝트 저장은 세 층으로 구성했습니다. 먼저 살아 있는 SQLite 문서를 바이트로 export해 DB blob을 갱신하고, 다음으로 현재 시트와 렌더 영역, zoom과 pan, 헤더·배경·격자선 설정을 작은 시각 상태로 저장했습니다. 마지막 manifest에는 두 스냅샷과 이미지·폰트 목록, 시트별 마지막 활성 뷰를 연결했습니다.저장이 시작되면 먼저 액션 큐가 완전히 비기를 기다렸습니다. 셀 blur에서 막 enqueue된 커밋이 DB export보다 늦게 실행되면 화면에는 보였던 마지막 글자가 저장 파일에서는 빠질 수 있습니다. 진행 중인 저장이 있을 때는 두 번째 저장을 병렬로 시작하지 않고 재실행 플래그만 남겨 첫 저장 종료 직후 최신 상태를 한 번 더 확정했습니다.iframe을 닫는 경계는 더 엄격하게 처리했습니다. 현재 focus를 강제로 blur해 열 이름과 셀 편집을 커밋하고, 액션 큐를 기다린 뒤 고아 이미지를 정리하고 즉시 저장했습니다. 이미 자동저장이 진행 중이라면 저장 lock과 재실행 플래그가 모두 사라질 때까지 제한 시간 안에서 기다린 뒤 부모 에디터에 완료 응답을 보냈습니다. 부모는 이 응답을 받은 뒤에만 iframe을 제거합니다.셀 입력, 한글 조합, 수식, 타입 변경과 코어션, 대량 붙여넣기, 채우기, 병합과 필터, 행·열 삽입, 이미지와 폰트, 연속 undo·redo, 자동저장 중 닫기, 프로젝트 재열기와 이식까지 반복해서 검수했습니다. 데이터베이스의 원본 상태와 화면 그리드, 저장된 프로젝트가 같은 값을 유지하는 것을 모두 확인했습니다. 구글 시트처럼 직접 편집하면서 출판용 표의 원본을 관리하는 스프레드시트 엔진은 완전하게 구현되고 안정화되었습니다.
이전글목록으로다음글
저작권 고시Copyright Notice본 웹사이트의 모든 디자인 결과물 및 영상에 대한 저작권은 Abstract Cloud에 있으며, 저작권법 및 관련 법령에 의해 보호받습니다. 웹, 영상, 본문, 표지, 내지 디자인을 포함한 모든 콘텐츠는 저작권자의 자산으로, 사전 동의 없이 무단 복제, 배포, 2차 저작물 제작, 온라인 공유 등을 금지합니다. 이를 위반할 시, 저작권법에 따라 민형사상 책임을 질 수 있습니다. 정당한 구매와 저작권 보호는 창작자의 권리를 지키며, 더 나은 작품으로 보답할 힘이 됩니다.저작권자: Abstract Cloud | 대표자: 배창규(uragen)
© Abstract Cloud. All Rights Reserved.
HOMEFAQ이용 약관개인정보 이용방침help@opkle.app
010-2747-3403
상호 :추상적 형상 디자인(Abstract cloud)  |  대표자 :배창규사업자등록번호 :249-74-00533통신판매업 신고번호 :2025-의정부송산-0634주소 :경기도 의정부시 부용로 49, 108동 402호웹의 모든 콘텐츠, 디자인, 소스 코드에 대한
저작권은 Opkle에게 있습니다.
img default description