본문으로 건너뛰기

pubmap 01. IA 미니맵·블록 인스펙터 구현과 실전 배포

·10 min read·1 / 2

화면설계서(서비스의 페이지 구성과 흐름을 정의한 기획 문서)의 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) 단위 선택 토글을 추가하는 작업이다. 다음 편에서는 이 전체 선택 모드를 마무리하는 과정부터 이어간다.