← 아티클 목록

개발일지WhoAmI — 공부 기록을 발행 가능한 형태로 좁힌 파이프라인 →

볼트는 저장소 밖에 두고 관리자 큐만 좁혔다

옵시디언 볼트 전체를 저장소와 동기화하지 않고 관리자의 반입 대기열을 30-publish로 좁혔다. CLI는 임의 노트와 --publish를 받는 별도 진입점이며, 두 경로가 공통 변환·민감정보 검사를 쓰는 범위와 한계를 정리했다.

목차 3

2026-08-09 업데이트 — Git의 symlink·submodule 저장 방식을 바로잡고, 관리자 30-publish/ 큐와 CLI의 입력·공개 범위를 구분했다. 두 경로가 공유하는 것은 변환·검사 함수다.

옵시디언 볼트에는 공개할 수 없는 것이 섞여 있다. 정리되지 않은 캡처, 시험 대비 메모, 남의 자료를 그대로 옮겨 적어 둔 것. 그 볼트에서 글을 꺼내 사이트에 올리는 일을 반복하려면 무엇을 꺼낼지 매번 판단해야 하는데, 판단은 반복되면 느슨해진다.

먼저 하지 않기로 정한 것이 있다. 볼트를 이 저장소 안에 두지 않는다. 일반 하위 폴더로 추가해 커밋하면 노트 내용이 이 저장소의 blob으로 남는다. 다만 symlink와 submodule은 동작이 다르다. Git 데이터 모델은 symlink를 mode 120000의 blob으로 기록하고, git fast-import 데이터 형식은 그 blob의 내용이 링크 대상 경로라고 명시한다. submodule은 별도 저장소이고, 상위 저장소에는 그 저장소의 특정 커밋을 가리키는 gitlink와 설정한 URL이 남는다.

그러므로 symlink나 submodule을 두면 사적인 노트 내용이 곧바로 이 저장소 이력에 복사된다는 과거 설명은 틀렸다. 그래도 사용하지 않기로 한 결정은 유지한다. symlink는 로컬 경로를 드러내고 운영체제와 도구에 따라 취급이 달라지며, submodule은 저장소와 접근 권한을 하나 더 관리해야 한다. 실제 노트가 일반 파일로 잘못 커밋되면 이력 재작성은 가능하지만 이미 생긴 복제본과 캐시까지 회수됐다고 보장할 수 없다.

관리자 큐는 30-publish로 좁혔다

볼트에 폴더 규약을 뒀다. 캡처는 00-inbox/, 개념 정리는 10-notes/, 실습 기록은 20-labs/에 쌓고, 관리자에서 반입을 기다리는 사본은 30-publish/에 둔다. 관리자의 큐 목록과 최종 반입은 이 폴더만 대상으로 하며, 파일명은 basename()으로 다시 제한한다.

그렇다고 다른 폴더를 도구가 전혀 읽지 않는 것은 아니다. 관리자는 볼트의 Markdown 노트를 목록화하고, 사용자가 고른 원본을 읽어 정리한 사본을 30-publish/에 만든다. 공통 반입 함수도 첨부 파일을 찾으려고 볼트의 파일명 인덱스를 만든다. 자동으로 게시 후보가 되는 경계가 30-publish/라는 것과 로컬 도구가 다른 폴더를 읽지 않는다는 것은 다른 주장이다.

CLI는 더 넓다. scripts/import-note.mjs는 첫 번째 인자로 받은 노트 경로를 그대로 공통 반입 함수에 넘기므로 30-publish/ 밖의 파일도 직접 입력할 수 있다. 현재 코드에서 폴더 규약은 관리자 기본 흐름의 경계이고, CLI까지 강제하는 저장소 전체의 보안 경계는 아니다.

관리자 흐름에서는 「무엇을 공개 후보로 둘지」를 노트를 대기열로 옮기거나 화면에서 사본을 만들 때 판단한다. CLI를 쓸 때는 어떤 경로를 인자로 넘겼는지가 그 판단이므로, 실행하는 사람이 같은 규칙을 지켜야 한다.

