pubmap 01. IA 미니맵·블록 인스펙터 구현과 실전 배포
화면설계서(서비스의 페이지 구성과 흐름을 정의한 기획 문서)의 IA를 브라우저에서 바로 훑어볼 수 있는 도구, pubmap을 만들기 시작했다. 하루 동안 Phase 1과 Phase 2의 뼈대를 세우고, 실제 서비스 두 곳에 설치하는 데까지 갔다.
FAB 하나에서 시작하는 IA 미니맵과 블록 인스펙터
Phase 1은 IA(Information Architecture, 페이지 구조 설계) 미니맵이다. FAB(화면 위에 떠 있는 실행 버튼)을 누르면 React Flow(노드 기반 캔버스 라이브러리)의 줌/팬 캔버스가 열리고, IA 트리를 라이브 iframe 썸네일로 렌더링한다.
Phase 2는 블록 인스펙터다. PageViewer로 페이지를 전체화면으로 확대하고, 개발자도구처럼 섹션/유닛 블록을 클릭해 선택한다. SelectionPanel은 선택한 블록의 메타를 보여주면서, Phase 3(Figma→AI 퍼블리싱)로 이어질 진입점을 미리 마련해뒀다.
샘플 앱으로 examples/playground를 같이 만들었다. 화면설계서 IA를 재현한 react-router 7페이지짜리 앱이다. 검증은 type-check, vite build, 그리고 Playwright E2E 스모크 테스트(핵심 흐름만 빠르게 확인하는 자동화 테스트)로 잡았다. FAB → 캔버스 8노드 → 뷰어 → 블록 선택까지 한 줄로 통과하는지 본다.
디자인은 DevTools 톤으로 걷어냈다
툴 UI를 DevTools/VSCode 톤으로 바꿨다. 플랫하게, radius는 2px 이하로, 무채색에 #0e639c 액센트 하나만 남겼다. 도트 배경, 원형 FAB, 글로우, 이모지를 전부 제거하고 개발 도구다운 절제된 UI로 만들었다.
playground를 실제 서비스 도메인으로 다시 만들었다
playground를 wavbor-next 기반으로 재구성했다. 실제 서비스(음악 아티스트 플랫폼) 도메인을 재현해서 로그인/홈/Artflo/Showcase/Chat/마이페이지에 상세 페이지 2종, 외부링크, 404까지 10노드짜리 IA가 됐다. wavbor 팔레트(deep-purple #5600BA, purple #C15BFF)와 Pretendard도 그대로 입혔다.
뱃지 하나를 못 집던 인스펙터, Figma식 드릴다운으로 풀었다
블록 선택이 <a> 같은 상위 인터랙티브 요소에 막혀서 그 안의 뱃지나 텍스트를 선택할 수 없었다. Figma처럼, 선택된 블록 안을 다시 클릭하면 클릭 지점 방향의 직계 자식으로 한 단계씩 파고들게 했다. hover 프리뷰도 같은 로직으로 동작해서 다음 클릭이 선택할 요소를 미리 보여준다.
하이라이트가 sticky 헤더를 뚫었다 — 같은 뿌리 버그 3연전
인스펙터 하이라이트는 z-index 최대의 fixed 오버레이다. 이게 sticky/fixed 헤더를 뚫고 그려졌다. 상단에 도킹된 바를 스캔해서 하이라이트 top을 헤더 하단으로 클램프해 잡았다.
끝난 줄 알았는데 박스 몸통만 클램프됐고, 라벨(top:-24px)이 여전히 헤더를 침범했다. 클램프 시 라벨을 박스 안쪽(top:2px)에 배치해 두 번째를 잡았다.
세 번째는 클램프 조건 자체의 버그였다. 스크롤이 더 진행돼 블록 top이 음수가 되면 0~bottom 밴드 안이라는 범위 조건을 벗어나 클램프가 풀렸다. 조건을 "블록 top이 바 bottom보다 작으면 항상 클램프"로 단순화했다. 범위 조건은 스크롤이 범위를 지나치면 풀리고, 단방향 부등식이 안전했다.
한 버그를 고치니 같은 뿌리의 변종이 2~3개 더 나왔다(클램프 → 라벨 → 조건 → 비클램프 라벨). 이 로직이 커버해야 할 상태 공간을 표로 그려보고 각 칸을 E2E로 못 박는 게 재발을 막았다.
소비자가 아무것도 안 해도 되는 패키징
배포용 라이브러리 빌드는 Vite lib mode로 잡았다. ESM과 타입 선언 파일(d.ts)을 생성하고, react/@xyflow/zustand는 external로 뺐다.
문제는 CSS였다. Next.js Pages Router는 node_modules 패키지의 글로벌 CSS import를 금지한다. @xyflow CSS를 ?inline으로 번들에 포함하고 런타임에 <style>로 주입해서 우회했다. 소비하는 쪽 설정이 0이 된다.
npm pack tarball로 wavbor-next(Next 13.5 + React 18 + yarn)에 실제로 설치해 검증했다. yarn은 file: tarball을 버전으로 캐시해서, 재설치하려면 버전 범프가 필수였다.
다음 버전에서는 남은 라벨 케이스를 잡았다. 클램프 여부와 무관하게, 라벨이 올라갈 공간에 상단 바가 있으면 라벨을 박스 안쪽에 배치한다 — 요소가 헤더 바로 아래에서 시작하는 비클램프 케이스다. dist에 'use client' 배너도 추가해서 Next App Router의 RSC(React Server Components) 서버 레이아웃에서 직접 import할 수 있게 했다. daisy-monorepo platform(Next 15 App Router + React 19)에 설치해 컴파일 200과 tsc 통과를 확인했다.
CSS 인라인 주입도 'use client' 배너도 방향은 하나다. 소비 프로젝트가 아무것도 안 해도 되게 만든다. 배포 호환성은 소비자 쪽 설정을 0으로 만드는 쪽이 옳다고 판단했다.
오늘 걸려 넘어진 네 지점
부딪힌 문제와 원인, 해결을 나란히 놓고 봤다.
| 문제 | 원인 | 해결 |
|---|---|---|
| React Flow 노드 클릭 불가 | nodesDraggable=false + elementsSelectable=false면 노드에 pointer-events: none이 걸림 | .react-flow__node { pointer-events: all !important } |
| 헤더 클램프가 간헐적으로 동작 안 함 | iframe onLoad 시점엔 SPA가 아직 헤더를 렌더하기 전이라 빈 스캔 결과가 캐시됨 | 클릭 시 즉시 재스캔 + mousemove 500ms 스로틀 재스캔 |
| Next에서 라이브러리 CSS import 에러 | Pages Router는 node_modules 패키지의 글로벌 CSS import 금지 | CSS를 ?inline으로 번들에 넣고 런타임 주입 |
| App Router 서버 레이아웃에서 import 불가 | RSC 환경에서 클라이언트 컴포넌트 경계 없음 | dist에 'use client' 배너 삽입 |
iframe onLoad는 SPA의 렌더 완료를 보장하지 않았다. onLoad에서 한 번 스캔하고 캐시하는 패턴은 SPA에서 빈 결과를 캐시하는 함정이었다. 상호작용 시점 재스캔(스로틀)으로 바꾸니 SPA 라우팅으로 헤더가 바뀌는 경우까지 커버됐다.
하루에 Phase 1+2 구현, 디자인 개편, 실전 앱 두 곳 설치까지 왔다. 진도는 좋았지만 헤더 클램프 버그로 커밋 3개를 소모했다. 처음부터 "스크롤 전 구간"을 E2E 케이스로 잡았다면 1커밋으로 끝났을 것이다.
E2E 스모크를 매 커밋 회귀로 돌린 건 유효했다. 인스펙터를 고치는 동안에도 캔버스와 뷰어가 깨지지 않는다는 확신을 계속 유지할 수 있었다. 실제 소비 환경(Next 13.5 Pages Router, Next 15 App Router)에 바로 꽂아본 것도 CSS와 RSC 같은 패키징 문제를 조기에 드러냈다.
저녁에는 granularity(블록 선택 단위의 세밀도) 'all' 모드에 착수했다. 섹션/유닛에 더해 페이지 전체(body) 단위 선택 토글을 추가하는 작업이다. 다음 편에서는 이 전체 선택 모드를 마무리하는 과정부터 이어간다.