이 글은 Claude Opus 5 를 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.


들어가며#

이 블로그에 바둑 입문 시리즈를 쓰다가 벽에 부딪혔습니다. 바둑 글에는 바둑판 그림이 필요한데, 그릴 방법이 마땅치 않았습니다.

결국 goban 이라는 이름의 Hugo shortcode 를 직접 만들었습니다. 본문에는 이렇게 쓰고,

{{< goban caption="빈 삼각형" >}}
       a b c d e
     1 . X . . .
     2 X O * . .
     3 . . . . .
{{< /goban >}}

렌더링은 SVG 바둑판으로 나가는 방식입니다. 실제 결과는 이렇습니다.

abcde123
빈 삼각형

이 글은 그 과정의 기록입니다. Part 1 에서는 Hugo 가 무엇인지 짧게 소개하고, 왜 이미지도 mermaid 도 답이 아니었는지, 그리고 첫 버전을 어떻게 만들었는지를 다룹니다. Part 2 에서는 이 표기를 이미 발행된 포스트 4편의 도해 65장에 적용하면서 겪은 일 — 특히 조용히 틀릴 뻔했던 지점 — 을 다룹니다.


1. Hugo 를 모르는 분을 위한 짧은 소개#

Hugo 를 이미 아신다면 2장으로 건너뛰셔도 됩니다.

1.1. 정적 사이트 생성기란#

Hugo 는 마크다운 파일을 HTML 로 바꿔 주는 도구입니다. 이런 마크다운 파일을 하나 쓰면,

+++
title = "첫 글"
date = "2026-08-12T13:00:00+09:00"
+++

안녕하세요. **첫 글** 입니다.

hugo 명령 한 번으로 public/ 아래에 완성된 HTML 파일들이 생깁니다. 목록 페이지, 태그 페이지, RSS 피드, sitemap 까지 알아서 만들어 줍니다. 그 결과물을 정적 파일 호스팅에 올리면 그게 곧 블로그입니다.

WordPress 같은 도구와 결정적으로 다른 점은 요청이 올 때 아무 일도 하지 않는다 는 것입니다. 데이터베이스도 없고 서버 쪽 코드도 돌지 않습니다. 미리 만들어 둔 HTML 파일을 그대로 내보낼 뿐입니다. 그래서 빠르고, 깨질 곳이 적고, 호스팅이 사실상 공짜입니다.

이 블로그는 글 468편, 생성 파일 1,700여 개 규모인데 전체 빌드가 4.3초 걸립니다.

1.2. shortcode 란#

마크다운은 문단, 목록, 링크, 표 정도를 표현하도록 만들어진 문법입니다. 그 이상이 필요할 때 쓰라고 Hugo 가 마련해 둔 확장 지점이 shortcode 입니다.

본문에 이렇게 쓰면,

{{< youtube dQw4w9WgXcQ >}}

Hugo 가 layouts/shortcodes/youtube.html 이라는 템플릿을 찾아 실행하고, 그 결과 HTML 을 그 자리에 끼워 넣습니다. 본문에는 짧은 한 줄만 남고, 지저분한 <iframe> 은 템플릿 안에 숨습니다.

핵심은 본문 소스가 읽을 만한 상태로 남는다 는 점입니다. 마크다운 파일을 열었을 때 HTML 덩어리가 아니라 의도가 보입니다. 이 성질이 뒤에 나올 판단의 근거가 됩니다.

1.3. 이 글에서 쓰는 Hugo 문법 세 가지#

Part 1·2 를 읽는 데 필요한 만큼만 추립니다.

문법
{{ .Inner }}shortcode 를 여닫는 태그 사이에 들어 있는 본문
{{ .Get "caption" }}shortcode 에 넘긴 이름 붙은 인자
{{ range $i, $v := $list }}목록을 순회. $i 는 인덱스, $v 는 값

Hugo 템플릿은 Go 의 text/template 문법을 씁니다. 처음 보면 {{- ... -}} 같은 표기가 낯선데, 앞뒤 하이픈은 그 자리의 공백을 지운다 는 뜻입니다. SVG 를 뽑을 때 빈 줄이 끼는 것을 막는 용도로 뒤에서 계속 나옵니다.


