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


들어가며#

Part 1 에서 바둑판 도해를 그리는 goban shortcode 를 만들었습니다. ASCII 로 쓰고 SVG 로 렌더링하는 방식입니다.

만들고 나니 다른 시리즈가 눈에 들어왔습니다. 9x9 바둑 교재 리뷰 4편에 PNG 이미지로 된 도해가 65장 있었습니다. 이걸 전부 표기로 옮기기로 했습니다.

Part 2 는 그 작업의 기록입니다. 결론부터 적으면 작업의 90%는 shortcode 와 무관한 일 이었고, 나머지 10% 에서 렌더링은 멀쩡한데 내용만 틀리는 종류의 버그를 하나 만났습니다.


1. 진짜 문제는 옮겨 적는 것이었다#

표기를 만들었으니 옮기기만 하면 될 줄 알았습니다. 그런데 이미지 한 장을 열어 보고 생각이 바뀌었습니다.

바둑판 도해 하나를 표기로 옮기려면 9x9 = 81개 교차점 각각이 흑돌인지 백돌인지 빈 점인지 를 알아야 합니다. 그리고 그게 65장입니다. 5,265개 교차점입니다.

눈으로 읽으면 어떻게 될까요. 저는 이렇게 판단했습니다.

  • 한 장에 하나씩만 틀려도 65개의 틀린 도해 가 생깁니다
  • 그리고 틀렸다는 걸 아무도 모릅니다. 렌더링은 정상이고, 바둑판 모양도 그럴듯합니다
  • 원본 이미지를 지우고 나면 대조할 방법도 사라집니다

그래서 눈으로 읽지 않기로 했습니다.

1.1. 이미지를 기계로 읽기#

이미지의 구조가 단순한 것이 다행이었습니다. 흰 배경에 회색 격자선, 그 위에 흑돌·백돌, 아래에 캡션이 붙은 형태입니다.

판독기를 만들었습니다. 순서는 이렇습니다.

  1. 격자선 찾기 — 가로로 거의 전부가 흰색이 아닌 행을 찾으면 그게 격자선입니다. 돌이 선을 가려도 선 하나의 일부만 가리므로 여전히 검출됩니다
  2. 격자 맞추기 — 검출된 선들로 9줄 사다리를 세웁니다
  3. 교차점 분류 — 각 교차점 주변의 밝기를 재서 흑/백/빈점/라벨을 판정합니다

핵심은 3번입니다. 처음 시도는 돌 테두리의 평균 밝기 였습니다. 측정해 보니 이렇게 갈렸습니다.

대상테두리 평균 밝기
흑돌약 38
백돌약 217
빈 점약 250

깔끔하게 나뉩니다. 손으로 읽은 도해 몇 장과 대조하니 전부 맞았습니다.

그리고 이 기준은 세 번 틀렸습니다.


2. 분류기가 틀린 세 번#

2.1. 캡션을 잘라내다 판을 잘랐다#

각 이미지 아래에는 (Diagram 61) 같은 캡션이 있습니다. 이게 격자선 검출을 방해할까 봐 이미지 아래쪽 10% 를 잘라내고 시작했습니다.

그랬더니 판의 아래쪽 테두리선까지 잘려 나갔습니다. 9줄이어야 할 격자가 8줄로 읽혔고, 그 위에 놓인 판 전체가 한 줄씩 밀렸습니다.

고친 방법은 자르지 않는 것이었습니다. 캡션 글자는 짧아서 가로 폭의 80% 를 넘기지 못합니다. 격자선은 넘깁니다. 애초에 검출 조건이 캡션을 걸러 주고 있었는데, 걱정이 앞서서 한 겹 더 자른 것이 화근이었습니다.

방어 코드를 넣기 전에, 이미 방어되고 있는지부터 확인할 것.

2.2. 빽빽한 판에서 빈 점이 백돌이 되었다#

한 도해는 81개 교차점 중 71개가 돌로 덮여 있었습니다. 종국 직전의 판입니다.

