pubuilder 04. 블록별 동시 퍼블리싱과 안전한 일시 종료
Phase 3 AI 퍼블리싱의 서버 코어는 있었지만, 브라우저에서 실제 코드 변경까지 잇는 경로는 비어 있었다. 이날 그 경로를 끝까지 연결했다. 서버 코어를 로컬 컴패니언 서버로 올리고, 퍼블리싱 UI를 붙이고, 패키지 구조를 정비하고, 블록별 동시 실행과 안전한 일시 종료까지 하나씩 채웠다.
명칭을 pubuilder 하나로 모았다
pubmap으로 흩어져 있던 패키지 경로·문서·테스트 픽스처의 이름을 pubuilder로 통일했다. 제품명이 두 갈래로 갈리면 import 경로와 문서 예시가 어긋나고, 신규 사용자가 어느 쪽이 맞는지 헷갈린다.
서버 코어를 로컬 컴패니언 서버로 올렸다
브라우저 단독으로는 파일을 쓸 수 없으니 서버 코어를 로컬 HTTP 서버로 올렸다. 스킬·토큰·퍼블리시·취소 API와 잡 이벤트를 SSE(Server-Sent Events, 서버가 클라이언트로 이벤트를 계속 흘려보내는 단방향 스트림)로 내보내는 컴패니언 서버다. serve로 서버만 단독 실행하고, dev로 앱 개발 서버와 서버를 동시에 띄우는 서브커맨드를 추가했다.
에이전트를 spawn(Node.js에서 자식 프로세스를 띄우는 함수)할 때 사용자의 전역 CLI 설정이 함께 로드되면, 앱이 선언한 권한 화이트리스트가 무력화됐다. --setting-sources(Claude Code CLI가 로드할 설정 소스를 제한하는 플래그)로 실행 환경을 격리해 앱이 정한 권한 정책만 적용되게 했다.
잡 종료 판정에서도 한 번 걸렸다. stdout이 끝나는 end 시점을 잡 종료로 보면 자식 프로세스가 아직 정리 중인데도 끝난 것으로 판정됐다. 종료 확정 기준을 프로세스 close 이벤트(자식 프로세스가 완전히 끝났을 때 발생)로 바꿨다. 여기에 404·요청 본문 오류, busy/cancel, 시그널 정리와 종료 코드 전달까지 테스트로 보강했다.
브라우저에서 outerHTML을 서버로 보내 퍼블리싱을 이었다
퍼블리싱 UI를 붙였다(v0.4.0). 브라우저에서 선택한 블록의 outerHTML(요소 자신을 포함한 HTML 문자열)과 Figma URL을 서버로 보내고, 돌아오는 SSE 로그와 중단 상태를 화면에 표시했다. 스킬 목록·토글·업로드·삭제와 Figma 토큰 설정은 드로어 하나에 모았다.
SSE UI는 성공 응답만 그리면 되는 게 아니었다. 연결 전 비동기 갭, 연결 단절, 그 사이 선택 변경, terminal 정리까지 하나의 상태 머신으로 다뤄야 했다. 이전 요청의 구독이 남아 최신 상태를 덮어쓰는 stale 구독, 중복 로드, checking 상태 고착, SSE 단절을 요청·구독 세대 가드로 하나씩 막았다.
패키지 구조를 정비하고 데모를 NIMBUS로 바꿨다
패키지와 제품 구조를 정비했다(v0.5.0~v0.6.0). pubuilder/config 서브패스 export와 Node ESM(ES Modules, import/export 기반 모듈 시스템)용 .js 확장자를 정리했다. 데모는 범용 AI 에이전트 플랫폼 NIMBUS로 교체하고 기획·아키텍처·기능 계획서와 CHANGELOG를 붙였다. 스킬함 진입점을 퍼블리싱 패널로 옮기고, 이미 끝난 잡에 AI 후속 답변을 이어 보낼 수 있게 했다.
블록별로 동시에 퍼블리싱을 돌리게 했다
잡을 선택 컴포넌트의 로컬 상태에 두면 블록을 바꾸거나 맵을 닫는 순간 실행이 끊겼다. 잡을 전역 publishJobs로 승격해 화면을 옮겨도 실행이 유지되게 했다. 블록 키별 SSE 구독을 모듈 레벨에서 관리하고, 서버 동시 실행 상한을 3개로 뒀다.
동시 작업의 식별자는 "현재 선택"이 아니라 잡이 시작될 때 확정된 안정적인 블록 키여야 했다. 선택은 언제든 바뀌지만 잡은 시작 시점에 묶여야 하기 때문이다. 이 과정에서 React Flow의 attribution 표시를 숨기고 캔버스 컨트롤러를 복원했다.
"일시 종료"는 프로세스 kill만으로 끝나지 않았다
실행 중인 잡을 한 번에 종료하는 기능을 넣으면서, 프로세스를 죽이는 것만으로는 부족하다는 게 드러났다(v0.7.0). AI가 이미 건드린 파일을 되돌려야 하는데, 그 파일에 사용자의 손 변경이 섞여 있으면 강제 복원이 사용자 작업까지 날릴 수 있었다.
잡 시작 시 git stash(작업 중인 변경을 임시로 치워 깨끗한 상태로 되돌리는 git 명령) 기반 baseline과 수정 파일 목록을 기록해뒀다. 종료할 때 사용자 변경이 없는 파일은 자동 복원하고, 사용자 변경과 겹치는 파일은 충돌 패널로 넘겨 이전으로·현재 유지·편집 중 하나를 고르게 했다.
suspend·resolve API, store 상태, 오버레이·뷰어 버튼을 연결하고, mock이 아니라 실제 git 저장소로 통합 테스트를 붙였다. 파일을 바꾸는 기능은 mock만으로 안전성을 보장할 수 없다. 실제 저장소에서 baseline 비교와 자동 복원이 사용자 변경을 건드리지 않는지 눈으로 확인해야 안심이 됐다.
구현하며 막힌 다섯 지점
기능을 붙이는 동안 다섯 곳에서 막혔고, 원인과 해결을 정리하면 아래와 같다.
| 문제 | 원인 | 해결 |
|---|---|---|
| 프로세스 출력 종료와 잡 종료 시점 불일치 | stdout end만으로 자식 프로세스 종료를 확정 | 프로세스 close 이벤트를 종료 기준으로 사용 |
| 퍼블리시 직후 상태·구독이 이전 선택을 덮어씀 | 요청 중 선택 변경과 비동기 응답 경합 | 요청·구독 세대 가드와 블록 키 기반 상태 관리 |
| 에이전트 권한 화이트리스트 우회 가능 | 사용자 전역 설정이 함께 로드됨 | spawn 시 --setting-sources를 제한해 실행 환경 격리 |
| 강제 종료가 사용자의 기존 작업까지 되돌릴 위험 | 에이전트 변경과 사용자 변경이 같은 파일에 공존 | baseline·수정 파일 비교 후 안전한 파일만 자동 복원, 나머지는 명시적 충돌 해결 |
| git 명령 stderr가 테스트 출력을 오염 | 자식 프로세스 stderr 상속 | 출력을 pipe로 받아 필요한 오류만 처리 |
아직 남은 것
- 반복 사용 가능한 기본 퍼블리싱 스킬을 패키지에 포함
- Figma 원본 JSON의 프롬프트 토큰 비용 절감
- 설치 직후 별도 설정 없이 기본 스킬이 보이도록 레지스트리 확장
서버 연결에서 제품화, 대화 연속성, 동시성과 안전성까지 v0.4.0에서 v0.7.0으로 넘어오는 각 단계는 방향이 뚜렷했다. 그중 "일시 종료"를 실제 git 저장소로 검증한 게 가장 마음에 남는다. 코드를 바꾸는 도구는 되돌리는 쪽이 훨씬 위험하다는 걸, 사용자 변경과 에이전트 변경이 한 파일에서 겹치는 케이스에서야 실감했다.