어두운 나무 책상 위에 노트북과 작은 은색 미니 컴퓨터, 가죽 수첩과 펜이 놓여 있다. 노트북 화면에는 파랑·주황·빨강 태그가 달린 위키 페이지가 떠 있고, 화면 위 하늘색 자물쇠로 문서·사진·대화·전구 아이콘 넷에서 뻗은 선이 모인다. 미니 컴퓨터와 노트북 사이는 하늘색 점선으로 이어진다.

개인 위키를 하나 만들었습니다. 기획을 시작한 날부터 마지막 단계를 배포하기까지 사흘 남짓 걸렸습니다.

그 사이 제가 한 일은 방향을 정하고, 단계마다 진행을 승인하고, 결과를 검토하는 것이었습니다. 초기 설계와 코드는 AI 에이전트들의 몫이었습니다.

만든 이유

공개해도 되는 글은 이 블로그에 씁니다. 그런데 개인 민감 정보는 이렇게 공개된 곳에 둘 수 없습니다. 그런 정보를 따로 담아 두고 필요할 때 꺼내 볼 곳이 필요했습니다. 그래서 위키에는 공개 페이지가 하나도 없고, 검색엔진에 노출되지 않게 막아 두었습니다. 블로그가 바깥을 향한 글이라면 위키는 안쪽을 향한 기록입니다.

나무 책상이 가운데 결을 따라 둘로 나뉜다. 햇빛이 드는 왼쪽에는 바다 사진이 가득한 블로그 화면의 노트북과 사진·잡지가 펼쳐져 있고, 등불이 비추는 어두운 오른쪽에는 은색 미니 컴퓨터 위로 하늘색 방패 속 자물쇠가 떠 있으며 가죽 수첩에서 미니 컴퓨터로 하늘색 선이 이어진다.

처음에는 NAS를 새로 사서 거기에 올릴 생각이었습니다. 후보와 가격을 알아보다가 하드디스크 값이 많이 올라 있어서 보류했습니다. 대신 이미 있던 장비로 가기로 했습니다. 마침 AI 에이전트의 기억과 도구를 모아 둔 저장소(Brain이라고 부릅니다)의 본체가 맥스튜디오로 옮겨 가던 참이라, 맥미니를 위키 서버로 쓰기로 했습니다. 서버는 나중에 바꿀 수 있으니 먼저 만들어 보자는 쪽이었습니다.

기성 위키를 고쳐 쓰는 길도 있었습니다. 기술 스택 초안을 AI에게 받아 함께 검토한 결론은 직접 만들자는 쪽이었습니다. 제가 원한 것은 세 가지였습니다. 문서 등급마다 추가 인증을 받는 것, 사진과 영상까지 암호화해 두는 것, 기한이 있는 공유 링크입니다. 셋 다 기성 위키의 저장과 인증 핵심부를 건드려야 해서, 고쳐 쓰는 것보다 새로 짜는 쪽이 작다고 봤습니다.

프로토타입

코드를 짜기 전에 HTML 한 장짜리 프로토타입을 먼저 만들었습니다. 실제 서비스에 들어가지 않고, 화면이 어떻게 구성되어야 하는지 보여 주는 동작 기준으로만 썼습니다.

위키 프로토타입의 문서 읽기 화면. 초록색 상단 바에 검색창과 읽기·편집·히스토리 탭, 수정 요청·공유·저장 원본 단추, 블루·오렌지·레드 세 칸 권한 스위치가 있고, 본문에는 블루 등급 칩이 붙은 '홈트 루틴' 문서와 왼쪽 목차가 보인다.

이 한 장에 위키의 거의 모든 화면이 들어 있습니다. 상단 바에는 검색창과 읽기·편집·히스토리 탭, 수정 요청과 공유 단추가 있고, 오른쪽 끝에는 블루·오렌지·레드 세 칸짜리 권한 스위치가 있습니다. 대문에는 등급별 문서 수가 보이고, 전체 문서·최근 업데이트·초안·수정 요청 목록으로 이어집니다. '저장 원본' 서랍을 열면 지금 보는 문서가 저장소에 실제로 어떤 모양으로 들어가는지 보여 줍니다. 권한을 올릴 때 뜨는 2FA2단계 인증. 비밀번호 말고 한 가지를 더 확인하는 로그인. 이 위키는 그 한 가지만 씁니다. 창, 기한 있는 공유 링크를 만드는 창, 판을 비교하는 히스토리 화면도 들어 있습니다.

이 한 장을 두고 여러 차례 고쳤습니다. 등급, 히스토리, 공유의 동작을 여기서 정했고, 민트색 테마도 이때 골랐습니다. 저녁에 확정판이 나왔고, 그 뒤 구현은 전부 이 화면을 기준으로 했습니다.