여기서 양쪽이 흑돌로 둘러싸인 빈 점 이 백돌로 읽혔습니다. 이유는 명백합니다. 테두리 평균 밝기를 재는데, 그 테두리의 절반이 이웃한 흑돌이었으니까요. 250 이어야 할 값이 150 근처로 내려와 백돌 구간(120~235)에 들어갔습니다.

테두리만 보는 방식이 성긴 판에서만 통했던 것입니다. 검증에 쓴 도해들이 전부 돌이 몇 개 없는 판이었던 탓에 여기까지 드러나지 않았습니다.

고친 방식은 안쪽과 테두리를 함께 보는 것 입니다.

대상안쪽 평균테두리 평균
흑돌약 60약 38
백돌약 222약 217
화점(빈 점)약 230약 250
빈 점약 245약 250

백돌은 안팎이 모두 어둡습니다. 흑돌에 둘러싸인 빈 점은 테두리만 어둡고 안쪽은 밝습니다. 화점이 찍힌 빈 점은 안쪽만 살짝 어둡고 테두리는 밝습니다. 둘 다 어두울 때만 백돌로 판정하도록 바꾸니 세 경우가 모두 갈렸습니다.

한 가지 신호로 안 되면 두 가지를 보면 됩니다. 문제는 신호가 부족했던 게 아니라, 하나로 충분하다고 믿을 근거가 부족했던 것 이었습니다.

2.3. 격자가 통째로 한 칸 밀렸다#

같은 도해에서 또 하나 나왔습니다. 판이 돌로 꽉 차 있으니 맨 왼쪽 세로선이 완전히 가려져 검출되지 않았습니다. 사다리가 한 칸 오른쪽에서 시작했고, 판 전체가 밀렸습니다.

이건 밝기 기준을 아무리 고쳐도 안 잡힙니다. 각 교차점 판정은 전부 “정상"이었으니까요. 틀린 것은 좌표계였습니다.

발견한 방법은 단순했습니다. 판정 결과를 원본 이미지 위에 겹쳐 그려서 눈으로 봤습니다.

글자가 돌 위에 정확히 얹히면 맞는 것이고, 한 칸씩 어긋나 있으면 틀린 것입니다. 표로 출력된 X O . . 를 아무리 들여다봐도 알 수 없었던 것이, 겹쳐 놓으니 즉시 보였습니다.

판독 결과를 원본에 겹쳐 보는 것. 이 작업에서 가장 값이 컸던 한 줄이었습니다.

2.4. 검증: 산술로 교차 확인#

밝기 기준을 고친 뒤, 손으로 읽어 둔 도해 9장과 다시 대조해 전부 일치 하는 것을 확인했습니다.

그런데 한 장은 더 확실한 검증이 가능했습니다. 덤(komi)을 설명하는 도해였는데, 본문에 “흑의 영토가 백보다 5집 넓다” 고 적혀 있었습니다.

판독한 배치로 집을 세어 봤습니다.

  • 흑: 1~3열 전체 27집 + 4열 일부 5집 = 32집
  • 백: 7~9열 전체 = 27집
  • 차이 = 5집

본문 숫자와 맞았습니다. 밝기 임계값이 아니라 바둑 규칙으로 판독 결과를 검증 한 셈입니다. 이런 교차 확인이 가능한 도해는 많지 않지만, 하나라도 있으면 나머지에 대한 신뢰가 달라집니다.


3. 표기법에 없는 것 세 가지#

판독이 끝나고 나니, 원본 도해에 있는데 표기로 옮길 수 없는 것들이 보였습니다.

3.1. 화살표#

여덟 장에 손으로 그린 화살표가 있었습니다. 돌 하나에서 사방으로 뻗어 나가며 영향력의 방향 을 나타내는 그림입니다.

shortcode 에 화살표 인자를 추가할 수도 있었습니다. 그런데 본문을 읽어 보니 이미 이렇게 쓰여 있었습니다.

