AI

도식 3개가 18×18px 로 찌그러졌다 — 규칙을 어디에 적어야 안 틀리나

한 줄로

규칙은 문서에 잘 쓰는 것보다, 코드가 강제하게 만드는 편이 확실하다.

이 블로그 도식 182개로 확인한 결과다.

강제 방식 위반
CSS 가 강제 0건
좌표 코드가 계산 0건
래퍼 문서에만 적음 3건

새어 나간 3개가 전부 “문서에만 적어둔” 항목이었다.

도식 규칙이 놓인 세 층 색은 CSS 가, 좌표는 코드가 자동으로 강제한다. 가운데 규칙 문서 층만 강제력이 없고, 실제로 새어 나간 도식 3개가 전부 이 층의 항목이었다. 1층 · CSS 계약 — 자동 _sass/diagram.scss 테마 토큰으로 칠함 색을 파일에 못 박음 위반 0건 2층 · 규칙 문서 — 읽어야 함 SKILL.md .diagram 으로 감싼다 항목을 합치지 않는다 다수인 표시는 안 단다 3층 · 결정론적 계산 — 자동 gen.py 좌표를 계산함 seq=True 없으면 안 이음 없는 순서 방지

도식 163개 중 3개가 안 보였다

사이트를 훑다가 도커 글에서 도식이 있어야 할 자리에 점 하나가 찍혀 있는 걸 봤다.
개발자 도구로 재보니 18×18 픽셀이었다.

원인은 래퍼 한 겹이었다. 이 블로그의 도식은 인라인 SVG(HTML 에 직접 심는 벡터 그림)이고,
색과 크기 규칙은 전부 _sass/diagram.scss.diagram 하위 선택자로 걸려 있다.
{% include %} 만 쓰고 감싸지 않으면 그 규칙이 하나도 안 붙는다.
대신 _sass/base.scss 51 번 줄의 인라인 아이콘용 규칙이 그대로 적용된다.

svg { width:18px; height:18px; fill:currentColor; display:block; }

_sass/base.scss:51 — 아이콘용 규칙인데, 감싸지 않은 도식까지 여기에 걸린다

아이콘 크기로 그려진 것이다. 전수 조사 결과는 이랬다.

대상 (도식 163개 중 아래 3개만 빠져 있었다)
- _pages/Docker/Text_Book_4.md              docker5--tag-not-copy
- _pages/Docker/Text_Book_5.md              docker6--mount-shadow
- _pages/Kubernetes/non-majors-Kubernetes-2.md  k8s--deploy-service

실측 (고치기 전 -> 고친 뒤)
- 크기   18x18        -> 736x307
- fill   #1f2328(검정) -> 테마 토큰 (라이트 #ffffff / 다크 #0d1117)

커밋 30f4ae4 본문

163 개 중 3 개, 1.8%. 낮아 보이지만 문제는 비율이 아니라 성질이다.
이 오류는 빌드를 깨뜨리지 않는다. Jekyll 은 정상 종료하고, HTML 도 유효하고,
CI 도 초록불이다. 눈으로 보기 전까지 아무도 모른다.

첫 설계 — 색을 파일에서 빼 CSS 로 옮긴다

이 문제의 뿌리는 3 주 전에 있었다. 처음엔 도식을 <img> 태그로 넣었는데,
테마를 어둡게 바꿔도 도식만 흰 배경 그대로였다.

<img> 로 넣은 SVG 는 페이지 안의 상태를 알 수 없다. SVG 가 볼 수 있는 건
prefers-color-scheme(OS 설정)뿐인데, 이 블로그의 테마 버튼은
body.dark-theme 클래스로 동작한다. 둘이 어긋나 OS 가 라이트면
페이지가 어두워도 도식은 계속 흰색이었다.

대안은 세 갈래였다.

방식 장점 버린 이유
SVG 안에 @media (prefers-color-scheme) 파일 하나로 끝 OS 설정만 보므로 테마 버튼과 계속 어긋난다. 원인이 안 없어진다
라이트·다크 SVG 를 2 장씩 확실함 도식이 곧 100 개를 넘을 참이었다. 두 장의 동기화가 새 문제가 된다
인라인 SVG + CSS 토큰 페이지 CSS 를 상속받아 테마와 항상 같이 움직인다 채택

인라인으로 옮기면서 SVG 안의 <style> 을 전부 걷어내고 클래스만 남겼다.
색은 여기서 준다.

