본문으로 건너뛰기

putree 20. 두 key로 갈라진 블록 선택 복구와 상시 패널 줄바꿈 규칙

·14 min read·20 / 20

테두리는 떴는데 눌러도 아무 일이 없었다

"확인 필요" 테두리는 카드 위에 제대로 떠 있었다. 그런데 그 카드를 눌러도 확인 질문이 열리지 않았다. 독의 행을 눌러도 같았다. 독은 진행 중인 퍼블리싱 작업 목록을 앱 화면 위에 늘 띄워두는 패널이다.

어느 쪽을 눌러도 질문을 열 수 없었다. 클릭 핸들러를 의심할 만한 증상이었다.

같은 요소가 두 개의 key로 갈라져 있었다

퍼블리싱 job은 pagePath + selector로 만든 key로 구분한다. 그런데 그 key를 만드는 곳이 두 군데였고, 두 곳이 요소를 찾는 방법부터 달랐다.

구분요소를 찾는 방법만드는 key
오버레이마커([data-putree-block])를 앞세운 후보 목록추적 시점의 pagePath + selector
선택누르는 순간 cssPath를 새로 계산방금 계산한 pagePath + selector

마커는 요소를 나중에 다시 찾으려고 소스에 심어두는 고유 속성이다. 반면 cssPath는 nth-of-type 기반의 구조 경로다. 퍼블리싱이 형제를 하나 늘리거나 래퍼를 씌우면 같은 요소인데도 문자열이 갈라진다. 그러면 패널이 찾는 key에는 job이 없어서 idle 퍼블리싱 폼이 떴다.

flowchart TD
    A["오버레이가 마커로 요소를 찾아 옛 key로 job 추적"] --> B["퍼블리싱이 형제를 늘리거나 래퍼를 씌움"]
    B --> C["같은 요소의 cssPath 문자열이 달라짐"]
    C --> D["카드 클릭: cssPath를 새로 계산해 새 key"]
    D --> E["패널이 새 key로 job 조회"]
    A -. "job은 옛 key에만 있음" .-> E
    E --> F["job 없음: idle 퍼블리싱 폼"]

selectBlock은 더 분명했다. 저장된 후보로 요소를 정확히 찾아놓고, 알릴 때는 cssPath를 새로 계산해 보고했다. 요소를 찾을 때 쓴 기존 key를 전달하지 않고 새 key를 만들고 있었다.

추적 중인 key를 그대로 물려받게 했다

emitSelecttrackedKeyOf(block)으로 그 요소를 추적 중인 오버레이 key를 먼저 찾는다. 있으면 그 key의 selector를 그대로 물려준다. 판정은 인스펙터에 뒀다. 같은 요소인지 비교하려면 실제 DOM이 필요한데, 패널에서는 문자열만 다루기 때문이다.

정확히 같은 DOM 요소일 때만 기존 key를 사용한다. 부모나 자식 요소까지 같은 것으로 보면, 안쪽 요소를 골랐는데 바깥 블록의 job이 선택될 수 있다. 그러면 엉뚱한 블록에 편집 지시가 전달된다.

재퍼블리싱으로 한 요소에 여러 작업이 연결되면 확인 → 진행중 → 작업완료 순으로 선택한다. 사용자의 답을 기다리는 질문을 먼저 보여주기 위해서다. 프레임 안에서 다른 경로로 이동한 직후 남은 엔트리는 pagePath가 다르면 건너뛴다. 다른 페이지의 selector가 섞인 key를 만들지 않기 위해서다.

key를 물려주면 store key와 리로드 후 하이드레이션 key에도 서버 job과 같은 selector가 쓰인다. selector를 새로 계산하면 다음 퍼블리싱에서 세 번째 key가 생긴다. 이전 경로를 넘겨도 에이전트가 소스를 찾는 데는 문제가 없다. 프롬프트에 selector는 "소스 위치를 찾는 단서"로만 들어가고, outerHTML과 앵커 마커는 매번 새로 실리기 때문이다.

테스트는 마커로만 찾을 수 있는 상태를 만들어 세 경우를 확인했다. 독 행 클릭과 카드 직접 클릭, 그리고 추적 중이 아닌 블록이다. 수정 전에는 앞의 두 개가 실패한다.

독 폭은 좁은 쪽에 고정하고 요약은 두 줄로 나눴다

독은 내용에 따라 폭이 달라졌다. min-width: 240 / max-width: 340으로 설정돼 있어 접으면 헤더 한 줄만 남아 240px이 되고, 펼치면 목록이 나타나면서 340px까지 넓어졌다. 토글할 때마다 상자가 커졌다 작아졌다 했다. 앱 화면 위에 늘 떠 있는 패널이라 좁았던 쪽으로 고정했다.

요약 문구는 세 번 갈아탔다.

시도왜 틀렸나
작업완료 2건수가 아니라 배지로 읽힌다
퍼블리싱 9건동시 퍼블리싱 상한(기본 8)을 넘긴 것처럼 읽힌다. 이 숫자는 슬롯이 아니라 끝나고 확인만 기다리는 것까지 센 값이다
총 9건무엇의 9건인지 말하지 않는다

문구만 세 번 바꿨지만 한 줄로는 목록 이름과 상태별 건수를 구분하기 어려웠다. 첫 줄에 "퍼블리싱 현황", 둘째 줄에 "진행중 6건 · 완료 3건"을 표시하니 무엇을 센 숫자인지 드러났다.