“이 수는 상하 변으로의 발전 을 도모하는 의미도 가집니다.”

화살표가 가리키던 바로 그것입니다. 그림이 정보를 더하는 게 아니라, 이미 쓴 문장을 반복하고 있었습니다. 화살표를 빼도 잃는 것이 없었습니다.

다만 한 문장은 고쳐야 했습니다.

- Diagram 02에서 화살표가 사방으로 뻗어나가듯, 천원 한 수는 …
+ 천원은 네 귀와 네 변 어느 쪽으로도 거리가 같아, 한 수로 …

그림을 직접 지칭하는 문장이 남아 있으면, 그림을 지운 순간 본문이 거짓말을 합니다.

한 걸음 더 나아간 판단도 있었습니다. 여섯 장은 “판 위에 돌 하나 + 화살표"가 전부였습니다. 화살표를 빼면 남는 것이 돌 한 점뿐인데, 그 여섯 지점은 이미 앞쪽 도해 한 장에 A~F 로 전부 표시되어 있었습니다. 그래서 여섯 장을 없애고 그 도해 하나로 통합한 뒤, 본문에서 “천원(Diagram 01의 A)” 처럼 가리키게 했습니다. 도해 65장이 59장이 된 이유입니다.

3.2. 따낸 돌 자리의 번호#

원본에는 이런 관례가 있습니다. 수순 도중 따내진 돌은 판에서 사라지지만, 그 자리에 번호만 남겨 어디에 두었는지 알려 줍니다.

표기에서 숫자는 곧 돌입니다. 빈 점에 숫자를 쓰면 돌이 그려집니다.

표시된 빈 점(*)으로 바꾸고 캡션에서 설명하는 방식을 골랐습니다.

{{< goban caption="흑6이 백1을 따냈다. 표시된 점이 백1이 있던 자리다" >}}

바둑책에서 흔히 쓰는 방식이라 독자에게 낯설지 않고, 표기를 늘리지 않아도 됩니다.

3.3. 아직 두지 않은 자리의 번호#

한 장에는 백이 앞으로 둘 수 있는 자리7 이 찍혀 있었습니다. 본문도 “백은 7의 자리를 둘 수 있습니다"라고 받고 있었습니다.

이건 수순이 아니라 라벨입니다. 라벨 C 로 바꾸고 본문도 “C 자리"로 고쳤습니다. 표기의 의미 체계에 맞추면서 본문과의 대응도 유지 하는 선택이었습니다.


4. 조용히 틀릴 뻔한 지점#

여기까지는 옮기는 작업이었습니다. 이제 실제 버그 이야기입니다.

4.1. 발견#

4장의 도해를 훑다가 이상한 것을 봤습니다. 번호 1, 3, 5 가 흰 돌이었습니다.

Part 1 에서 만든 규칙은 이랬습니다.

{{- $black := eq (mod (int $cell) 2) 1 }}

홀수는 흑, 짝수는 백. 흑이 먼저 두니 당연한 규칙이고, 실제로 대부분의 도해가 그렇습니다.

그런데 이 장은 정석의 중간부터 시작합니다. 백이 침입해 들어가는 국면이라 백이 1번을 둡니다. 원서가 그렇게 번호를 매겼고, 포스트 본문도 이미 그 번호를 그대로 쓰고 있었습니다.

“백은 3 으로 끼우는 수를 선택할 수 있습니다” “흑이 4 로 자신의 돌을 연결하면, 백은 5 로…”

이대로 옮기면 어떻게 될까요. 렌더링은 완벽합니다. 바둑판이 그려지고, 번호가 찍히고, 아무 경고도 없습니다. 다만 흑백이 전부 뒤집혀 있습니다.

본문은 “백3"이라고 하는데 그림의 3번은 검은 돌입니다. 이런 오류는 원서를 펴 놓고 대조하는 독자만 발견합니다.