세 단계 등급

문서는 민감한 정도에 따라 블루, 오렌지, 레드 세 단계로 나눕니다. 블루는 일반 문서입니다. 낮은 권한으로 보면 오렌지 문서는 있다는 것만 보이고, 레드 문서는 존재 자체가 드러나지 않습니다. 목록에도, 검색에도, 문서 개수에도 나오지 않습니다. 권한을 한 단계 올릴 때마다 2FA를 한 번 더 거칩니다. 오렌지와 레드는 공유할 수 없습니다.

처음에는 문서마다 따로 인증을 받을 생각이었는데, 권한 스위치 하나로 단계를 올리는 쪽을 택했습니다. 링크도 같은 규칙을 따릅니다. 링크 색은 가리키는 문서의 등급색이고, 낮은 권한으로 보는 화면에는 원래 글자 대신 대체 글자만 내려갑니다. 화면에서 가리는 것이 아니라 원래 글자가 아예 내려가지 않습니다.

설계

프로토타입이 확정된 뒤 Claude의 Fable 모델이 구조와 단계 초안을 썼습니다. 기술은 이렇게 골랐습니다.

  • 앱: Next.js + TypeScript, 편집기는 Tiptap(새 탭에서 열림)
  • 저장: Postgres + Drizzle
  • 인증: 직접 작성, TOTP시간 기반 일회용 비밀번호. 인증 앱이 시간에 맞춰 만들어 내는 여섯 자리 숫자. 이 위키의 2FA가 이 방식입니다. 방식
  • 암호화: AES-256-GCM
  • 미디어: 이미지 변환 sharp(새 탭에서 열림), 바이러스 검사 ClamAV(새 탭에서 열림)
  • 인프라: 리버스 프록시바깥에서 오는 접속을 먼저 받아 안쪽 서버로 넘겨주는 중계 서버. Caddy, 컨테이너앱과 그 실행 환경을 한 묶음으로 떼어 내 띄우는 것. Docker Compose는 여러 컨테이너를 설정 하나로 함께 띄우는 도구입니다.는 Docker Compose
  • 벡터 검색: 임베딩글의 뜻을 숫자 묶음(벡터)으로 바꾼 것. 뜻이 비슷하면 숫자도 비슷해져서 낱말이 달라도 찾을 수 있습니다. 이렇게 찾는 것을 벡터 검색이라고 합니다. 모델 KURE-v1(새 탭에서 열림)

설계에서 정한 것 가운데 글로 적을 만한 것은 이렇습니다.

  • 로그인에 아이디와 비밀번호가 없습니다. 2FA 여섯 자리만 넣습니다. 기기를 기억하는 기능도 두지 않았습니다.
  • 문서 제목, 본문, 미디어, 검색용 벡터까지 모두 암호화해 저장합니다. DB 덤프DB 내용을 통째로 파일 하나로 뽑아 둔 것.를 열어도 평문이 없습니다. 미디어는 볼 때만 메모리에서 풀고 디스크에는 평문을 쓰지 않습니다.
  • 크롬 전용이고, 공개 페이지가 없고, 검색엔진 노출을 막습니다.
  • AI는 초안과 수정 요청까지만 쓰고 게시는 사람이 합니다. AI가 위키에 접근하는 범위는 규칙으로 정했고, 위키 화면에서 AI에게 초안을 요청하는 기능은 뺐습니다.

구조 초안이 나오고, 구현에 들어가기 전에 AI가 물은 질문 아홉 개에 답한 뒤 "착수"라고 지시했습니다.

단계 나누기

일은 단계 0부터 10까지 열한 개로 쪼갰습니다. 단계 하나가 끝나면 보고를 받고, 제가 "진행"이라고 해야 다음 단계로 넘어갑니다. 표의 Brain은 앞에서 말한 AI 에이전트용 저장소이고, 위키 저장소는 그 안에 서브모듈git 저장소 안에 다른 저장소를 정해진 커밋으로 끼워 넣는 방법.로 들어 있습니다.

단계내용
0뼈대, 상단 바, 검색엔진 노출 차단, 자동 배포
12FA 로그인, 권한 스위치, IP 차단, 요청 기록, 보안 페이지
2문서 저장(암호화), 편집기, 대문, 목록, 검색, 삭제
3위키 DB와 Brain 노트의 장비 간 주 1회 상호 백업, 복구 절차서
4내부 링크, 링크 플레이스홀더, 스포일러, 각주
5히스토리, 판 비교
6미디어(바이러스 검사, 변환, 조각 암호화, 구간 재생)
7초안, 수정 요청, AI용 API와 토큰, Brain의 wiki 스킬
8기한 있는 공유 링크
9외부 공개 준비(HTTPS 전환, 쿠키 강화, API 내부망 한정, 점검 스크립트, 공개 절차서)
10벡터 검색(임베딩 + 키워드 합산)

