6편에서
자정 레이스를 잡은 뒤로, 이번엔 실행 로직이 아니라 화면 쪽에서 문제가
왔다. 스크린샷 한 장이었다 — /refactor가 띄운 분할 제안 카드가, 도저히
읽을 수 없는 모양으로 나와 있었다.
스크린샷 한 장이 보여준 4가지 문제 — reason 필드 줄바꿈 붕괴
카드 안에는 이런 게 들어 있었다. reason 텍스트는 [구조 결함]:
[복리 가치]: [연결 설계]: 세 문단으로 쓰이도록 프롬프트가 설계돼
있는데, 화면엔 줄바꿈이 전부 붕괴돼서 세 문단이 한 덩어리로 이어져
있었다. [라벨]: 마커는 그냥 평범한 글자라 구조가 전혀 안 보였다.
“연결 설계” 문단은 다른 노트를 실제 [[위키링크]] 문법으로 인용하라고
프롬프트가 지시하는데, 그게 클릭 가능한 링크가 아니라 리터럴 [[...]]
대괄호로 그대로 노출되고 있었다. 그리고 병합 소스나 분할 타겟이 3개
이상이면, pill 여러 개가 제목 줄 안에서 인라인으로 wrap되면서 한국어
단어들이 뒤섞인 것처럼 보였다.
넷 다 원인은 제각각이었다. 첫 번째는 reason을 렌더링하는 쪽이 그냥
white-space 지정 없는 flat 텍스트 노드 하나로 찍고 있어서였다. 세
번째는 그 렌더링 함수가 [[wikilink]] 문법을 파싱하는 로직 자체가
없어서였다. 네 번째는 제목 줄이 flex 레이아웃이라 pill이 몇 개든
그냥 나열만 하고 있어서였다.
네 개를 따로따로 고치지 않기로 했다 — white-space: pre-line
각각 고칠 수도 있었지만, 한 화면에서 동시에 겪는 문제라 따로 고치면
서로 어긋날 위험이 있었다. reason 텍스트를 [라벨]: 마커로 문단
분리해서 각각 <strong>[라벨]</strong> 본문 구조로 렌더링하고
white-space: pre-line을 줬다. [[wikilink|별칭]] 문법은 별칭이
있으면 별칭으로, 없으면 파일명으로 치환해서 대괄호가 화면에 안
남게 했다. pill 개수 문제는 두 안을 놓고 골랐다 — 인라인을 유지하되
폭만 제한할지, 2개 초과면 세로 목록으로 아예 분리할지. 후자를
골랐다. 제목 줄엔 “(N개)“라는 요약과 타겟 pill 하나만 남기고, 나머지는
- 기호를 붙인 세로 목록으로 이동시켰다.
테스트는 렌더링 함수를 실제 DOM 없이 순수 데이터로 뽑아내는 기존
구조를 그대로 따라갔다 — 병합/분할 세그먼트를 만드는 함수가 이제
{ titleSegments, listItems }를 반환하도록 반환 타입을 바꾸고, 2개
이하면 예전과 완전히 똑같이 인라인으로, 3개 이상이면 요약+목록으로
갈리는 걸 각각 검증했다. tsc/lint/테스트 전부 클린을 확인하고
커밋했다.
고치고 나니, 실제로 써볼 차례였다
여기서 끝났으면 평범한 UI 버그 수정 이야기였을 거다. 그런데 새로 정리된 화면으로 실제 분할 제안 하나를 진짜로 실행해봤다 — 회사 조직 개편을 다루는 노트 하나를 전략/조직/양산/공급망 네 갈래로 쪼개는 제안이었다. 실행 결과를 열어보니, 분할된 새 노트 네 개는 링크도 말끔하고 내용도 잘 나왔다. 그런데 그 원본 노트를 인용하고 있던 다른 노트 하나를 열어보니 이상한 게 보였다.
본문에 남은 링크 하나를 열어보니, “이 구조는 [양산 목표 노트 경로]를 가리키지만 화면엔 [전략 및 조직 개편]이라고 표시”되는 식으로 되어 있었다. 링크가 가리키는 경로는 방금 분할로 새로 생긴 “양산 목표” 노트로 정확히 재지정돼 있었다. 그런데 화면에 표시되는 텍스트는 이번 분할로 삭제된 옛 노트 제목(“전략 및 조직 개편”) 그대로였다. 클릭하면 양산 목표 노트가 열리는데, 화면엔 존재하지도 않는 옛 이름이 찍혀 있는 상태였다.
별칭이 갱신 안 되는 이유 — 위키링크
분할이 실행되면, 원본 노트를 인용하던 다른 노트들의 링크를 새 타겟 중 어디로 재배정할지 AI가 판단하는 단계가 있다. 그 판단 결과를 실제로 링크에 반영하는 코드를 열어봤다.
원인은 한 줄이었다. 재배정된 새 경로로 링크를 다시 쓰는 코드가, 링크를 처음 스캔할 때 캡처해둔 별칭 텍스트를 재배정 이후에도 그냥 그대로 재사용하고 있었다. 경로는 새로 계산하면서, 별칭은 옛 상태를 붙들고 있었던 거다. 병합 쪽에도 똑같은 패턴이 있었다 — 여러 옛 경로를 하나의 새 경로로 합칠 때도, 캡처해둔 별칭을 그대로 복붙하는 구조였다.
그런데 무조건 새 제목으로 덮어쓰면 안 됐다 — 548개 중 182개(33%)는 커스텀 별칭
처음엔 “재배정될 때마다 별칭을 새 타겟 제목으로 갱신하면 되겠다”고
단순하게 생각했다. 그런데 이 vault의 링크가 전부 그런 식으로
쓰이는 게 아니었다. [[GE와 DBS의 사례|options]]처럼, 별칭이 그냥
문장에 맞춘 짧은 표현인 경우가 꽤 있었다.
실제로 vault 전체의 별칭 붙은 링크 548개를 스캔해봤다. 182개(33%)가 타겟 제목과 다른 별칭을 쓰고 있었다 — 문장 흐름에 맞춘 표현, 소문자, 축약형 같은 것들이었다. 이 상태에서 재배정할 때마다 별칭을 새 타겟 제목으로 무조건 덮어썼다면, 이 33%는 오히려 새로 망가졌을 거다.
그래서 조건을 하나 걸었다. 별칭이 옛 링크의 제목을 정확히 그대로 미러링한 경우에만 새 타겟 제목으로 갱신하고, 그 외의 커스텀 별칭은 손대지 않고 그대로 둔다. 병합 쪽 단일 타겟 치환과 분할 쪽 AI 재배정, 두 갈래 모두 같은 조건으로 고쳤다. 회귀 테스트도 두 경우를 각각 검증하도록 추가했다 — 미러링된 별칭이 갱신되는 케이스, 커스텀 별칭이 보존되는 케이스.
남는 생각
이번 이야기의 두 축은 성격이 완전히 달랐다. 첫 번째(가독성)는 화면을 보고 바로 눈에 띄는 문제였고, 두 번째(별칭)는 그 화면을 실제로 써서 진짜 분할을 실행해보지 않았으면 절대 안 보였을 문제였다. 링크의 경로와 화면에 찍히는 텍스트가 서로 다른 값이라는 건, 코드를 읽는 것만으로는 좀처럼 떠오르지 않는 구분이다 — 실제로 실행해서 결과물을 열어봐야, “이게 왜 이렇게 보이지?”라는 질문이 나온다.
그리고 그 질문에 답을 찾는 과정에서 한 번 더 배웠다 — “일관되게 고치자”는 직관이 언제나 옳은 게 아니라는 것. vault 실측(33%가 커스텀 별칭)이 없었다면, 더 “깔끔해 보이는” 무조건 갱신 쪽으로 갔을 거고, 그게 오히려 새로운 손상이었을 거다.