이 시리즈에서 이런 도해가 33장 중 상당수였습니다.

4.2. 고려한 우회로들#

shortcode 를 건드리지 않고 푸는 방법을 먼저 생각해 봤습니다.

우회로 1 — 그림의 흑백을 뒤집고 본문에서 흑/백을 바꿔 쓰기

성립하지 않습니다. 이 도해들에는 번호 없는 돌들이 이미 놓여 있습니다. 번호만 뒤집으면 국면 자체가 성립하지 않고, 판 전체를 뒤집으면 “백이 침입한다"는 4장 전체의 서사가 “흑이 침입한다"로 뒤집힙니다. 원서와도 어긋납니다.

우회로 2 — 번호를 하나씩 올려 백이 짝수를 두게 하기

첫 수가 2번부터 시작합니다. 1번을 찾는 독자가 반드시 생깁니다. 원서를 펴 놓고 보는 독자와도 번호가 어긋납니다. 무엇보다 33장 × 그 사이의 해설 문장을 전부 손으로 고쳐야 하는데, 하나만 빠뜨려도 렌더링은 멀쩡하고 내용만 틀립니다. 처음 문제와 똑같은 실패를 반복하는 셈입니다.

4.3. 실제로 한 일#

shortcode 에 인자를 하나 더했습니다. 4줄입니다.

{{- $oddIsBlack := cond (eq (.Get "first") "white") 0 1 -}}
...
{{- $black := eq (mod (int $cell) 2) $oddIsBlack }}

본문에서는 이렇게 씁니다.

{{< goban first="white" caption="백3의 끼움" >}}

빌드해서 확인했습니다.

기본        : 1=black 2=white 3=black
first=white : 1=white 2=black 3=white

기존 도해는 한 글자도 고치지 않아도 됩니다. 인자를 주지 않으면 동작이 그대로이기 때문입니다.

우회로 1·2 는 본문 수백 줄을 손으로 고치면서 조용한 오류를 만들 위험을 감수하는 길이었고, 이쪽은 템플릿 4줄이었습니다. “코드를 고치지 않고 푼다"가 늘 싼 것은 아닙니다.


5. 결과#

항목
도해PNG 65장goban 표기 59개
정적 파일10MB0
빌드 산출물1,781 파일 / 48MB1,716 파일 / 38MB
도해 수정바둑 프로그램 재실행텍스트 한 글자

도해가 텍스트가 되면서 따라온 것들도 있습니다. 독자의 글꼴 크기를 따라 커지고, 사이트 테마를 그대로 쓰고, 브라우저에서 선택·복사됩니다. 그리고 diff 에 무엇이 바뀌었는지 그대로 보입니다.


6. 정리#

이틀치 작업에서 남은 것을 셋으로 줄이면 이렇습니다.

1. 옮겨 적는 일은 눈으로 하지 않습니다. 5,265개 교차점을 눈으로 읽었다면 몇 개는 반드시 틀렸을 것이고, 틀렸다는 것조차 몰랐을 것입니다. 판독기를 만드는 데 든 시간보다, 판독기가 잡아낸 세 가지 오류를 눈으로 찾는 데 들었을 시간이 훨씬 깁니다.

2. 도구가 맞았는지는 도구 밖에서 확인합니다. 밝기 임계값을 아무리 조정해도 격자가 한 칸 밀린 것은 못 잡습니다. 결과를 원본에 겹쳐 보고, 판독 결과로 집을 세어 본문 숫자와 맞춰 보는 것 — 다른 축의 검증 이 필요했습니다.

3. 조용히 틀리는 것이 요란하게 실패하는 것보다 나쁩니다. 빌드가 깨지면 즉시 압니다. 흑백이 뒤집힌 도해는 발행되고, 읽히고, 아무도 말해 주지 않습니다. 이 작업에서 가장 위험했던 지점은 판독기가 아니라 “당연한 규칙"이라고 믿고 하드코딩한 두 줄 이었습니다.


References#