다이어그램을 비교했더니 달러 기호가 사라졌다

Archify 비교 화면에서 PID $$가 PID $로 바뀌는 문제를 고쳤습니다. JavaScript replace의 치환 문자열 규칙과, 수정 코드를 빼서 회귀 테스트를 확인한 과정을 기록합니다.

#오픈소스#JavaScript#Archify#SVG#회귀테스트

PID $$라는 이름의 노드가 있습니다. 다이어그램 하나를 그릴 때는 이름이 제대로 나옵니다. 그런데 그 노드를 지우거나 옮긴 뒤 변경 전후를 비교하면, 이전 위치에 표시되는 이름이 PID $로 바뀝니다. 달러 기호 하나가 사라집니다.

Archify는 아키텍처 다이어그램 등을 HTML과 SVG로 만드는 에이전트 스킬입니다. 여기에 두 아키텍처를 비교하는 기능이 있는데, 비교 결과에 이전 다이어그램의 요소를 끼워 넣는 과정에서 이 문제가 생겼습니다. 제가 보낸 PR #459는 2026년 9월 20일 dev 브랜치에 병합됐습니다.

replace의 두 번째 인자

JavaScript의 replace는 두 번째 인자로 받은 문자열을 그대로 넣지 않습니다. 그 안에 특정 달러 기호 조합이 있으면 치환 규칙으로 해석합니다. $$는 달러 기호 하나가 되고, $&는 매칭된 문자열이 됩니다.

문제를 작게 줄이면 이렇습니다.

const svg = '<svg><!-- slot --></svg>';
const markup = '<text>PID $$</text>';

svg.replace('<!-- slot -->', markup);
// '<svg><text>PID $</text></svg>'

첫 번째 인자에 정규식을 쓰지 않아도 같습니다. markup을 만들 때까지는 $$가 온전합니다. replace에 치환 문자열로 넘기는 순간 $로 바뀝니다.

Archify의 비교 화면은 현재 다이어그램에 삭제된 요소와 이동 전 요소를 덧붙입니다. 이전 SVG에서 가져온 마크업 안에는 사용자가 지은 이름도 들어 있습니다. 그 마크업을 replace의 두 번째 인자로 넘기면서, 이름에 있던 문자가 치환 규칙으로 해석됐습니다.

수정은 콜백을 넘기는 방식입니다. 콜백이 반환하는 문자열에는 이 치환 규칙을 적용하지 않습니다.

svg.replace('<!-- slot -->', () => markup);
// '<svg><text>PID $$</text></svg>'

실제 변경 중 하나도 같은 형태입니다.

-delta = delta.replace('</defs>', `${baseDefinitions}</defs>`);
+delta = delta.replace('</defs>', () => `${baseDefinitions}</defs>`);

기존 HTML/SVG 이스케이프 처리는 유지했습니다. 콜백으로 바꾼 것은 이미 만들어진 마크업을 삽입하는 방식입니다. 콜백 자체가 HTML을 안전하게 바꿔주는 것은 아닙니다.

화면에 보이는 이름 외에도 남는 값

같은 방식으로 문자열을 넣는 곳이 여러 군데 있었습니다. 이전 노드와 관계선을 삽입하는 코드, 경계 영역의 프레임과 라벨을 넣는 코드가 영향을 받았습니다. 경계 영역을 식별하는 data-delta-boundary-key 속성도 수정했습니다.

예를 들어 경계 영역 이름이 PID $$라면 식별 키는 region:PID $$여야 합니다. 화면의 텍스트만 고쳐도 속성에 region:PID $가 남아 있다면 원래 값을 보존한 상태가 아닙니다. 그래서 테스트에서 표시되는 이름과 경계 탐색용 키를 함께 확인했습니다.

이 저장소에는 앞서 이슈 #2에서 다룬 applyTemplate 경로의 별도 수정이 있었습니다. 이번 PR은 비교 결과를 조립하는 Delta 경로를 대상으로 했습니다. 변경 코드와 테스트에서 수정 범위를 확인할 수 있습니다.

수정 코드를 빼도 테스트가 통과할까

새 테스트는 CLI로 비교 명령을 실행하고, 생성된 HTML을 파싱합니다. 최종 SVG 안에서 이전 요소를 찾은 다음 텍스트와 속성 값이 입력과 같은지 검사합니다.

노드, 관계선, 경계 영역 각각에 삭제와 위치·경로·크기 변경을 적용했습니다. 여기에 네 가지 라벨을 넣었습니다.

PID $$
Match $&
Before $`
After $'

테스트는 6개이고, 각 테스트가 라벨 4개를 확인하므로 총 24가지 조합입니다. replace만 따로 호출하는 테스트로 끝내지 않고, 실제 비교 명령이 내놓은 결과까지 확인했습니다.

dev의 변경을 PR 브랜치에 합친 뒤에는 관련 테스트 62개 중 6개가 실패했습니다. 새로 만든 입력 fixture에 당시 dev가 요구하는 meta.output이 없었습니다. 라벨을 검사하는 줄에 도달하기도 전에 입력 검증에서 멈춘 겁니다.

fixture에 출력 경로를 추가하자 62개가 모두 통과했습니다. 그 상태에서 런타임 수정만 제거해 보니 새 테스트 6개가 모두 실패했습니다. 수정을 복원하면 다시 통과했습니다. fixture가 유효한지, 테스트가 달러 기호 손상을 실제로 잡는지 각각 확인할 수 있었습니다.

PR에 기록한 전체 로컬 테스트 결과는 2,060개 중 1,991개 통과, 실패 0개, 건너뜀 69개입니다. 최종 커밋의 원격 CI도 성공했습니다. 건너뛴 항목은 통과 수에 포함하지 않았습니다.

ZIP 안의 코드까지 확인하기

Archify는 저장소에 archify.zip도 함께 둡니다. dev를 합칠 때 ZIP 충돌이 생겨서, 합쳐진 소스로 Node 22에서 다시 만들었습니다.

두 번 빌드한 ZIP의 바이트가 같았고, dev의 ZIP과 비교했을 때 엔트리는 84개로 추가되거나 빠진 파일이 없었습니다. 달라진 파일은 archify/delta/architecture-delta.mjs 하나였으며, 압축 안의 모듈이 합쳐진 소스와 일치하는 것도 확인했습니다. 이 검증 결과는 PR 본문에 남겼습니다.

이번 수정에서 확인한 범위는 비교 결과의 텍스트와 메타데이터 보존입니다. 새 스크린샷으로 배치를 검토하지는 않았습니다. dev 병합까지 완료됐고, 사용자에게 배포된 버전은 이 글에서 확인하지 않았습니다.

아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!