2. 문제: 바둑판을 어떻게 그릴 것인가#

바둑 입문 글에서 도해는 장식이 아니라 본문입니다. “이 모양은 빈 삼각형이라 나쁘다” 같은 문장은 그림 없이는 아무 뜻도 전달하지 못합니다.

시리즈 한 편에 도해가 20장 넘게 들어갑니다. 그러니 도해를 만드는 비용이 곧 글을 쓰는 비용이 됩니다.

요구사항을 정리하면 이랬습니다.

  1. 한 편에 20장 이상 을 부담 없이 만들 수 있을 것
  2. 글을 고칠 때 도해도 같이 고칠 수 있을 것
  3. 나중에 도해 하나가 틀린 걸 발견했을 때 어디가 틀렸는지 diff 로 보일 것
  4. 다크 테마인 이 블로그에서 흑돌과 백돌이 구분될 것

2.1. 후보 1 — 이미지#

가장 쉬운 길입니다. 바둑 프로그램에서 화면을 캡처해 static/img/ 에 넣고 참조하면 됩니다.

실제로 이 블로그의 다른 시리즈가 그렇게 되어 있었습니다. 그리고 그게 어떤 상태인지 알고 있었습니다.

  • 도해 하나를 고치려면 바둑 프로그램을 다시 열어야 합니다
  • 파일이 바뀌면 diff 는 Bin 57207 -> 0 bytes 라고만 알려 줍니다. 무엇이 어떻게 바뀌었는지 알 수 없습니다
  • 65장이 10MB 를 차지합니다
  • 독자의 글꼴 크기를 따라가지 못하고, 다크 테마 위에 흰 사각형으로 뜹니다

요구사항 2·3 을 정면으로 위반합니다.

2.2. 후보 2 — 코드 블록 안의 ASCII#

처음 쓴 방법입니다. 마크다운 코드 블록에 이렇게 적었습니다.

       a b c d e
     1 . X O . .
     2 X O . O .
     3 . X O . .

장점이 분명합니다. 쓰기 쉽고, 고치기 쉽고, diff 가 완벽하게 보입니다. 요구사항 1·2·3 을 전부 만족합니다.

문제는 독자 쪽입니다. 바둑을 처음 배우는 사람에게 XO 가 늘어선 격자를 주고 “여기서 백이 잡힙니다"라고 말하는 건, 입문서가 할 일을 독자에게 떠넘기는 것 입니다. 격자선도 없어서 무엇이 붙어 있고 무엇이 떨어져 있는지 눈에 들어오지 않습니다.

2.3. 후보 3 — mermaid#

이 블로그는 이미 mermaid 를 쓰고 있습니다. 순서도나 시퀀스 다이어그램을 코드로 적으면 브라우저에서 그림으로 그려 주는 도구입니다. 도해도 여기서 해결되면 가장 깔끔합니다.

그래서 실제로 시험해 봤고, 답이 아니라는 결론을 얻었습니다. 이 블로그에 물려 있는 mermaid v10 기준입니다.

시도한 다이어그램결과
block-beta격자선을 그릴 수 없습니다. 게다가 칸 크기가 내용물에 맞춰 정해져서, 돌이 있는 칸과 빈 칸의 너비가 달라집니다. 열이 어긋나면 바둑판이 아닙니다
quadrantChart사분면 구분선과 축이 강제로 그려집니다. 바둑판 한가운데 십자선이 지나갑니다

mermaid 에는 교차점 위에 무언가를 놓는 격자 를 그리는 다이어그램 종류가 아예 없습니다. 없는 기능을 비슷한 것으로 흉내 내려다 보니 매번 원하지 않는 장식이 따라붙었습니다.

여기서 얻은 교훈이 하나 있습니다. 가진 도구로 안 되는 일을 억지로 시키면, 결과물이 아니라 우회 흔적이 남습니다. block-beta 로 만든 판은 “격자선 없는 표"였지 바둑판이 아니었습니다.


3. 결정: 소스는 ASCII, 렌더링은 SVG#

세 후보를 놓고 보니 답이 보였습니다.