입력 경로는 둘이지만 변환과 검사는 scripts/lib/import-core.mjs에 모여 있다. 관리자 큐의 사본과 CLI가 직접 받은 노트는 모두 이 함수를 거쳐 민감정보를 검사한다. 위키링크는 공개된 글이면 실제 경로로 바뀌고 아직 없는 노트면 링크를 지운 평문이 된다. %%주석%%은 제거되고 몇 개를 지웠는지 보고한다. 본문 태그 줄은 frontmatter의 태그로 옮겨진다.

여기서 한 가지를 일부러 건드리지 않는다. 코드 블록 안이다. 인젝션이나 XSS 실습 노트에는 [[ 나 #태그 가 코드 예시로 등장하는 게 정상이라, 변환기는 코드 영역을 먼저 자리표시자로 빼내고 모든 치환이 끝난 뒤에 되돌린다. 민감정보 스캐너도 문법 잔여물을 볼 때는 코드 영역을 같은 길이의 공백으로 덮어 오탐을 피한다. 행과 열 번호는 유지되므로 어디서 걸렸는지는 그대로 나온다.

양방향 동기화와 자동 발행을 버렸다

선택한 원본 노트를 고치지 않는다. 도구는 원본을 읽어 발행 후보 사본을 만들고, 관리자 대기열을 쓸 때는 볼트 안 30-publish/에 그 사본을 생성하거나 수정한다. 사이트에서 글을 고쳐도 원본 노트로 돌아가지 않는다. 양쪽을 맞추는 기능은 만들면 편하지만, 어느 쪽이 원본인지가 흐려지는 순간 충돌을 사람이 매번 판단해야 한다. 단방향이면 그 질문 자체가 생기지 않는다.

관리자에서 새 노트를 곧바로 공개하는 흐름은 두지 않았다. 관리자 큐는 공통 함수에 publish: false를 넘긴다. 새 글은 draft: true가 되고, 기존 글을 다시 가져오면 그 글의 공개·초안 상태를 보존한다. CLI도 기본 동작은 같지만, 사용자가 --publish를 명시하면 draft: false로 쓸 수 있다. 따라서 “모든 가져오기가 항상 초안”은 코드가 보장하는 규칙이 아니다.

공부 노트와 아티클은 품질 기준이 다르므로 보통은 초안에서 검토한다. --publish는 그 검토 단계를 건너뛸 수 있는 명시적 우회로이고 자동 발행은 아니다. 이 옵션을 썼는지까지 포함해 마지막 판단은 실행한 사람이 책임져야 한다.

한글 파일명에서 slug를 자동으로 정하는 것도 하지 않는다. 흔히 쓰는 변환기는 한글을 그냥 버려서 AI 활용 정리.md가 ai가 된다. 그런 URL을 나중에 바꾸면 리다이렉트가 없는 한 기존 링크와 검색 신호가 새 주소로 이어지지 않는다. CLI는 파일명에 비ASCII 문자가 섞였고 frontmatter에도 slug가 없으면 --slug를 요구한다. ASCII 파일명은 자동 변환할 수 있다. 관리자 화면은 용어 사전으로 뜻을 옮긴 후보를 채워 주되 값은 언제든 고칠 수 있게 뒀다.

화면과 명령이 같은 함수를 부른다

같은 작업을 브라우저에서도 하고 터미널에서도 한다. 변환·첨부 복사·민감정보 스캔·파일 쓰기는 scripts/lib/import-core.mjs에 두고 관리자와 CLI가 그 함수를 부른다. 같은 입력과 옵션이면 이 공통 단계의 결과는 같다.

두 진입점의 경계까지 같은 것은 아니다. 관리자는 30-publish/와 publish: false를 강제하고, CLI는 임의 노트 경로와 --publish를 허용한다. 이 구조를 도입한 뒤 두 경로의 반입은 공통 함수를 거치지만, 현재 글 전부가 과거에도 이 통로를 지났다고 입증할 반입 기록은 없다.

아직 처리하지 못하는 것은 남아 있다. 수식과 Mermaid 다이어그램은 별도 변환을 하지 않고, 반입 단계에서는 이미지가 400KB를 넘으면 경고만 한다. 공통 변환의 문제는 import-core.mjs에서 고치고, 관리자 큐와 CLI의 입력·공개 경계는 각 진입점에서 따로 확인해야 한다.

함께 보면 좋은 글

이 기록과 태그가 겹치는 아티클입니다.