본문으로 건너뛰기

pubuilder 10. 일시 종료 안전장치와 슬롯 기본값 로더 정리

·10 min read·10 / 10

퍼블리싱을 도중에 멈추는 일시 종료 기능은 이미 있었다. 잡을 시작할 때 baseline(되돌릴 기준이 되는 스냅샷)을 떠 두고, 종료 시 그 상태로 파일을 되돌리는 방식이다. 이날은 새 기능을 붙이는 대신 그 되돌리기가 어디까지 안전한지를 다시 봤다.

일시 종료가 사용자의 직접 편집까지 덮어쓸 수 있었다

되돌리기 기준이 잡 시작 시점의 스냅샷 하나뿐이었다. 에이전트가 파일을 고친 뒤 사용자가 같은 파일을 이어서 편집했다면, 일시 종료는 그 편집까지 통째로 시작 시점으로 되돌린다. 에이전트가 실제로 쓰기를 끝낸 시점의 파일 상태를 아무도 기억하고 있지 않았기 때문이다.

그래서 에이전트의 tool_usetool_result(Claude Agent SDK가 도구를 호출할 때와 그 결과를 돌려받을 때 주고받는 메시지)를 짝지어, 파일 쓰기가 끝난 시점의 SHA-256 해시(파일 내용으로 만든 지문 값)를 기록했다. 일시 종료 시에는 현재 파일 해시와 이 값을 비교한다. 같으면 에이전트 작성본 그대로이니 되돌리고, 다르면 사용자가 뒤이어 손댄 것이니 자동 롤백하지 않고 충돌 해결 대상으로 넘긴다.

롤백 안전성은 시작 전 스냅샷만으로는 부족했다. 에이전트 작성 이후에 생긴 변경까지 구분할 수 있어야 사용자 작업이 사라지지 않는다.

되돌릴 수 없는 파일을 조용히 건너뛰고 있었다

git 저장소가 아닌 프로젝트에서는 baseline 스냅샷 자체를 만들 수 없다. 이 경우 해당 파일을 그냥 건너뛰고 있었고, 사용자 화면에는 아무것도 남지 않았다. 되돌려졌는지 아닌지를 알 방법이 없는 상태였다.

이 상황을 unrestorable이라는 정상 결과 값으로 추가하고, 잡 시작 로그와 종료 결과 패널 양쪽에서 드러나게 했다. 복구할 수 없는 상태도 결과 타입에 정식으로 포함해야 서버와 API, UI가 빠짐없이 같은 사실을 전달할 수 있다. 같은 맥락에서 일시 종료 API 요청 자체가 실패한 경우도 조용히 넘어가지 않고 원인을 사용자에게 알리도록 했다.

일시 종료 시 파일 하나는 세 갈래로 갈린다.

기본 슬롯이 하나 늘 때마다 로더 코드를 고쳤다

장비창(스킬을 슬롯에 꽂아 퍼블리싱 지침을 조립하는 화면)의 슬롯에는 스킬을 꽂지 않아도 동작하도록 기본 스니펫이 들어간다. 그런데 로더가 슬롯별 기본 파일을 하나씩 하드코딩해 불러오고 있어서, 기본값을 붙일 슬롯이 늘어날 때마다 코드를 열어야 했다.

전체 슬롯을 순회하며 대응하는 skills/_defaults/<slot>.md를 찾아 읽는 방식으로 바꿨다. 이제 파일만 추가하면 코드 수정 없이 기본값으로 조립된다. 슬롯 추가 비용이 줄고, 기본 지침을 코드가 아니라 콘텐츠 변경으로 관리하게 됐다.

그 위에 비어 있던 두 슬롯의 지침을 채웠다. Icon 슬롯에는 구조를 보고 아이콘인지 판별하는 기준, SVG를 어디까지 적용할지, currentColor로 색을 치환할 때 mask와 clipPath를 보존해야 한다는 규칙을 넣었다. Image 슬롯에는 추출한 에셋을 쓸 것, 프로젝트의 이미지 컴포넌트를 우선할 것, 대체 텍스트와 크기를 명시할 것을 넣었다. 그리고 퍼블리시 로그 첫머리에 슬롯별로 무엇이 장착됐고 무엇이 기본값이며 무엇이 비었는지 보여 주는 조립 영수증을 붙였다.

정리하는 김에 이 흐름에서 더 이상 쓰지 않는 enabledSkillContents()와 실제 구현이 없던 tokenMap 설정 필드를 지웠다. 관련 타입과 테스트도 같이 정리해서, 설정 계약과 코드가 어긋난 채 남지 않게 했다.

오버레이가 사라져도 아무 흔적이 없었다

퍼블리싱 중·확인 필요 오버레이는 대상 요소의 selector로 위치를 잡는다. DOM이 바뀌어 selector가 안 맞으면 오버레이가 화면에서 사라지는데, 재조회 실패가 아무 기록 없이 반복되기만 해서 왜 사라졌는지 알 수 없었다.

재조회는 그대로 계속하되, 10회 연속 실패하면 콘솔에 selector와 상태를 담은 진단 경고를 한 번만 남기도록 했다. 리렌더 도중의 일시적인 실패에는 경고를 내지 않고 지속 실패만 드러낸다. 실패 횟수와 경고 여부를 오버레이 엔트리별로 들고 있게 해서, 첫 실패마다 경고를 뿌릴 때보다 불필요한 로그가 훨씬 줄었다.

UI에 붙어 있던 로직을 떼어 테스트로 고정했다

클라이언트 쪽 핵심 로직은 컴포넌트에 묻혀 있어서 회귀가 나도 눈으로 확인하는 수밖에 없었다. jsdom(Node에서 브라우저 DOM을 흉내 내는 테스트 환경) 설정을 추가하고, IA 트리(페이지 정보 구조를 트리로 표현한 화면) 레이아웃 계산과 스토어 상태 전이, selector 휴리스틱을 각각 독립 유닛 테스트로 고정했다. 순수 로직을 UI에서 떼어 두니 레이아웃과 상태 전이의 회귀를 더 빠르고 정확하게 잡아낼 수 있었다.

CLI 쪽에서는 autoPort 테스트가 특정 포트 번호를 하드코딩하고 있었다. 그 포트가 이미 점유된 환경에서는 코드가 멀쩡해도 실패한다. 런타임에 직접 확보한 빈 포트를 기준으로 검증하도록 바꿔 환경 의존성을 없앴다.

Unreleased 변경 이력을 Added·Changed·Fixed·Removed로 나눠 정리하고, 제품 개요와 7월 20~30일 개발 일지를 현재 구현에 맞게 고쳤다. 패키징 결과물인 *.tgz.gitignore에 넣어 저장소에서 뺐다.

기능의 폭을 넓힌 날은 아니었다. 일시 종료가 사용자의 직접 편집을 보존하고, 되돌릴 수 없는 경우도 숨기지 않게 됐다. 슬롯 기본값과 테스트를 정리해 다음 기능을 얹기도 편해졌다.

확인이 남은 것도 있다. 같은 파일을 여러 실행이 번갈아 수정할 때 해시 기준과 충돌 UX가 버티는지 봐야 한다. unrestorable로 표시된 파일을 수동으로 백업·복구하는 안내 흐름도 아직 없다. 오버레이 selector의 지속 실패를 콘솔 말고 UI 진단 정보에도 띄울지, 일시 종료 API와 클라이언트 컴포넌트의 통합 테스트를 어디까지 넓힐지는 정하지 못했다.