단계 0은 프로토타입이 확정된 날 밤에 배포됐습니다. 다음 날 새벽에 단계 1이 올라갔고, 단계 2를 절반쯤 진행하던 새벽 세 시에 제 PC가 꺼지면서 세션이 끊겼습니다. 낮에 다시 이어서 그날 안에 단계 2부터 7까지 올렸습니다. 그다음 날 새벽에 단계 8과 9를 함께 배포하고 바깥에 열었습니다. 집 안에서 돌린 점검은 19개 통과, 실패 0이었습니다. 마지막 단계 10은 "백그라운드로" 마무리하라고 지시하고 잤고, 자는 동안 서브에이전트메인 에이전트가 일을 떼어 맡기는 에이전트. 빈 상태에서 지시서만 들고 시작합니다.가 끝내 배포했습니다.

단계 0부터 10까지 약 28시간이 걸렸습니다. 기획을 시작한 날부터는 사흘 남짓입니다. 위키 저장소에는 커밋 36개와 DB 마이그레이션DB 구조를 바꾸는 변경을 순서대로 적어 둔 파일. 하나가 변경 한 번입니다. 12개가 쌓였습니다.

역할 분담

AI 에이전트는 하나가 아니었습니다. 메인 역할을 맡은 Claude가 하나 있고, 그 아래에 일을 나눠 받는 서브에이전트들이 있습니다. 메인은 구현을 직접 하지 않습니다.

역할맡은 일
나방향 결정, 단계마다 "진행" 승인, 결과 검토와 지적
Claude 메인(Opus)지시서 작성, 린트·타입·테스트·빌드 재검사, 맥미니 Docker 시험 빌드, 화면 확인 지시, 커밋과 배포, 사용법 문서 갱신, 단계 보고
Fable 서브기술 스택·구조·단계 초안, 보안 설계 반박 검토, 단계별 독립 코드 검증, 테스트 축소 확정안
Opus 서브단계별 구현
Sonnet 서브요구사항 명세서 작성, 화면 확인, 지적 사항 수정

단계 하나는 이렇게 돌아갑니다. 메인이 지시서를 쓰고, 서브가 구현하고, 만든 것과 다른 모델이 그 코드를 따로 검증합니다. 메인이 린트·타입·테스트·빌드를 다시 돌리고 맥미니에서 Docker 시험 빌드를 거친 뒤 커밋하고 배포합니다. 그리고 제게 보고합니다. 제가 "진행"이라고 하면 다음 단계 지시서를 씁니다.

검증

검증에서 잡힌 것 몇 가지를 적어 둡니다.

모든 접속이 같은 주소로 보인 문제

보안 설계를 반박하는 검토에서 나왔습니다. 처음 구조에서는 리버스 프록시도 컨테이너 안에 있었습니다. 그러면 앱이 보는 접속 주소가 전부 같아집니다. 어떤 접속 하나를 차단하면 그것이 곧 모든 접속의 차단이 되는 구조였습니다. 검토가 이 점을 실제로 접속해 확인했고, 리버스 프록시를 컨테이너 밖 장비 본체로 옮겨 해결했습니다.

독립 검증이 잡은 누출

단계마다 구현한 서브와 다른 모델이 코드를 검증했습니다. 여기서 등급이 높은 문서가 새어 나가는 경로가 세 번 잡혔습니다. 제목이 겹칠 때 레드 문서가 있다는 것이 드러나는 문제, 줄바꿈에 붙은 링크가 가림을 우회해서 낮은 권한 화면의 데이터에 레드 문서 식별자가 들어가는 문제, 수정 요청의 인용문이 높은 권한에서만 보여야 할 글자를 낮은 권한에 보여 주는 문제였습니다.

성공을 실패로 끝낸 배포 스크립트

자동 배포가 성공하고도 실패로 끝나는 일이 있었습니다. 교체 뒤 옛 이미지를 지우는 단계에서, 지울 이미지가 없을 때 검색 명령이 돌려주는 종료 코드를 실패로 읽은 것이었습니다. 고친 뒤 다음 자동 배포는 8초 만에 반영됐습니다.

테스트 367개에서 23개로

가장 크게 고친 것은 테스트입니다. 단계마다 지시서가 항목별 자동 검사를 요구했고, 검증에서 지적이 나올 때마다 테스트를 하나씩 더 붙였습니다. 그러다 보니 같은 규칙을 지키는 테스트가 층마다 반복됐습니다. 줄이기 직전 테스트는 367개, 7,400줄이었습니다. 파일 28개가 각자 메모리 DB를 띄워서 무거웠고, 배포 빌드도 매번 이 테스트를 전부 돌렸습니다.

