자원 박스를 누르면 바로, 핀을 누르면 펼친 카드의 마지막 줄에서 상세 화면으로 넘어갑니다. 실무 화면에서는 이 클릭이 자원 상세로 이동하고, 여기서는 무엇이 넘어갔는지를 대신 적습니다.
과정은 아래 검토 기록에 있습니다 — 회전·확대·이동 · 자원 이름 배치 · SVG와 WebGL 비교 · 구획 이름 배치
클라우드 관리 플랫폼에서 VPC 자원의 관계를 보여주는 화면입니다. 표로 나열하면 어느 서브넷이 어느 라우팅 테이블을 거쳐 어느 게이트웨이로 나가는지 한눈에 따라가기 어려워서, 계층이 한 방향으로만 흐르는 도식으로 바꿨습니다.
저는 먼저 작업 계획을 세우고, 화면을 직접 써 보며 문제가 생기는 지점을 찾았습니다. 코드는 AI가 작성했고, 발견한 문제와 고칠 방향을 정리해 전달한 뒤 수정 결과를 같은 조건에서 다시 확인했습니다.
3D 는 WebGL 이 아니라 아이소메트릭 투영입니다. 좌표 계산만으로 만들어지므로 글자는 그대로 글자이고, 박스는 키보드로 이동할 수 있는 요소로 남습니다. 도식을 끌면 시점이 돌아가는데, 이것도 WebGL 없이 투영 각도 하나를 바꾸는 것입니다 — 회전 후에도 여전히 아핀 변환이기 때문입니다.
자원 이름은 박스 위에 얹지 않고 콜아웃 핀으로 띄웁니다. 라벨이 박스 평면에서 떨어져 나오므로 이름 길이가 박스 크기를 좌우하지 않고, 시점을 돌려 라벨이 겹칠 때는 정보를 버리는 대신 지지대를 늘려 비켜 세웁니다. 핀을 누르면 그 자리에서 상세 카드로 펼쳐지고, 헤더의 [핀 | 텍스트 | 가리기] 로 표시 방식을 바꿔 볼 수 있습니다.
실무 코드를 그대로 옮기지 않고, 화면에 표시되는 자원 이름·주소·식별자를 전부 가상 값으로 대체했습니다. AWS 자원 유형(VPC · 서브넷 · 라우팅 테이블 · 게이트웨이)은 공개된 개념이라 그대로 두었습니다.
자원 박스를 누르면 바로, 핀을 누르면 펼친 카드의 마지막 줄에서 상세 화면으로 넘어갑니다. 실무 화면에서는 이 클릭이 자원 상세로 이동하고, 여기서는 무엇이 넘어갔는지를 대신 적습니다.
이 도식의 회전·확대·이동은 한 번에 된 것이 하나도 없습니다. AI가 작성한 코드를 제가 직접 조작해 결함을 찾고 수정 방향을 정하는 과정을 열다섯 번 반복했습니다. 아래는 그 중 원인이 증상과 어긋나 있던 것들입니다.
끌어서 돌리면 화면이 깜빡이고 중간중간 멈칫한다
원인이 둘이었습니다. 커서는 가만히 있어도 도식이 그 아래에서 돌아가므로 새 박스가 계속 커서 밑으로 들어와 호버 강조가 프레임마다 켜졌다 꺼졌다 했고, 구역 이름이 '앞쪽' 모서리에 매달려 있어 90°마다 다른 변으로 건너뛰었습니다(측정한 점프 최대 442px, 주변은 프레임당 2px).
회전 중에는 강조를 무시하고, 각도가 연속으로 움직이는 동안만 구역 이름을 감춥니다. 한 번의 변화까지 감췄더니 방향키를 한 번 눌렀을 때 라벨이 이유 없이 깜빡였습니다 — 없애려던 증상을 조작 방식만 바꿔 되살린 셈입니다.
회전이 한 방향으로만 된다
회전 코드가 아니라 컨트롤의 문제였습니다. 슬라이더가 값을 min/max 로 잘라내므로 0°에서 왼쪽으로 밀어도 359°로 넘어가지 않습니다. 각도는 순환하는 값이라 선형 컨트롤로는 양 끝을 이을 수 없습니다.
감기는 단계 버튼(↺↻)을 함께 뒀습니다. 정규화가 음수를 반대쪽으로 이어주므로 이 버튼은 경계를 지납니다. 정규화에서는 반올림을 나머지 연산보다 먼저 해야 합니다 — 나중에 하면 359.6°가 360°로 표시되어 슬라이더 상한을 넘습니다.
회전하거나 확대할 때마다 도식이 놓이는 공간의 높이가 재설정된다
각도마다 장면의 경계 상자가 달라집니다. 같은 데이터로 894×547 ~ 994×542 로 변하고, 그 값을 캔버스 크기로 쓰면 컨테이너 높이가 598~1141px(변동폭 91%)로 움직입니다.
여러 각도를 미리 훑어 '어느 각도에서도 내용이 들어가는 크기'를 고정 창으로 잡고, 높이는 CSS 로 못 박았습니다. 이 계산이 각도에 의존하면 끌 때마다 다시 돌아 회전이 끊기므로, 데이터와 보기 방식만 보게 했습니다.
확대한 상태에서 맵을 끌면 상하로만, 그것도 아래로만 더 움직인다
가운데 정렬을 flex 로 하고 있었습니다. flex 정렬은 내용이 뷰포트보다 클 때 시작 쪽(위·왼쪽) 넘침에 스크롤이 닿지 않고, 주축인 가로는 스크롤 범위가 아예 생기지 않습니다. 증상의 두 절반이 한 원인에서 나왔습니다.
자식의 `margin: auto` 로 바꿨습니다. 들어갈 때만 가운데로 놓이고 넘칠 때는 0 이 되어 양방향 모두 닿습니다. 스크롤바를 없앤 대가로 큰 맵을 한눈에 볼 방법이 사라져 맞춤 버튼도 이때 붙였습니다.
좌우 이동은 여전히 되지 않는다
이 컴포넌트는 확장 테이블 행 안에 들어갑니다. 표 셀은 내용의 max-content 폭으로 늘어나므로 뷰포트가 도식 폭만큼 넓어져 가로 넘침 자체가 사라집니다. 세로는 높이를 고정해 둬서 영향이 없었고, 그래서 '위아래만 움직인다'로 보였습니다.
`contain: inline-size` 로 인라인 크기를 내용과 무관하게 만듭니다. 컴포넌트를 어디에 넣는지가 컴포넌트의 동작을 바꾼 경우라, 격리해서 보면 절대 나오지 않는 종류의 문제였습니다.
맵마다 100% 의 기준이 다르다 — 어떤 맵은 150%부터 200%까지 배율이 그대로다
패치로 넘기지 말고 근본적으로 해결하도록 방향을 잡아, 100% 의 뜻을 먼저 정했습니다. 맵마다 100% 를 다시 정의하면 맵마다 노드 크기가 달라지고, Figma 처럼 언제나 1:1 로 두면 노드 크기는 같은 대신 필요한 배율 범위가 맵마다 달라집니다. 후자를 택했습니다.
그러면 한계를 고정된 한 쌍의 숫자로 둘 수 없습니다. 하한을 맞춤 배율에 연동했습니다 — 하한이 맞춤보다 높으면 큰 맵은 전체를 볼 방법이 아예 없습니다. 범위가 10~400% 로 넓어지자 끌기 감도가 무의미해져(1px 당 일정한 %p 를 더하면 400% 부근에서는 티가 안 나고 10% 부근에서는 두 배씩 튄다) 덧셈을 곱셈으로 바꿨습니다.
첫 번째는 "100% 를 넘겨 확대하면 도식은 커지지 않고 공간의 높이만 길어진다"였습니다. 배율이 장면의 격자 단위에 곱해지고 있었고, 글자 크기는 CSS 로 고정이라 함께 커지지 않으니 라벨에서 역산하는 노드 폭이 배율마다 달라졌습니다. 배율을 레이아웃에서 뷰포트로 옮겼습니다 — viewBox 는 그대로 두고 width·height 에만 곱하면 글자까지 함께 커집니다. 진단은 옳았고, 증상은 남았습니다.
두 번째는 AI가 격리된 하네스에서 배율이 멀쩡히 동작하는 것을 보고 "앱이 옛 코드를 서빙하는 것 같다"고 결론 낸 것입니다. 제가 실제 화면에서 찍은 스크린샷 한 장이 그 결론을 곧바로 반증했습니다. 13서브넷 맵이 400% 라고 표시된 채 전체가 화면에 들어와 있었습니다.
원인을 특정한 단서는 "둘 다 400% 인데 노드 크기가 확연히 다르다"는 제 관찰이었습니다. 배율이 안 걷히는 것이 아니라 맵에 따라 다르다면, 화면 크기에 의존하는 무언가가 끼어든다는 뜻입니다. 실제 화면에서 재보니 width 속성은 1198.96px 인데 실제 렌더 폭이 903px 이었습니다. 전역 CSS 리셋의 `svg { max-width: 100% }` 가 범인이었습니다. 이미지가 부모를 넘치지 않게 하려는 통상적인 규칙이지만, 배율을 width 로 주는 이 도식에서는 지정한 폭이 뷰포트를 넘는 순간 SVG 가 눌리고 viewBox 가 비율을 지켜 내용이 통째로 축소됩니다.
그리고 그 리셋에는 max-height 가 없습니다. 폭만 눌리고 높이는 계속 자랐습니다 — 여덟 라운드 전의 "확대는 안 되고 높이만 길어진다"가 바로 이것이었습니다. 고침은 이 도식에만 `max-width: none` 을 돌려주는 한 줄이고, 공유 리셋은 건드리지 않았습니다.
| 맵 | viewBox 폭 | 100% | 200% | 400% |
|---|---|---|---|---|
| 서브넷 4 · 라우팅 테이블 3 | 1199 | 132.5 | 265.0 | 530.0 |
| 서브넷 13 · 라우팅 테이블 3 | 1986 | 132.5 | 265.0 | 530.0 |
이 페이지의 도식에 그대로 들어 있습니다 — Space + 휠로 커서 밑 지점을 붙든 채 확대하고, Space + 드래그로 맵 안을 옮기고, 배율 값을 눌러 전체를 봅니다. 전체 보기는 처음에 [맞춤] 버튼으로 붙였는데, 값 손잡이 바로 옆이라 같은 일을 하는 것처럼 보여 손잡이 누르기로 합쳤습니다. 100% 는 정지 배율에 있어 [+] [−] 로도 닿지만 맞춤은 맵마다 달라지는 계산값이라 다른 길이 없어서, 손잡이에 남길 값으로는 맞춤을 택했습니다. 남은 것도 있습니다. 스크롤바를 없앤 탓에 내용이 캔버스를 넘쳤는지 화면에 표시되지 않아, 넘친 방향에 단서가 필요합니다.
자원 이름을 박스 윗면에 얹는 방식은 처음부터 어긋나 있었습니다. 이름이 길수록 박스를 넓혀야 하고, 마름모 윗면에서는 글자를 가운데 두어도 한쪽으로 쏠려 보이고, 시점을 돌리면 라벨끼리 겹칩니다. 라벨을 박스 평면에서 떼어 내 지지대 끝 칩에 담는 것으로 세 가지를 한 번에 풀었고, 그 뒤로 열두 번을 더 돌았습니다. 아래 목록은 순서가 곧 시간 순인데, 뒤로 갈수록 앞의 고침이 불러온 결함이 늘어납니다 — 핀을 얹느라 먼저 있던 규칙에 구멍이 나고, 그 구멍을 막다가 다시 순서가 뒤집히는 식입니다. 결함은 모두 제가 화면을 직접 써 보며 찾았고, 고칠 방향을 정해 AI에게 수정을 맡겼습니다.
박스 위에 얹은 이름이 어색하다
라벨이 박스 평면에 묶여 있는 것이 원인이었습니다. 그러면 박스가 라벨 폭을 떠안아 자원의 크기가 이름 길이를 따라 변하고, 마름모 윗면이라 좌우 여백이 비대칭으로 읽히고, 회전하면 행 사이 세로 엇갈림이 줄어 라벨이 겹칩니다. 당시 겹침 회피는 보조 라벨을 통째로 감추는 것이었는데, 정보를 버리는 회피입니다.
번호 핀·아이콘 핀·콜아웃 핀 세 가지를 비교해 콜아웃 핀으로 정했습니다. 박스 윗면 중심에서 짧은 지지대를 세우고 그 끝에 화면 수평 칩을 답니다. 겹침은 정보를 버리는 대신 지지대를 늘려 풉니다 — 앞(화면 아래) 자원부터 자리를 잡고 겹치는 핀만 한 단계씩 올립니다. 뒤쪽 자원의 이름표가 더 높이 뜨는 편이 깊이감과 어긋나지 않기 때문입니다.
칩에 이름 한 줄뿐이라 결국 표나 상세 화면으로 건너가야 한다
어느 가용영역에 있는지, 무엇에 붙어 있는지는 칩 한 줄에 들어가지 않습니다. 그런데 그것을 보려고 화면을 통째로 옮기는 것은 과합니다.
핀을 누르면 그 자리에서 카드로 펼쳐 그만큼을 먼저 보여주고, 더 볼 것이 있으면 상세 화면으로 잇습니다. 한 번에 하나만 펼칩니다 — 여러 장이 뜨면 카드끼리 겹쳐 도식을 덮고, 어느 것을 닫을지도 매번 판단해야 합니다. 카드에 무엇을 넣을지는 도메인을 아는 어댑터가 정하고, 렌더러는 라벨과 값 문자열만 그립니다.
카드가 맵 안에 다 들어오지 않는다
카드 기하를 도식과 같은 좌표계로 계산하고 있었습니다. 배율은 도식 전체에 걸리므로 카드도 함께 커집니다. 400% 에서는 카드 하나가 캔버스보다 넓어져 어떻게 밀어 넣어도 잘립니다.
칩은 도식의 일부라 함께 커지는 것이 맞지만, 카드는 도식 위에 뜬 UI 입니다. 카드 기하를 화면 픽셀로 계산하고 앵커에 배율의 역수를 걸어 되돌렸습니다. 여기에 더해, 카드를 도식 프레임 안으로만 밀어 넣는 것으로는 모자랍니다 — 배율이 맞춤보다 크면 프레임 안이어도 스크롤 밖이면 잘려 보입니다. 지금 실제로 보이는 범위를 재서 그 안에 앉힙니다.
낮은 배율에서 카드 위쪽이 잘린다
SVG 는 기본적으로 자기 viewBox 밖을 잘라냅니다. 맞춤 배율은 대개 가로에 걸리므로 SVG 는 캔버스보다 한참 낮아진 채 세로 가운데에 놓이고, 그 위아래로 남는 빈 캔버스는 화면에는 보이지만 SVG 밖입니다. 카드가 그리로 올라가면 절반이 사라집니다.
도식 SVG 에만 잘라내기를 끕니다. 넘친 것은 그 바깥 캔버스가 대신 잘라 주므로 경계는 그대로입니다. 도식 자체는 프레임을 내용에서 역산해 만들기 때문에 밖으로 나갈 것이 없고, 칩도 프레임 안으로 밀어 넣습니다 — 이 규칙이 풀어 주는 것은 카드뿐입니다.
가장자리 자원의 카드가 맵 테두리와 맞닿는다
카드를 창 안으로 밀어 넣는 여백이 2px 이었습니다. 0 에 가까우면 카드가 맵 위에 뜬 것이 아니라 테두리에 물려 잘린 것처럼 보입니다. 실제로 잘리지 않아도 눈에는 같은 인상입니다.
네 변 모두 12px 을 남깁니다. 카드 크기도 함께 키웠습니다.
자원 유형마다 카드 너비가 달라진다
가장 긴 줄을 재서 폭을 정하고 있었습니다. 그러면 서브넷·라우팅 테이블·네트워크 자원이 저마다 다른 폭으로 떠서, 핀을 옮겨 볼 때마다 카드가 늘었다 줄었다 합니다.
한 번에 하나만 뜨는 카드라 지면을 아낄 이유가 없습니다. 폭을 내용과 무관한 고정값으로 두고, 항목 수가 가장 많은 유형(서브넷 4줄)에 맞춰 높이 바닥을 깔았습니다. 상세로 가는 줄은 마지막 항목이 아니라 카드 바닥에 고정합니다 — 유형을 오갈 때 이 줄이 위아래로 뛰지 않습니다. 결과는 어느 자원을 눌러도 같은 크기입니다.
핀과 텍스트를 오가면 보던 자리의 높이가 달라진다
제가 화면에서 짚은 원인이 맞았습니다. 칩은 앵커보다 위에 서므로 도식만 감싼 프레임으로는 맨 뒷줄 자원의 칩이 잘립니다. 그래서 캔버스 위쪽에 여백을 더 붙이고 있었는데, 핀일 때만 붙였습니다. 텍스트로 바꾸는 순간 그 여백이 사라지면서 도식이 그만큼 위로 뜁니다.
핀을 기준으로 두고 라벨만 갈아 끼웁니다. 여백은 표시 방식과 상관없이 항상 적용합니다. 셋 중 가장 넓은 핀 기준이라 텍스트·가리기에서는 여백이 조금 남지만, 보던 자리가 어긋나는 것보다 낫습니다. 여백을 실제 핀 높이로 재지 않고 회전각과 무관한 상수로 둔 것도 같은 이유입니다 — 각도마다 캔버스가 달라지면 돌리는 동안 도식이 출렁입니다.
평면도에서도 핀이 선다
핀이 풀어 주는 문제 — 마름모 윗면에서 글자가 쏠려 보이는 것, 박스 폭이 이름 길이를 떠안는 것 — 가 2D 에는 없습니다. 박스가 반듯한 사각형이라 글자가 그 안에 그대로 들어가고, 오히려 핀을 세우면 칩이 박스 위를 덮어 더 어수선합니다.
평면도는 늘 텍스트로 그립니다. 고른 값은 그대로 두어 3D 로 돌아가면 핀이 되살아납니다. 선택지에서는 핀만 빼고 가리기는 남깁니다 — 가리기까지 감추면 2D 에서 라벨을 끌 방법이 사라집니다.
평면도에서 이름이 도형 밖으로 넘친다
박스 폭(82px)이 가장 긴 이름(88px)보다 좁았습니다. 3D 는 윗면이 마름모라 넘친 글자가 옆면 위로 흘러도 덜 드러나지만, 2D 는 반듯한 사각형이라 글자가 변을 넘는 것이 그대로 보입니다. 말줄임 기준이 글자 수(16자)였던 것도 함께 걸렸습니다 — 글자 수로 자르면 라틴 이름은 자리가 남는데도 잘리고 한글 이름은 같은 글자 수여도 두 배 가까이 넓어 넘칩니다.
박스를 116px 로 넓히고 말줄임을 폭 기준으로 바꿨습니다. 쓸 수 있는 폭은 레이아웃이 노드에 실어 보냅니다 — 렌더러는 박스의 그리드 치수를 모르고 완성된 경로만 받으므로 스스로 계산할 수 없습니다. 폭을 정한 기준은 실측이 아니라 말줄임이 쓰는 추정식입니다. 추정은 라틴을 글자 크기의 0.58 배로 보는데 실제 렌더는 0.49 배라 19% 과대평가하므로, 실측에 맞춰 잡으면 잘릴 이유가 없는 이름이 잘립니다. 3D·2D 가 같은 그리드를 공유하므로 한쪽만 넓힐 수는 없는데, 핀 모드에서는 칩이 박스 크기와 무관하니 이 값이 이름을 좌우하지는 않습니다.
WebGL 렌더러에는 핀이 없다
핀 배치를 SVG 렌더러 안에서 계산하고 있었습니다. 렌더러 토글은 같은 장면을 두 방식으로 그려 비교하자는 것인데, 한쪽만 핀이고 다른 쪽은 박스 위 텍스트면 비교가 아니라 딴 물건을 보는 셈입니다.
핀 계산과 캔버스 여백을 래퍼로 올려 두 렌더러가 같은 값을 받습니다. 고친 뒤 두 렌더러에서 같은 칩의 화면 위치를 재면 (458.8, 116.4) 로 같습니다. WebGL 에서 핀만 오버레이로 그리는 이유가 있습니다 — 칩의 세로 위치는 이미 놓인 칩과의 겹침에 따라 정해지므로 투영 결과를 보고서야 알 수 있고, 3D 점에 매다는 오버레이로는 그 조정을 표현할 수 없습니다. 여백을 렌더러 안에서 붙이고 있던 것도 이때 함께 고쳤습니다 — SVG 판만 넓어져 WebGL 로 토글하면 도식이 그 차이만큼 밀리고 있었습니다.
WebGL 에서 확대하면 핀이 떨리고 제자리를 잃는다
React 는 배율이 바뀌면 컨테이너 크기와 핀 층의 확대율을 렌더 시점에 곧바로 반영하는데, WebGL 캔버스 크기는 effect 안에서 정하고 있었습니다. 그 effect 는 화면을 그린 뒤에 돌므로 한 프레임 동안 캔버스만 옛 크기로 남습니다. 한 번이면 눈에 띄지 않지만 휠이나 값 끌기처럼 배율이 연속으로 바뀌면 매 프레임 어긋나, 도식이 떨리고 핀이 제자리를 잃은 것처럼 보입니다. 정지 상태에서 재면 400% 까지도 정확히 맞아떨어져 더 찾기 어려웠습니다.
캔버스 크기와 카메라를 그리기 전 단계에서 맞춥니다. WebGL 렌더는 동기라 이 단계에서 끝나므로 어긋난 프레임 자체가 생기지 않습니다. 함께 걸린 것이 하나 더 있었습니다 — 렌더 버퍼는 배율의 제곱으로 커져서 레티나 400% 에서는 5천만 픽셀이 됩니다. 배율이 바뀔 때마다 그만한 버퍼를 다시 잡느라 확대가 끊기므로, 상한에 닿으면 화면 배속을 낮춰 1600만 픽셀 안에서 잡습니다(400% 에서 5100만 → 1600만). 값 손잡이를 끌어 배율을 연속으로 바꾸면서 재면 캔버스와 핀 층의 폭·좌표 차이가 모두 0 입니다. 다만 이 고침이 곧바로 다음 문제를 만들었습니다.
WebGL 로 바꾸면 도식은 없고 핀만 떠 있다
바로 위 고침의 부작용입니다. 렌더러 생성과 지오메트리 구성은 그리기 뒤 단계에, 크기·카메라만 그리기 전 단계에 두었더니 실행 순서가 적어 둔 순서를 따르지 않았습니다. 전 단계가 통째로 먼저 돌기 때문에, 첫 진입에서 크기·카메라가 렌더러보다 먼저 실행되어 아직 아무것도 없는 상태로 건너뜁니다. 의존하는 값이 그대로라 다시 불리지도 않아 캔버스가 빈 채로 남습니다.
세 단계를 같은 종류로 맞췄습니다. 한 단계만 앞당기면 나머지와의 순서가 뒤집힌다는 것이 요점입니다 — 순서는 적어 둔 차례가 아니라 단계별로 정해집니다.
Space + 드래그로 맵을 옮길 수 없다
핀 칩을 눌러 상세로 가게 하려고 칩에 포인터를 받도록 해 두었는데, 그 규칙이 더 구체적이라 Space 를 쥐는 동안 도식 전체의 포인터를 끄는 규칙을 이깁니다. 도식이 핀으로 덮여 있으니 드래그를 핀 위에서 시작하기 쉽고, 그러면 이동 대신 시점 회전이 걸립니다.
포인터를 끌 때 자손까지 함께 끕니다. 핀 칩 위에서 Space + 드래그를 시작해도 스크롤만 (57.5, 28.5) → (115, 0) 으로 움직이고 시점은 45° 그대로입니다. 이동이 회전보다 먼저 잡혀야 한다는 규칙은 원래 있었는데, 나중에 얹은 핀이 그 규칙에 구멍을 낸 경우입니다.
핀을 붙이면서 두 가지가 함께 깨졌습니다. 자원 박스를 눌러도 상세로 가지 않고, 칩을 눌러도 카드가 어쩌다 한 번만 열립니다. 처음에 AI는 회전 드래그 직후의 클릭을 삼키는 표식이 풀리지 않은 채 남는 경로를 의심해, 새 제스처가 시작될 때 표식을 만료시키도록 고쳤습니다. 그리고 그 수정을 스크립트로 만든 클릭 이벤트로 검증했습니다. 통과했습니다.
제가 실제 마우스로 눌러 보니 증상은 그대로였습니다. 진짜 원인은 회전 드래그를 시작하는 코드가 pointerdown 시점에 포인터를 잡고 있던 것이었습니다. 포인터가 잡혀 있으면 브라우저는 뒤따르는 마우스 호환 이벤트 — mousedown, mouseup, 그리고 click — 을 원래 눌린 요소가 아니라 잡은 요소로 보냅니다. 여기서 잡는 것은 도식 SVG 이므로, 박스를 눌러도 클릭은 박스가 아니라 SVG 에 떨어집니다. 노드의 클릭 처리기가 아예 돌지 않습니다.
"어쩌다 한 번" 열리던 것도 이것으로 설명됩니다. 포인터를 놓는 시점과 브라우저가 click 을 합성하는 시점이 경쟁하므로, 가끔 풀린 뒤에 합성되면 통과합니다. 제가 테스트하며 전달한 "기준을 전혀 모르겠다"는 증상 설명 자체가 경쟁 조건의 모습이었습니다.
그리고 왜 틀린 진단이 검증을 통과했는지도 여기서 드러납니다. 스크립트가 직접 던진 클릭은 포인터 캡처를 거치지 않습니다. 캡처 리타기팅은 브라우저가 실제 입력에서 호환 이벤트를 합성할 때 적용되는 규칙이라, 만들어 낸 이벤트에는 해당이 없습니다. 그래서 합성 클릭은 언제나 통과하고 진짜 마우스는 언제나 실패합니다. 고침은 잡는 시점을 옮기는 것이었습니다 — 잡기가 필요한 이유는 도식 밖으로 끌고 나가도 회전이 이어지게 하려는 것뿐이므로, 실제로 끌기 시작한 뒤에 잡으면 됩니다. 누르고 떼기만 한 제스처는 잡기를 거치지 않으므로 클릭이 제 요소로 갑니다.
이 포트폴리오로 옮기면서도 같은 자리를 다시 밟았습니다. 카드 잘림을 막는 잘라내기 해제를 빠뜨린 채로 옮겼더니, 33% 로 축소한 순간 카드 오른쪽 절반이 사라졌습니다. 원인을 이미 알고 있었는데도 이식 목록에서 빠지면 그대로 재현됩니다.
| 표시 방식 | 캔버스(viewBox) | 첫 노드 화면 위치 | 핀 | 박스 텍스트 |
|---|---|---|---|---|
| 핀 | -477.91 -137.62 1002.92 652.04 | (648.7, 289.5) | 11 | 0 |
| 텍스트 | 동일 | (648.7, 289.5) | 0 | 11 |
| 가리기 | 동일 | (648.7, 289.5) | 0 | 0 |
| 핀 복귀 | 동일 | (648.7, 289.5) | 11 | 0 |
이 기록을 지나며 검증 방식도 바뀌었습니다. 처음에 AI는 스크립트로 만든 클릭으로 확인했지만 그것이 브라우저 규칙을 건너뛴다는 것이 드러났고, 다음에는 실제 포인터로 상태를 하나씩 찍어 쟀지만 그것이 전환 중에만 어긋나는 결함을 놓쳤습니다. 지금은 실제 입력으로, 값이 바뀌는 동안 계속 재는 방식입니다. 그래도 결함을 먼저 찾은 것은 매번 제가 화면을 직접 써 본 테스트였습니다 — 측정은 원인을 좁히는 데 쓰였지 결함을 찾아 주지는 않았습니다. 지금 위 도식에서 [핀 | 텍스트 | 가리기] 를 바꿔 볼 수 있습니다. 남은 것도 있습니다. 지지대를 네 단계까지 올려도 칩이 다 풀리지 않는 각도가 있어, 근본적으로는 각도마다 다시 푸는 대신 배치를 전역으로 최적화해야 합니다. 그리고 가리기는 지금 자원 이름만 지우고 바닥에 새겨진 구획 이름은 남깁니다 — 이 토글이 "자원 이름을 어디에 두는가"를 고르는 자리라 그 범위로 잡았고, 구획 이름까지 가릴지는 아직 정하지 않았습니다.
같은 어댑터가 만든 하나의 배치를 두 렌더러로 그렸습니다. 화면 위 토글이 그 둘을 바꿉니다. 카메라를 SVG 투영과 같아지도록 유도해서, 두 렌더러는 콘텐츠를 같은 픽셀에 올립니다 — 노드 11개의 가로 위치 차이가 0.00px 입니다.
| 항목 | SVG | WebGL |
|---|---|---|
| 번들 | 0KB. 좌표 계산뿐이라 라이브러리가 없습니다. | gzip 121KB. 토글을 누를 때만 내려오도록 지연 로드했습니다. 이 페이지의 초기 스크립트는 187KB 이므로, 즉시 로드하면 65% 가 늘어납니다. |
| 글자 | 전부 <text> 입니다. 선택·검색·스크린리더가 그대로 닿습니다. | 박스 이름은 HTML 오버레이로, 콜아웃 핀은 캔버스 위에 겹친 SVG 오버레이로 남겨 글자로 지켰습니다. 핀만 방식이 다른 이유는 칩의 세로 위치가 투영 결과를 보고서야 정해져 3D 점에 매달 수 없기 때문입니다. 하지만 바닥에 눕는 구획 이름은 오버레이로 안 됩니다 — 오버레이는 항상 카메라를 정면으로 보기 때문입니다. 캔버스에 그려 텍스처로 올렸고, 그 순간 글자가 아니게 됩니다. |
| 선 굵기 | 구획 레일은 stroke-width 한 줄입니다. | WebGL 에는 선 굵기가 없습니다. linewidth 는 대부분의 플랫폼에서 무시되고 1px 로 그려집니다. 5px 레일을 내려면 폴리라인을 따라 띠 메시를 직접 만들어야 합니다. |
| 같은 평면 겹침 | 깊이 버퍼가 없습니다. 구획 레일과 바닥 이름이 바닥판과 같은 평면에 있어도 그린 순서대로 얹힙니다. | 깊이 버퍼가 판정합니다. 같은 평면이면 z-fighting 이 나서 시야각에 따라 레일과 글자가 얼룩덜룩 깨지거나 사라집니다. 실제로 겪었고, 데칼을 살짝 띄우고 polygonOffset 을 줘서 고쳤습니다. |
| 클릭·호버 | DOM 이벤트입니다. 키보드 포커스도 공짜입니다. | 광선을 쏴서 맞은 물체를 찾아야 합니다(레이캐스터). 키보드 접근은 따로 만들어야 합니다. |
| 시점 회전 | 투영식의 각도 하나입니다. | 카메라 방위각 하나입니다. 여기서는 두 방식의 비용이 같습니다. |
| 화면 개수 | 제한이 없습니다. | 브라우저의 WebGL 컨텍스트 상한(대개 16개)에 걸립니다. 목록에 관계도를 여러 개 놓는 화면이라면 이것이 곧 한계가 됩니다. |
만들어 보고 나서도 결론은 원본 문서와 같습니다 — 이 규모의 위젯에는 SVG 가 맞습니다. WebGL 이 값을 하는 지점은 원근, 조명, 수천 개의 인스턴스처럼 아핀 변환으로는 안 되는 것들인데, 자원 관계도에는 그 중 아무것도 필요하지 않습니다. 달라진 것은 근거입니다. 처음에는 표로 적은 예상이었고, 지금은 같은 도식을 두 번 만들어 본 실측입니다. 그리고 얻은 것도 있습니다: 한글을 WebGL 에 올리는 방법이 글리프 아틀라스만은 아니라는 것, 그리고 아이소메트릭 투영이 직교 카메라의 특수한 경우라는 것을 수식으로 확인했습니다(고각 35.264°, 오차 1e-13 px).
구획 레일은 모서리를 둥글립니다 — 바닥에 그려진 것과 위에 떠 있는 것을 모양으로 구분하기 위해서입니다. 그런데 이름을 모서리 꼭짓점에 맞추자 어색해졌습니다. 레일이 꼭짓점에 닿지 않기 때문입니다. 네 가지를 거쳐 다섯 번째로 갔습니다.
레일은 꼭짓점에 닿기 전부터 휩니다. 탭의 왼쪽이 곡선 위에 얹히고 그 끝은 레일이 없는 자리로 나갑니다. 처음 상태입니다.
라운딩을 없애면 꼭짓점이 실제로 생겨 어긋남이 사라집니다. 대신 바닥과 볼륨을 모양으로 구분하던 신호를 잃습니다.
곡선을 피해 가운데로 옮기고 레일보다 높은 판으로 덮었습니다. 그런데 판 폭이 글자 수에 비례해 늘어납니다 — 이름이 길면 변을 거의 다 채우는 덩어리가 되고, 그러면 가운데 정렬도 포기해야 합니다.
판을 없애니 훨씬 가볍습니다. 다만 외곽선이 글자 모양을 따라가므로 획 사이 틈으로 레일이 비치고, 덮는 색이 뒤에 무엇이 있는지에 의존합니다.
가리는 대신 윤곽선 path 자체를 이름 자리만큼 비웠습니다. 가릴 것이 없으니 비침도 배경 의존도 생기지 않고, 판이 없으니 이름이 길어져도 빈 구간만 늘어납니다.
덮어서 해결하려는 동안은 계속 새 문제가 생겼습니다. 판은 글자 수에 끌려다니고, 외곽선은 글자 모양에 끌려다니고, 둘 다 뒤에 깔린 색을 알아야 했습니다. 가리는 것을 그만두고 선을 끊자 세 가지가 함께 사라졌습니다. 대신 구획 여백을 0.55에서 0.8로 넓혔습니다 — 이름이 들어갈 빈 구간이 모서리 곡선을 침범하지 않으려면 변이 이름보다 충분히 길어야 하기 때문입니다. 배치 규칙이 레이아웃 상수를 되돌아 바꾼 셈입니다.