// 글 안에 들어가는 설명 도식(SVG).
//
// <img> 로 넣으면 SVG 안에서 페이지의 다크 여부를 알 수 없다. SVG 는
// prefers-color-scheme(OS 설정)만 볼 수 있는데, 이 블로그의 테마 버튼은
// body.dark-theme 클래스로 동작하므로 둘이 어긋난다 — 어두운 페이지에
// 흰 도식이 뜨는 문제가 실제로 있었다.
//
// 그래서 SVG 를 페이지에 그대로 심고(인라인), 색은 여기서 준다.
.diagram {
  margin: 1.75rem 0;
  overflow-x: auto;          // 좁은 화면에서 도식만 가로 스크롤

  svg {
    display: block;
    width: 100%;
    height: auto;
    max-width: 46rem;
    margin: 0 auto;
  }

  .dg-row, .dg-item { fill: var(--bg); stroke: var(--border); }
  .dg-t, .dg-m      { fill: var(--fg); }
  .dg-s             { fill: var(--fg-subtle); }
  .dg-n             { fill: var(--accent); }
}

_sass/diagram.scss — 전체 정의는 파일 참조

전환 비용은 작지 않았다. 477 줄을 지우고 344 줄을 새로 썼다. 사실상 전량 폐기다.
이때 클래스에 dg- 접두사를 붙이고, <marker> id 에 파일명을 붙였다 —
한 페이지에 도식이 여러 개 들어가면 같은 id 가 겹쳐 화살표가 엉킨다.

여기까지가 1 층: CSS 가 계약을 강제하는 층이다. 색을 파일에 박을 수 없게 만들었다.
이후 이 규칙을 어긴 도식은 0 건이다.

그런데 “감싸는 것”은 CSS 가 강제하지 못한다

감싸지 않으면 규칙이 안 붙을 뿐, 아무도 오류를 내지 않는다.
18 일 뒤 도커·쿠버네티스 글에서 정확히 그 3 개가 새어 나갔다.

그래서 2 층: 규칙을 스킬 파일에 적는 층을 만들었다.
.claude/skills/blog-diagram/SKILL.md 의 맨 앞은 이렇게 시작한다.

## 두 가지 필수 조건

이 둘 중 하나만 빠져도 도식이 깨진다. 먼저 확인한다.

1. **`.diagram` 으로 감싼다.** `_sass/diagram.scss` 의 색 규칙이 전부
   `.diagram` 하위 선택자다. `{% include %}` 만 쓰면 스타일이 안 붙어
   검은 도형이 된다.

2. **SVG 안에 색을 쓰지 않는다.** `fill="#fff"`, `stroke="#333"` 를 쓰면
   다크모드에서 그대로 남는다.

.claude/skills/blog-diagram/SKILL.md

핵심은 “먼저 확인한다”가 문서 최상단에 있다는 점이다.
규칙 목록의 27 번째 항목이 아니라 첫 줄이다.

규칙은 한 번에 완성되지 않았다

스킬을 만든 날, 백엔드 로드맵 글의 도식 7 개를 생성했다. 결과물에서 두 가지가 어긋났다.

첫째, 항목이 합쳐졌다. 원본에 MySQL·MariaDB·SQLite 세 상자가 있는데
도식에는 MySQL · MariaDB · SQLite 한 상자로 들어갔다. 폭은 줄지만
읽는 사람은 그게 세 갈래 선택지인지 알 수 없다.

둘째, 표시가 화면 절반을 덮었다. 노드 132 개 중 72 개가 ‘추천’인데
전부 달아놓으니 같은 글자가 반복돼 아무것도 구분되지 않았다.

개별 결과물만 고치면 다음 도식에서 다시 나온다. 그래서 결과물과 함께 규칙을 고쳤다.

+### 원본의 항목을 합치지 않는다
+
+원본에 상자가 3개면 도식에도 상자가 3개다. `MySQL · MariaDB · SQLite` 처럼
+가운뎃점으로 묶으면 폭은 줄지만 **원본에 없는 "한 덩어리"를 만들어낸다.**
+
+`wrap_rows` 가 폭을 넘으면 알아서 다음 줄로 접으므로 합칠 이유가 없다.
+세로로 길어지는 것은 문제가 아니다 — 도식 하나가 720×780 까지는 잘 읽힌다.
+
+### 다수인 표시는 달지 않는다
+
+표시(`s`)는 **예외를 가리킬 때만** 정보가 된다. 로드맵 132개 중 72개가 '추천'인데
+그걸 전부 달면 화면의 절반이 같은 글자로 덮여 아무것도 구분되지 않는다.
+소수인 '대안'·'순서 무관'만 달고, 다수는 표시 없음이 기본값이라고 본문에서 밝힌다.

SKILL.md, 커밋 5fbe636dd6d3cd (+15 줄)

주목할 점은 금지만 적지 않았다는 것이다.
“합치지 마라” 뒤에 “wrap_rows 가 알아서 접으니 합칠 이유가 없다”를 붙였다.
합치는 행동은 폭이 넘칠까 봐 나오는 것이므로,
그 걱정이 근거 없음을 같이 적어야 규칙이 지켜진다.

결과가 같은 커밋의 나머지다. 생성 스크립트 217 줄 재작성, 도식 7 개 재생성,
diff 합계 +537 / −310.

3 층 — 판단과 계산을 분리한다