ASCII 는 쓰는 사람에게 좋았고, 그림은 읽는 사람에게 좋았습니다. 둘 중 하나를 고를 게 아니라, 소스는 ASCII 로 두고 렌더링만 그림으로 바꾸면 됩니다.

그게 정확히 shortcode 가 하는 일입니다.

{{< goban >}}          ← 본문에 남는 것: 읽을 수 있는 ASCII
       a b c d e
     1 . X O . .
{{< /goban >}}
<figure class="goban">     ← 독자가 보는 것: SVG 바둑판
  <svg viewBox="0 0 ...">

정리하면 이렇습니다.

요구사항이미지ASCIIshortcode
20장을 부담 없이
글과 함께 수정
diff 로 보임
독자가 알아봄

4. v1 구현#

4.1. 입력을 좌표로 바꾸기#

shortcode 가 받는 것은 여닫는 태그 사이의 문자열 하나뿐입니다. 이것을 격자로 해석해야 합니다.

{{- $lines := slice -}}
{{- range (split (chomp .Inner) "\n") -}}
  {{- $t := trim . " \t\r" -}}
  {{- if ne $t "" -}}{{- $lines = $lines | append $t -}}{{- end -}}
{{- end -}}

{{- $cols := findRE `\S+` (index $lines 0) -}}
{{- $rows := after 1 $lines -}}

.Inner 를 줄 단위로 자르고 빈 줄을 버립니다. 그리고 첫 줄을 열 라벨로 삼습니다. 첫 줄에 토큰이 5개면 5열 판이고, 이후 각 줄의 첫 토큰이 행 라벨, 나머지가 칸이 됩니다.

판 크기를 인자로 받지 않는 것이 중요합니다. 소스에 이미 적혀 있는 정보를 다시 적게 하면 둘이 어긋날 수 있습니다. 열 라벨을 하나 지우면 판도 한 칸 줄어드는 게 맞습니다.

findRE 로 공백 구분 토큰을 뽑기 때문에, 소스에서 열을 맞추려고 공백을 몇 개 넣든 결과가 같습니다. 사람이 읽기 좋게 정렬해 둘 자유가 생깁니다.

4.2. SVG 좌표 계산#

{{- $s := 34 -}}    {{/* 칸 간격 */}}
{{- $px := 36 -}}   {{/* 좌측 여백: 행 라벨 자리 */}}
{{- $py := 38 -}}   {{/* 상단 여백: 열 라벨 자리 */}}
{{- $r := 13 -}}    {{/* 돌 반지름 */}}
{{- $w := add $px (add (mul (sub $nc 1) $s) 20) -}}
{{- $h := add $py (add (mul (sub $nr 1) $s) 16) -}}

Go 템플릿에는 산술 연산자가 없어서 add, sub, mul 함수를 씁니다. 처음엔 답답하지만 계산 자체는 단순합니다. 격자선은 첫 열에서 마지막 열까지, 첫 행에서 마지막 행까지 직선을 긋습니다.

여기서 판의 가장자리가 저절로 해결됩니다. 9x9 전체 판을 그리면 가장 바깥 선이 곧 판의 테두리가 되고, 일부만 잘라낸 도해는 테두리 없이 격자만 나옵니다. 바둑 도해의 관례와 맞습니다.

4.3. 색: 다크 테마를 상속하지 않기로#

이 블로그는 다크 테마입니다. 자연스러운 선택은 판도 어둡게 만드는 것이었습니다.

그런데 그렇게 하면 흑돌이 배경에 묻혀 빈 원처럼 보입니다. 바둑 글에서 이건 그냥 못생긴 정도가 아니라 틀린 신호 입니다. 독자는 “여기 돌이 없구나"라고 읽습니다.

그래서 판만 밝은 나무색으로 두기로 했습니다.

