마크다운 기호를 본문 밖으로 밀어냈다 (Verso 제작기)
글을 쓸 때 쓰는 앱과 코드를 만질 때 쓰는 앱이 따로였다. 화면 한쪽에 율리시스를 띄워 두고 다른 쪽에 서브라임을 띄워 놓고, 두 창을 오가며 하루를 보내는 게 몇 년째 당연한 일이었다. 그런데 어느 날 그게 갑자기 거슬렸다. 두 앱이 하는 일이 결국 “글자를 친다” 하나인데 왜 둘이어야 하나 싶었던 거다.
그래서 필자가 원하는 마크다운 에디터의 조건을 적어 봤다. 넷이 나왔다. 파일을 복제하지 말고 있는 자리에서 열고 그 자리에 저장할 것, 확장자에 맞는 문법 강조가 붙을 것, 컬럼 선택이 될 것, 가벼울 것.
적어 놓고 보니 이걸 다 하는 물건이 없었다.
마크다운 에디터와 코드 편집기, 각각 반씩만 됐다
율리시스는 글 쓰는 감각이 좋다. 대신 파일을 제 라이브러리로 복제해 간다. 내 디스크에 있던 그 파일에 바로 저장이 안 되고, 가져오기와 내보내기를 거쳐야 한다. 글이 원드라이브와 개발 폴더와 외장 SSD에 흩어져 있는 입장에서는 이게 꽤 거슬린다.
서브라임은 편집 기능이 훌륭하다. 컬럼 선택은 한번 손에 익으면 못 놓는다. 그런데 마크다운을 열면 여전히 #과 별표가 본문에 그대로 섞여 있다. 미리보기 창을 옆에 띄울 수는 있지만, 그건 결과를 따로 보여 주는 것이지 쓰는 자리가 바뀌는 건 아니다.
둘을 합치면 답이 나오는데 합쳐 놓은 물건이 없었다. 그래서 만들기로 했다.
일렉트론만은 안 쓰기로 했다
처음 정한 게 이거였다. 요즘 데스크톱 앱은 일렉트론이 기본값이다시피 하고 자료도 제일 많은데, 굳이 피했다.
이유는 맥북이 타이핑을 놓치기 시작했을 때 2주를 태우고 찾아낸 범인이 일렉트론 렌더러였기 때문이다. 창에 내용이 쌓일수록 무거워지고, 그 렌더가 끝날 때까지 내 키 입력이 줄을 서서 기다린다. 타이핑이 기능의 전부인 앱을 만들면서 그 함정에 제 발로 들어갈 이유가 없었다.
그래서 Tauri 2와 CodeMirror 6으로 갔다. Tauri는 크로미움을 통째로 안고 가지 않고 맥에 이미 있는 WKWebView를 빌려 쓴다. 껍데기는 러스트고 안은 웹인데, 크롬 한 벌을 같이 설치하지 않는다는 점이 결정적이었다.
마크다운 기호를 본문 밖 왼쪽 열로 밀어냈다
핵심은 여기다. 흔한 마크다운 WYSIWYG은 기호를 화면에서 지우고 결과만 보여 준다. 그러면 지금 이 줄이 제목인지 본문인지, 커서가 기호 앞인지 뒤인지 알 수 없어진다.
그래서 지우지 않고 옮겼다. 제목의 #은 본문 왼쪽 바깥에 따로 마련한 좁은 열에 단계 표시로 남는다. 본문 흐름에서는 빠져 있는데 눈으로는 확인이 된다.

#, ##, ###이 흐릿하게 서 있다. 본문 안에는 기호가 없고 제목은 제목 크기로, 인용은 왼쪽에 선을 그어, 코드 블록은 문법 강조가 붙은 채로 보인다. 저장되는 파일은 그냥 마크다운이다.기호를 직접 칠 일도 없앴다. 제목은 ⌘1부터 ⌘6, 본문으로 되돌리기는 ⌘0, 굵게는 ⌘B다. 처음에는 #을 치고 스페이스를 누르면 자동으로 제목이 되게도 만들어 뒀는데, 나중에 걷어냈다. 제목을 만드는 길이 둘이 되면 어느 쪽이 원칙인지가 흐려진다.
이름도 여기서 나왔다. Verso는 책을 펼쳤을 때 왼쪽 페이지를 뜻한다. 마크업을 왼쪽 여백으로 밀어낸 설계와 뜻이 맞았다. 원래 쓰던 이름은 Pilcrow(¶)였는데 같은 종류의 앱이 이미 쓰고 있어서 접었다.
한글이 자모로 깨졌다
여기서부터가 진짜다.
라이브 프리뷰는 글자를 칠 때마다 화면을 다시 그린다. 그런데 한글은 자음과 모음을 조합해서 한 글자를 만드는 중간 상태가 있다. 조합이 끝나기 전에 화면을 다시 그리면 WKWebView가 조합을 통째로 버린다. 한을 치려는데 ㅎ, ㅏ, ㄴ이 따로 떨어져 나오는 거다.
고치는 방법 자체는 한 줄이다. 화면을 갱신할 때 조합 중인지를 먼저 보고, 조합 중이면 위치만 밀어 두고 그냥 빠져나온다. 문제는 이걸 모르면 원인이 한글 폰트나 입력기에 있다고 착각하며 한참을 헤맨다는 것이다. 영어로 테스트하면 절대 안 나온다.