세 번째 설계는 좌표를 아예 맡기지 않는 것이다.
도식이 여러 개면 여백과 노드 높이가 미세하게 어긋난다.
그래서 gen.py 가 존·노드 목록을 받아 좌표를 계산한다.

def wrap_rows(items, avail_w):
    """노드를 가로로 채우다 폭이 넘치면 다음 줄로 접는다."""

def node_width(label, sub):
    """글자 수로 대략의 폭을 잡는다. 한글은 넓게 친다."""
    def vis(s):
        return sum(2 if ord(c) > 0x2000 else 1 for c in s)
    n = max(vis(label), vis(sub or "") * 0.82)
    return max(96, min(232, int(n * 5.4) + 40))

.claude/skills/blog-diagram/gen.py

gen.py 에는 LLM 호출이 없다. 결정론적 계산기다.
여기서 정해지는 것은 무엇을 그릴지가 아니라 어디에 놓을지다.
판단(존을 어떻게 나눌지, 무엇을 강조할지)만 남기고 계산은 코드가 가져간다.

같은 원리가 순서에도 적용된다.

# 같은 줄의 노드는 기본적으로 나열(병렬)이다. 순서가 있는 존만
# seq=True 를 줘서 화살표로 잇는다 — 선택지끼리 화살표로 이으면
# "PostgreSQL 다음에 MySQL" 처럼 없는 순서를 만들어낸다.

기본값이 “잇지 않음”이다. 화살표를 그리려면 명시적으로 seq=True 를 줘야 한다.
틀린 쪽이 더 수고롭게 만들어 둔 것이다.

전후 측정

지표 출처
첫 설계(<img>) 폐기 규모 −477 줄 / +344 줄 커밋 9cc03de
.diagram 계약 위반율 163 개 중 3 개 (1.8%) 커밋 30f4ae4
위반 시 렌더 크기 18×18px → 736×307px 동일
스킬 개정 후 재생성 +537 / −310 줄 커밋 dd6d3cd
현재 도식 총계 182 개 ls _includes/diagrams/*.svg
최다 생성일 하루 128 개 (커밋 14 개) 2026-08-12

검증은 빌드가 아니라 눈으로 했다. 이 오류가 빌드를 안 깨뜨리기 때문이다.

검증
- diff 는 래퍼 6줄 추가뿐 — 본문·도식 파일 변경 없음
- 세 도식 라이트/다크 6개 조합 전부 통과
- 사이트 전체 47개 페이지 순회: 안 감싼 도식 0, 찌그러진 도식 0
- 로드맵 글 회귀 없음 (도식 9개, 상자 133개 유지)

커밋 30f4ae4 본문

안 된 것

1. 문서화가 위반을 0 으로 만들지는 않았다.
SKILL.md 의 첫 조건이 .diagram 래퍼인데도 3 개가 새어 나갔다.
규칙을 읽는 것과 지키는 것은 다르다. 진짜 해법은 빌드를 실패시키는 것인데 아직 안 했다.
Jekyll 플러그인에서 _includes/diagrams/ 를 참조하는 페이지가 감쌌는지 검사해
빌드를 깨뜨리는 방식이 가능하다. 이미 _plugins/tag_pages.rb 가 있으므로 자리는 있다.

2. “규칙 도입으로 수정이 줄었다”를 수치로 못 쓴다.
계산은 해봤다. 스킬 도입 전 fix 비율 29/95(30.5%), 후 9/35(25.7%).
하지만 쓸 수 없다. 앞 구간은 테마 전면 개편이고 뒤 구간은 글 추가다.
작업 성격이 달라 비교가 성립하지 않는다.
줄어든 것처럼 보이는 5%p 는 규칙의 효과가 아니라 작업이 쉬워진 결과일 수 있다.

3. 검증이 여전히 사람 눈에 의존한다.
라이트·다크 양쪽을 브라우저로 본다. 도식 182 개 × 2 테마 = 364 회.
전수 확인은 실제로 못 하고 바뀐 글만 본다. 스크린샷 비교 자동화가 다음 과제다.

4. Mermaid 한계는 실측했지만 대안을 못 찾았다.
노드 16 개를 flowchart LR 로 그리면 2543px 한 줄로 펴져
본문 폭(52rem)에 맞춰 축소되면서 글자가 안 읽힌다.
subgraph 를 화살표로 이으면 dagre 가 내부 direction 을 무시하고 전부 세로로 세운다.
elk 레이아웃도 결과는 같았다. 그래서 긴 선형 흐름은 인라인 SVG 로 간다 —
문제를 푼 게 아니라 경계를 그어 피한 것이다.

남는 결론

가운데 층만 강제력이 없고, 실제로 새어 나간 3 개가 전부 그 층의 항목이었다.
규칙을 잘 쓰는 것보다 규칙을 코드로 내리는 편이 확실하다는 게 이 작업에서 남은 것이다.

문서로 남겨야 할 것은 “무엇을 하지 마라”가 아니라,
코드로 내릴 수 없어서 사람이 기억해야 하는 것뿐이다.