.goban__board  { fill: #d9c9a3; }   /* 나무판 */
.goban__black  { fill: #14140f; }
.goban__white  { fill: #fbfaf6; stroke: #4a3f2c; }

사이트 테마와의 일관성보다 도해가 전달해야 할 정보 를 앞에 둔 판단입니다. 일관성은 취향이지만, 흑돌이 빈 점으로 보이는 것은 오류입니다.

4.4. v1 의 표기#

여기까지가 첫 커밋입니다. 칸에 쓸 수 있는 기호는 넷이었습니다.

기호
.빈 점
X흑돌
O백돌
*표시된 빈 점(급소 등)

바둑 입문 시리즈 Pt.2 의 도해 23장을 여기에 옮겼습니다. 소스 텍스트는 한 글자도 바뀌지 않았습니다. 코드 블록을 감싸던 세 개의 backtick 이 shortcode 태그로 바뀐 것이 전부입니다.

이때 본문의 표기법 설명도 함께 고쳤습니다. 전에는 X 는 흑돌, O 는 백돌이라고 소스 문자를 설명했지만, 이제 독자는 소스를 보지 않습니다. “검은 돌 = 흑돌”, “붉은 점 = 눈여겨볼 자리"처럼 화면에 보이는 것 으로 다시 썼습니다.

4.5. 곧바로 드러난 부족함#

Pt.1 의 도해를 옮기려니 네 기호로는 모자랐습니다. Pt.1 은 수순을 보여 주고(“흑1, 백2, 흑3”), 본문에서 특정 지점을 이름으로 가리키고(“A 자리에 두면”), 이미 놓여 있던 돌 하나를 구별해야 했습니다.

기호 네 개를 더했습니다.

기호
1 2 3수순. 홀수는 흑, 짝수는 백
A B C빈 점에 붙이는 라벨
x표시가 붙은 흑돌
o표시가 붙은 백돌

수순 번호는 관례를 그대로 따랐습니다.

{{- else if findRE `^[0-9]+$` $cell -}}
  {{- $black := eq (mod (int $cell) 2) 1 }}
  <circle class="goban__{{ if $black }}black{{ else }}white{{ end }}" .../>
  <text class="goban__num-{{ if $black }}on-black{{ else }}on-white{{ end }}" ...>{{ $cell }}</text>

홀수면 흑, 짝수면 백. 흑이 먼저 두니까 당연한 규칙입니다.

이 두 줄이 다음 날 문제를 일으킵니다. Part 2 에서 다룹니다.

라벨은 대문자 한 글자만 받습니다. XO 는 이미 돌이라 라벨로 쓸 수 없다는 제약이 여기서 나옵니다. 그리고 라벨 아래에는 판 색깔 원을 하나 깔았습니다. 그러지 않으면 격자선이 글자를 관통합니다.

<circle class="goban__labelbg" cx="{{ $cx }}" cy="{{ $cy }}" r="10"/>
<text class="goban__pointlabel" x="{{ $cx }}" y="{{ $ty }}">{{ $cell }}</text>

이렇게 두 편의 도해 39장이 표기로 옮겨졌습니다. 여기까지가 첫날입니다.


5. 첫날 정리#

전체 shortcode 는 105줄입니다. 클라이언트 스크립트는 한 줄도 없습니다. 브라우저는 그냥 SVG 를 받습니다.

돌아보면 판단의 갈림길은 셋이었습니다.

  1. 가진 도구(mermaid)로 안 되는 것을 확인하는 데 시간을 썼습니다. 그냥 되겠거니 하고 시작했다면 block-beta 로 열이 어긋난 판을 만들어 놓고 CSS 로 우겨넣고 있었을 것입니다. 안 된다는 것을 왜 안 되는지까지 확인해 둔 덕분에 다음 선택이 빨랐습니다.
  2. 쓰는 사람의 편의와 읽는 사람의 편의를 하나로 합치려 하지 않았습니다. 소스는 ASCII, 출력은 SVG. 둘은 다른 문제였고 다르게 풀면 됐습니다.
  3. 일관성보다 정확성을 골랐습니다. 다크 테마에 맞추는 대신 판을 밝게 뒀습니다.

Part 2 에서는 이 표기를 이미 발행된 다른 시리즈 4편 — PNG 이미지 65장 — 에 적용합니다. 눈으로 옮겨 적으면 안 되는 이유, 이미지를 기계로 판독한 방법, 그리고 4.4 의 그 두 줄이 어떻게 렌더링은 멀쩡한데 내용만 틀린 도해 를 만들 뻔했는지를 다룹니다.


References#