이 테스트가 작업 전체를 늦추는 병목이었습니다. 이를 발견하고, 구현을 맡은 모델보다 한 단계 위인 Fable에게 테스트 전체를 다시 검토하라고 지시했습니다. Fable은 지켜야 할 위협 네 가지를 기준으로 테스트를 다시 골랐습니다. 누출, 접근, 공유·링크·저장소, 인용 등급입니다. 이 넷만 지키도록 남기니 23개, 1,480줄이 됐습니다. 줄인 뒤에는 보안 코드 일곱 곳을 일부러 망가뜨려 각각 테스트가 하나 이상 실패하는지 확인했습니다. 전체 실행은 2.5초입니다. 배포 빌드에서는 테스트를 빼고 개발 장비에서 한 번만 돌립니다.

기타 이슈

바이러스 검사기 공식 이미지에 arm64애플 실리콘 맥이 쓰는 CPU 구조. 공식 이미지에 이 판이 없으면 그 맥에서는 돌지 않습니다. 판이 없어 다른 판으로 바꿨고, 장비 방화벽이 새 프록시를 막고 있어서 직접 허용했습니다. 검증은 위험에 비례해서 하기로 했습니다. 기본은 검사 한 번과 배포 빌드이고, 무거운 작업은 동시에 하나씩만 돌립니다.

배포 구조

개발은 Brain이 있는 맥스튜디오에서 합니다. 위키는 Brain 저장소의 서브모듈이고, 거기서 고치고 로컬에서 확인합니다. 맥미니에서는 직접 코딩하지 않고 배포만 받습니다.

브랜치는 둘입니다. main은 개발용이고, release에 올릴 때만 배포됩니다. 맥미니가 5분마다 release를 확인하고, 새 커밋이 있으면 맥미니에서 직접 빌드합니다. 교체하기 전에 DB를 덤프하고, 정해진 시간 안에 정상 응답이 없으면 직전 이미지로 되돌립니다.

맥미니 위에는 컨테이너 세 개(앱, Postgres, 바이러스 검사기)가 Docker Compose로 떠 있고, 리버스 프록시만 장비 본체에서 돕니다.

백업은 양방향입니다. 위키 DB는 맥미니에서 맥스튜디오로, Brain 노트는 맥스튜디오에서 맥미니로 매주 한 번 보내고 최근 두 개를 남깁니다. 서로가 서로의 백업 장소입니다.

왼쪽에 키 큰 은색 사각 컴퓨터와 코드가 뜬 노트북, 오른쪽에 납작한 은색 미니 컴퓨터가 있다. 둘 사이를 하늘색 아이콘 다섯 개(새로고침·망치·데이터베이스·맞바꿈·되돌림 화살표)가 화살표로 잇고, 미니 컴퓨터 위에는 상자 셋과 그 앞의 방패가 떠 있다. 위쪽에는 데이터베이스와 문서를 실은 곡선 화살표 둘이 서로 반대 방향으로 오간다.

마치며

저번 글에서 사람의 몫으로 남는다고 쓴 일은 무엇을 만들지 정하는 것, 요구사항의 빈칸을 찾는 것, AI가 내놓은 결과를 의심하고 책임지는 것이었습니다. 이번에 제가 한 일도 거기서 크게 벗어나지 않았습니다. NAS를 미루고 맥미니로 간 것, 로그인을 2FA 하나로 줄인 것, 문서마다 인증하는 대신 권한 스위치를 둔 것, 테스트를 다시 검토하게 한 것은 모두 사람이 내린 결정이었습니다.

다만 검토는 혼자 하지 않았습니다. 린트와 빌드는 도구가, 코드 검증은 만든 것과 다른 모델이, 화면 확인은 또 다른 서브가 했고, 저는 그 결과를 보고받아 검토하고 다음 진행을 정했습니다. 책임은 사람이 지되 검토 자체는 여러 겹으로 나눈 셈입니다.

코드는 에이전트에게 맡기고 결정과 승인, 검토는 제가 하는 것은 평소에도 일하던 방식입니다. 이번 글에서는 그 방식으로 개인 위키를 만들며 어떤 과정을 거쳐 작업이 진행되었는지 작성해 보았습니다.

아침빛이 드는 나무 책상 가운데 노트북 화면에 초록·분홍 줄이 나뉜 검토 화면과 민트색 승인 단추가 떠 있다. 노트북 위로 하늘색 아이콘 넷이 선으로 이어져 쉬고 있고, 왼쪽 위 은색 미니 컴퓨터에는 작은 불이 켜져 있으며, 오른쪽에는 김이 나는 커피 잔과 체크 표시만 있는 카드 세 장이 놓여 있다.