축약형(DOCK_STATUS_SHORT)은 요약 자리에서만 쓰고 행 안에서는 전체 이름을 유지한다. 목록의 행은 급한 작업부터 보여주되, 요약에는 진행 → 완료 → 요청 순으로 건수를 표시했다. 요약에서는 상태별 건수를 일정한 순서로 보여주면 된다.

목록을 열고 닫을 때는 언마운트하지 않고 감싸는 요소(.putree-dock-body)의 max-heightopacity를 바꿨다. 접었을 때는 visibility로 숨겨 키보드로 탐색할 때도 목록 안으로 포커스가 이동하지 않게 했다. boolean inert는 React 19부터라 peerDependencies로 React >=18을 받는 이 패키지에서는 쓸 수 없었다.

헤더 경계선을 표시하는 조건은 :not(:last-child)에서 [aria-expanded='true']로 바꿨다. 목록이 상시 마운트되면 헤더는 더 이상 :last-child가 아니어서, 접었을 때 밑변에 1px이 남는다. 헤더와 행에는 누름 반응(:active)을 넣고 prefers-reduced-motion에서는 전환을 껐다.

테스트에서는 data-collapsedaria-expanded 값으로 접혔는지 확인한다. 접어도 목록이 DOM에 남으므로 요소가 있는지만 검사해서는 구분할 수 없다. 두 줄 요약은 상태가 섞인 경우와 하나뿐인 경우를 모두 확인했다. 폭과 전환 효과는 이 테스트로 확인하지 못했다.

긴 이름은 줄바꿈하고 툴팁으로 전체를 보여줬다

확인 대기 목록에도 같은 부류의 문제가 있었다. .putree-review-pending-nameflex-shrink: 0을 뒀던 것이 문제였다. 블록 이름이 짧은 라벨이라는 전제였는데, 본문 텍스트를 통째로 이름으로 쓰는 블록에서는 이름이 패널 밖으로 밀려나 오른쪽이 잘렸다. 무엇을 확인하는 건지 알 수 없었다.

패널이 좁거나 이름이 길 때는 줄바꿈하도록 했다. 페이지 경로와 블록 이름이 잘리면 어떤 항목인지 구분하기 어려웠다. 한국어 줄바꿈에는 두 규칙이 함께 필요하다.

.putree-dock,
.putree-review-pending-name {
  word-break: keep-all;
  overflow-wrap: anywhere;
}

keep-all은 어절 단위로 끊고, anywhere는 경로나 셀렉터처럼 끊을 자리가 없는 토큰만 아무 데서나 끊는다. 하나만 쓰면 문장이 글자 단위로 끊기거나 긴 selector가 영역 밖으로 넘친다.

이름은 남는 폭을 채우고(flex: 1 1 auto) 최대 두 줄까지 표시한다. 파일명은 이름 뒤에 붙다가 자리가 없으면 아랫줄로 내려간다(head를 flex-wrap: wrap). 전체 이름은 툴팁에서 볼 수 있게 했다. title이 selector만 담고 있어서 잘린 이름을 확인할 방법이 없었다. 이름을 먼저 두고 selector를 다음 줄에 붙였다.

네 가지 문제의 원인과 수정 내용

문제원인해결
독 폭이 토글마다 240↔340으로 벌어짐폭을 내용에 맡겼다(min/max)좁은 쪽 240px로 고정 + max-width: calc(100vw - 32px)
접었을 때 헤더 밑변에 1px이 남음목록을 상시 마운트하니 헤더가 더 이상 :last-child가 아니다경계선 조건을 [aria-expanded='true']
확인 필요 카드를 눌러도 질문이 안 열림오버레이는 마커로 찾은 옛 key, 선택은 cssPath를 새로 계산한 새 key. 퍼블리싱이 형제를 늘리면 문자열이 갈라진다emitSelecttrackedKeyOf로 추적 중인 key의 selector를 물려받는다
긴 블록 이름이 오른쪽에서 잘림이름에 flex-shrink: 0. 짧은 라벨이라는 전제flex: 1 1 auto + 2줄 접기 + 툴팁에 이름 우선

요소를 찾을 때와 선택을 알릴 때 서로 다른 key를 쓰니 연결된 작업을 열 수 없었다. 사용자에게 보인 모습은 "테두리는 떴는데 눌러도 아무 일이 없다"였다. UI 버그처럼 보이지만 실제로는 식별자를 계산하는 위치가 어긋난 문제였다. 구조가 바뀌면 경로도 달라지므로 이를 식별자로 쓸 때는 주의해야 했다. 앵커 마커를 도입했어도 일부 경로에서 cssPath를 다시 계산하고 있어 문제가 남았다.

고친 코드 자체는 인스펙터 몇십 줄이다. 왜 두 key가 갈라졌는지 밝히는 데 시간이 훨씬 더 들었고, 그게 맞는 비율이었다고 생각한다. 요약 문구 쪽은 반대였다. 문구를 세 번 갈아타는 동안 정작 줄을 나눌 생각은 못 했다. 같은 정보를 한 줄에 밀어넣으려 할 때 문구 다듬기로 해결하려는 습관이 있는 것 같다.

독의 폭과 전환 효과는 실제 화면에서 확인해야 한다. 카드 상태 색 글로우와 독 색 체계가 따로 노는지도 나란히 놓고 봐야 한다. 퍼블리싱을 돌려보는 동안 playground 픽스처에 남은 앵커 마커와 그리드 열 수 변경 흔적도 정리해야 한다. cssPath를 key로 쓰는 나머지 경로도 마커 기반으로 통일할 수 있는지 조사해야 한다.