되돌리기 이야기도 해야겠다. 편집기 하나로 문서 여러 개를 번갈아 여는 구조인데, 되돌리기 기록을 문서마다 비우지 않았다. 그랬더니 A 문서를 열고 ⌘Z를 계속 누르면 직전에 보던 B 문서의 글이 A 파일 안으로 들어왔다. 데이터가 섞이는 버그라 발견했을 때 등이 서늘했다.
고정 높이 편집기에 overflow:auto를 안 줬더니 스크롤러가 문서 전체 높이인 10만 픽셀로 자라고, 거기에 화면 높이를 계속 더하는 되먹임이 걸려 한 번에 1,718회를 다시 계산한 일도 있다. 줄 간격을 margin으로 줬더니 커서 좌표가 어긋난 것도 같은 종류다. CodeMirror는 줄 높이를 잴 때 margin을 세지 않는다.
네 가지 결정은 도로 뒤집었다
처음에 옳다고 생각한 것 중 넷을 나중에 갈아엎었다.
왼쪽 사이드바를 폴더 브라우저로 만들었다가 열어본 문서 목록으로 바꿨다. 내 글이 한 폴더에 모여 있지 않아서다. 힌트는 스스로가 먼저 줬는데, 만들어 놓고 제일 먼저 원한 기능이 “목록에서 지우기”였다. 폴더를 그대로 비추는 창이라면 나올 수 없는 요구다. 폴더의 거울이 아니라 내가 고른 목록을 기대하고 있었던 거다.
문서를 전환할 때 자동 저장하던 것도 뺐다. 편집 상태가 문서가 아니라 화면 쪽에 붙어 있어서, 다른 문서로 넘어가면 ‘편집 중’이라는 사실 자체가 사라졌다. 자동 저장과 문서별 임시 보관은 같이 갈 수 없어서 자동 저장을 접었다.
⌘W도 바꿨다. 원래 창이 닫히는 게 맥 관행인데, 문서 하나를 덜어냈다고 창이 사라질 이유가 없다 싶어 문서 닫기로 돌렸다.
검사를 늦게 만들고 그 값을 치렀다
만드는 동안에는 방금 만든 것만 확인하고 넘어갔다. 구조가 계속 바뀌는 중이라 검사 장치를 지어 봐야 같이 버려진다고 생각했다. 그 판단 자체는 틀리지 않았다고 본다.
문제는 끝내는 시점을 못 잡은 것이다. 기능을 하나 고칠 때마다 다른 기능이 깨지기 시작했고, 두 번 연속으로 필자가 아니라 쓰는 쪽에서 먼저 발견했다. 그때가 검사를 만들었어야 할 자리였다.
뒤늦게 ./selftest.sh 한 벌로 묶었다. 타입 검사, 단위 98개, 러스트 빌드, 그리고 실제 DOM과 실제 핸들러를 거치는 UI 시나리오 108개가 명령 하나에 전부 돈다. 넣고 나서 처음 돌렸을 때 직전 수정에서 낸 버그가 바로 잡혔다. 값을 치르고 배운 셈이다.

이 속도가 가능한 건 손이 빨라서가 아니다. 판단과 문장은 내가 하고 타이핑과 검증은 AI에게 넘겼기 때문이다. 다만 위에 적은 함정들은 AI가 알아서 피해 준 게 아니라 하나씩 밟고 나서 적어 둔 것들이다. 밟는 속도만 빨라졌지 안 밟게 되지는 않는다.
지금은 여기까지 왔다
1.1.0까지 왔고 한국어·영어·일본어·중국어로 쓸 수 있다. 번역을 코드가 아니라 JSON 파일 하나에 두어서, 설정 폴더에 언어 팩을 떨어뜨리면 목록에 뜬다. 빌드도 릴리스도 필요 없다.
Finder에서 오른쪽 클릭으로 열리게 하는 데도 한참 걸렸다. 「다음으로 열기」에서 앱이 회색으로 비활성이면 서명 문제가 아니라 선언 문제다. 앱이 “나는 이런 종류의 파일을 연다”고 밝혀야 목록에 올라온다. 그런데 선언만으로는 절반이다. Finder는 파일을 명령줄이 아니라 애플 이벤트로 건네주는데, 앱이 찬 상태에서 켜지면 그 이벤트가 화면이 만들어지기도 전에 도착한다. 러스트 쪽에 받아 세워 두고 화면이 준비되면 가져가게 만들어서야 실제로 열렸다.
남은 건 내 손을 떠난 것들이다. 깃허브에 공개할 라이선스를 아직 못 정했고, 앱스토어는 서명 인증서가 하나도 없어서 개발자 프로그램 등록부터 해야 한다. 샌드박스에 들어가면 지금 방식으로는 문서 목록이 다음 실행에 전부 “열 수 없음”이 되는 문제도 남아 있다.
검수 도구를 만들었을 때와 같은 이야기를 또 하게 되는데, 이번에도 계획해서 만든 게 아니다. 같은 짜증을 또 겪고 나니 그 자리에 도구가 하나 생겨 있었다. 순서는 늘 그랬다.