본문으로 건너뛰기

Pointly 03. 편집 실패 원인 노출과 그려서 지정하는 삽입 자리

·11 min read·3 / 7

Pointly는 개발자가 아닌 사이트 오너가 바꾸고 싶은 부분을 클릭하고, 원하는 결과를 말로 적고, 미리 보고, 배포하는 도구다. 이날 한 일은 지나고 보니 주제가 두 개였다. 실패를 말이 되게 만드는 일과, 아직 없는 것을 가리키는 방법.

실패는 이유와 함께 나가야 했다

편집이 실패했을 때 오너가 볼 수 있는 문장은 딱 하나였다.

편집을 끝내지 못했어요. 다시 시도해 주세요.

로그인이 풀린 것도, 사용량 한도에 걸린 것도, 네트워크가 끊긴 것도 전부 같은 문장이었다. 그리고 당연히, 다시 시도해도 같은 이유로 다시 실패했다.

원인을 따라가 보니 실패 이유가 세 곳에서 각각 버려지고 있었다.

  • claude CLI의 stderr는 아무도 읽지 않았다.
  • stream-json의 result 줄은 실은 is_error / subtype을 들고 있었는데, 요약 변수에 담긴 뒤 종료 코드 분기에서 사라졌다. stream-json은 CLI가 한 줄에 JSON 이벤트 하나씩 흘려보내는 출력 형식이고, 마지막 result 줄에 성공/실패 정보가 들어 있다.
  • 클라이언트는 HTTP 500과 "아예 붙지도 못함"을 같은 문구로 덮었다.

이제 원인은 결과 줄을 먼저 보고, 없으면 stderr 꼬리, 그다음 종료 코드 순으로 찾는다. 알아볼 수 있는 원인이면 조치까지 문장에 담는다. 화면에 나가는 건 여전히 한 줄이지만, 정제한 원문을 error 이벤트의 detail로 함께 실어 보낸다. UI에서는 기본 접힘에 펼치기 버튼, 그 옆에 복사 버튼을 뒀다. 평소엔 읽을 필요가 없고, 필요할 때는 남에게 전달할 수 있어야 하니까.

stderr를 읽는 건 메시지 문제만이 아니었다. 자식 프로세스가 stderr에 쓴 내용을 부모가 읽지 않으면 버퍼가 차고, 자식은 다음 write에서 그 자리에 멈춘다. 원인을 버리던 코드가 멈춤도 만들고 있었다. 버그 하나를 고쳤더니 사실 두 개였던 경우다.

상수 13이 모든 편집을 막고 있었다

편집 후 data-copy 개수를 상수 13과 비교해서 다르면 되돌리는 안전장치가 있었다. data-copy는 편집 가능한 문구 자리를 표시하는 속성이다. 사이트가 자라서 문구 자리가 55개가 되자, 모든 편집이 이렇게 거부됐다.

문구 연결이 끊어져서 되돌렸어요 (data-copy 55/13)

지켜야 하는 건 특정 숫자가 아니라 "편집 전에 있던 문구 자리를 잃지 않는 것"이었다. 이제 실행 직전에 개수를 재고, 그보다 줄었을 때만 막는다. 섹션이 통째로 지워진 편집은 그 안의 문구도 같이 사라지는 게 맞으니 검사에서 빼 준다.

flowchart TD
    A["실행 직전 data-copy 개수 측정"] --> B["편집 후 개수 비교"]
    B --> C{"편집 전보다 줄었나"}
    C -->|아니오| D["통과"]
    C -->|예| E{"섹션이 통째로 지워진 편집인가"}
    E -->|예| D
    E -->|아니오| F["되돌리기"]

불변식을 코드에 숫자로 적으면 사이트가 바뀔 때마다 사람이 따라 고쳐야 한다. 아무도 안 고치면 안전장치는 조용히 전면 차단으로 바뀐다.

없는 것을 가리키려면 그려야 했다

지금까지 편집기는 "있는 요소"만 가리킬 수 있었다. 그런데 오너가 진짜 하고 싶은 말 중 하나는 "여기에 뭘 하나 추가해 줘"다. 추가는 요소가 아니라 순서 안의 틈을 가리킨다. 클릭으로는 틈을 못 집는다.

그래서 드래그로 사각형을 그려서 자리를 지정하는 레이어를 만들었다(SlotDrawLayer, 엔진 slot.ts). 그린 사각형은 의도의 표현이고, 저장하는 건 그 의도를 번역한 DOM 관계다. 설계에서 이 판단이 제일 중요했다. 좌표 대신 "어느 요소의 앞/뒤"처럼 트리 위치로 저장한다. 좌표를 들고 있으면 뷰포트가 바뀌는 순간 다른 곳을 가리킨다.

엔진과 테스트로 1,800줄 넘게 나온, 이날 제일 큰 덩어리였다.

그리고 바로 이어서 한 번 깎아냈다. 처음엔 자리 라벨에 형제 이름("이미지 오른쪽"), "전폭", 앵커 섹션 이름까지 다 적었는데, 후보에 올리면 그 자리가 이미 박스로 칠해진다. 같은 말을 글자로 또 쓰고 있었고, 두 배로 길어진 줄은 카드에서 잘려 앞부분만 남았다. 라벨을 짧게 줄이고 칩 너비도 264에서 240으로 같이 줄였다. slot.ts는 21줄 추가에 50줄 삭제로, 기능을 다듬었더니 오히려 줄었다.

Cmd+D 단축키도 붙였다. 키 조합 판정은 순수 함수로 떼어내서 DOM 없이 경계를 고정했다. 입력 중 Cmd+D는 받고, Ctrl+D는 흘린다. 맥 텍스트 필드에서 커서 오른쪽 글자를 지우는 키라서다. preventDefault로 브라우저 북마크 창도 막는다. 그리고 버튼에 단축키 표기를 적었다. 안 보이는 단축키는 없는 단축키다.

단축키로 그리기를 켜면 마우스가 그 자리에 그대로 있어서, 상태만 비워서는 hover 윤곽이 새어 나왔다. 그리는 중에는 hover 윤곽을 아예 그리지 않도록 했다.

다른 자리에서 도는 작업은 화면에 흔적이 없었다

요청 패널은 지금 선택한 자리 하나만 비춘다. 선택과 무관하게 전체를 세는 뷰(JobBoard)와, 해당 블록 위에 단계를 계속 표시하는 오버레이(JobOverlays)를 더했다.

jobs 상태에는 review(사람의 확인)와 target(되돌아갈 자리), 블록별 첨부를 추가했다. reviewstatus와 별개의 축이다. 끝난 작업이 아직 확인 안 된 상태일 수 있으니, 하나의 상태 머신에 욱여넣지 않았다.

동시 편집 얘기가 나온 김에 서버 쪽도 손봤다. 복원과 변경 감지를 이 실행이 쓴 파일로 좁혀서 동시에 도는 편집이 서로를 지우지 않게 했다.

SiteMapbusyPaths(boolean)를 phases(단계 맵)로 바꿨는데, App 쪽 변경과 떨어뜨리면 컴파일이 안 됐다. 그래서 첨부·삽입 자리·작업 목록을 편집기에 연결하는 배선 작업에 SiteMap 변경도 함께 넣었다.

참고 시안은 검증을 어디서 할지부터 갈랐다

요청에 이미지를 붙일 수 있게 했다. 검증 규칙은 플랫폼 중립 엔진(editor/attachments.ts)에 두고, File과 FileReader를 만지는 입력 처리는 소비 스코프(components/attachment-intake.ts)로 뺐다. 목록과 첨부 버튼은 화면에서 자리가 달라 두 컴포넌트로 나눴다.

서버에서는 data: URL로 온 시안을 디스크에 쓰고 프롬프트에는 그 경로를 적는다. data: URL은 파일 내용을 문자열로 담은 URL이다.

이날 고친 것들의 공통점은 "정보를 어디서 잃어버리는가"였다.

  • 실패 원인은 파이프와 분기에서 잃었고,
  • 불변식은 상수로 굳으면서 의미를 잃었고,
  • 동시에 도는 작업은 "선택된 것만 본다"는 UI 전제 때문에 잃었고,
  • 자리 이름은 오히려 너무 많이 말해서 잘려 나갔다.

마지막 게 재밌다. 정보를 살리는 일과 늘리는 일은 다르다. 이미 화면이 박스로 보여주고 있는 걸 글자로 또 쓰면, 그건 정보가 아니라 잘려 나갈 줄이다.