바둑을 배우기 전에, AI와 공유할 바둑판부터 만들었다
앞선 글에서는 내가 바둑을 배우는 순서와 프로그램이 바둑을 배우는 순서를 최대한 맞춰보기로 했다.
나는 바둑을 제대로 둘 줄 모른다.
그러니 활로를 배우면 프로그램에도 활로를 계산하는 기능을 넣고, 단수를 배우면 단수를 찾는 기능을 넣는다. 내가 이해한 개념을 코드로 옮기고, 바둑판 위에서 다시 확인한다.
그렇게 바둑 공부와 프로그램을 만드는 과정을 하나로 묶어보기로 했다.
로드맵까지 정하고 나니 이제 할 일은 비교적 분명해 보였다.
9×9 바둑판을 만들고 활로부터 시작하면 될 것 같았다.
그런데 실제로 개발을 시작하려고 하자 그보다 먼저 생각해야 할 것이 하나 생겼다.
나는 어디에 돌을 놓고, AI는 어디에 돌을 놓을까?
사람과 LLM은 바둑판을 사용하는 방법이 다르다#
내가 바둑을 배우려면 바둑판을 눈으로 볼 수 있어야 한다.
브라우저에 바둑판이 있고, 원하는 교차점을 클릭하면 돌이 놓인다. 상대가 둔 돌도 같은 화면에서 확인한다.
이런 화면을 GUI, Graphical User Interface라고 부른다. 그림이나 버튼처럼 눈에 보이는 요소를 통해 프로그램을 사용하는 방식이다.
나에게는 자연스럽다.
하지만 LLM에게도 같은 방식을 사용하게 할 필요가 있을까?
화면에서 바둑판을 찾아 마우스로 클릭하게 할 수도 있겠지만, 이번에는 돌을 놓으려는 좌표를 명확하게 전달할 수 있으면 충분했다.
현재 판을 읽는다.
D4에 돌을 놓는다.
판을 초기화한다.
이런 동작을 도구로 제공하면 된다.
get_board()
play_move("D4")
reset_game()
이 글에서 사용하는 함수 모양의 표기는 동작을 쉽게 읽기 위한 것이다. 실제 MCP 호출에서는 도구 이름과 입력값을 정해진 메시지 형식으로 전달한다.
사람은 GUI를 통해 바둑판을 사용하고, LLM 쪽은 MCP라는 도구 연결 방식을 통해 바둑판을 사용하도록 생각했다.
사람 → GUI → 바둑판
LLM → MCP → 바둑판
입구는 달라도 된다.
다만 그 입구가 이어지는 바둑판은 같아야 한다.
처음에는 GUI부터 만들고 나중에 MCP를 붙이면 될 것 같았다.
우선 아주 단순한 바둑판부터 만들었다#
첫 번째 바둑판에는 필요한 것만 넣기로 했다.
- 9×9 바둑판에 흑과 백의 돌을 놓는다.
- 흑부터 시작해서 번갈아 둔다.
- 이미 돌이 있는 곳에는 다시 놓을 수 없다.
- 현재 차례와 착수 기록을 관리한다.
- 판을 초기화할 수 있다.
아직 활로도 없고, 단수도 없고, 돌을 잡는 기능도 없다.
패를 처리하거나 집을 계산하지도 않는다.
따라서 빈자리에 돌을 놓을 수 있다고 해서 바둑의 모든 착수 규칙을 구현한 것은 아니다. 지금은 흑과 백이 번갈아 돌을 놓을 수 있는 정도다.
개발할 때 말하는 MVP, Minimum Viable Product라는 표현을 이 단계에도 붙일 수 있을 것이다. 핵심 목적을 확인할 수 있을 만큼의 최소 기능을 갖춘 버전이라는 뜻이다.
이번에 확인하려는 목적은 작았다.
사람과 LLM이 같은 판을 읽고, 같은 판에 돌을 놓을 수 있는가.
앞선 글에서는 바둑 개념을 구현한 뒤 LLM을 불러보기로 했었다. 이번에 연결한 것은 그 개념을 판단하는 AI가 아니라, 같은 판을 읽고 조작하는 통로다. LLM의 바둑 판단을 실험하는 일과 그 실험에 사용할 판을 준비하는 일을 나누어 생각하게 됐다.
화면과 바둑판의 상태를 분리했다#
GUI를 가장 단순하게 만든다면 클릭한 위치에 돌을 표시하고, 화면 쪽 배열에도 그 좌표를 기록하면 된다.
GUI에서 D4 클릭
↓
GUI 내부의 바둑판 배열 변경
↓
D4에 돌 표시
그런데 이 프로젝트에서는 다른 입구에서도 돌을 놓아야 한다.
GUI에만 착수 처리가 들어 있다면 MCP를 만들 때 같은 처리를 다시 작성하거나, 화면 내부의 데이터에 접근하는 방법을 찾아야 한다.
그래서 먼저 바둑판 자체의 처리를 화면에서 분리했다.
이 부분을 Baduk Core라고 부르기로 했다.
Core는 프로그램의 핵심 처리를 맡는 부분이다. 여기서는 돌의 위치, 현재 차례, 착수 기록을 관리하고, 착수 요청을 받아 허용할지 판단한 뒤 상태를 바꾼다.
반면 UI, User Interface는 사용자가 프로그램과 만나는 부분이다. 이번 UI는 브라우저의 GUI다. 바둑판을 그리고, 클릭을 좌표로 바꾸고, 요청 결과나 오류를 보여준다.
예를 들어 D4를 클릭했을 때의 흐름은 이렇게 바뀐다.
GUI: 사용자의 클릭을 D4라는 좌표로 해석한다.
↓
Core: D4에 돌을 놓을 수 있는지 확인한다.
↓
Core: 돌의 위치, 현재 차례, 착수 기록을 갱신한다.
↓
GUI: 결과로 받은 상태를 화면에 그린다.
Core는 사용자가 마우스로 클릭했는지 알 필요가 없다.
GUI는 흑돌을 놓은 뒤 차례를 어떻게 바꿀지 따로 결정할 필요가 없다.
이렇게 나누면 같은 착수 처리를 여러 입구에서 사용할 수 있다. 화면이 없어도 Core에 좌표를 전달하고 결과를 검사할 수 있으니 테스트하기도 쉬워진다.
여기서 분리한 것은 파일만이 아니었다.
무엇을 보여줄 것인가와, 어떤 상태 변경을 허용할 것인가의 책임을 나눈 것이다.
Source of Truth는 어느 쪽을 기준으로 삼을 것인가의 문제였다#
이 과정에서 Source of Truth라는 표현을 접했다.
그대로 옮기면 ‘진실의 원천’ 정도가 되지만, 이번 바둑판에서는 현재 상태를 판단할 때 최종 기준으로 삼는 곳이라고 이해하는 편이 쉬웠다.
예를 들어 화면에는 D4가 비어 있는데 Core에는 흑돌이 있다고 하자.
이때 GUI가 빈자리라고 판단해서 또 돌을 놓으면 곤란하다. Core가 가진 상태를 기준으로 착수를 거절하고, 화면도 그 상태에 맞추어야 한다.
반대로 클릭했다는 이유만으로 화면에 돌이 나타났더라도, Core가 그 요청을 받아들이지 않았다면 실제 착수로 확정된 것이 아니다.
그래서 이번 구조에서는 게임 상태의 최종 기준과 변경 권한을 한곳으로 모으기로 했다. 이런 의미에서 Single Source of Truth, 하나의 기준 상태라는 표현을 사용할 수 있다.
그렇다고 GUI에 아무 데이터도 없어야 한다는 뜻은 아니다.
화면을 그리려면 GUI도 돌의 위치를 전달받아 보관해야 한다. 선택한 메뉴나 연결 상태처럼 UI 자체에 필요한 정보도 있다.
다만 GUI가 받은 바둑판 데이터는 화면에 보여주기 위한 사본이다. 그 사본이 독자적으로 다음 차례를 결정하거나 새로운 착수 기록을 확정해서는 안 된다.
핵심은 데이터가 물리적으로 한 군데에만 존재하는지가 아니었다.
상태가 다르게 보일 때 어느 쪽을 기준으로 맞출 것인지, 누가 상태 변경을 확정하는지가 분명해야 했다.
여기까지 생각하고 나니 이제 MCP에서도 같은 Core를 사용하면 될 것 같았다.
그런데 아직 문제가 하나 남아 있었다.
같은 코드를 사용하는 것과 같은 상태를 사용하는 것은 달랐다#
처음에는 GUI와 MCP가 모두 같은 createBoard()를 사용하면 같은 바둑판을 사용하는 것처럼 생각했다.
하지만 createBoard()가 새로운 바둑판을 만드는 함수라면, 두 곳에서 각각 호출했을 때 만들어지는 판도 두 개다.
브라우저에서 createBoard() 실행 → 바둑판 A
MCP 서버에서 createBoard() 실행 → 바둑판 B
코드는 같다.
판의 크기도 같고, 흑부터 시작하는 규칙도 같다.
하지만 A에 D4를 두었다고 해서 B에도 D4가 놓이지는 않는다.
같은 설계도로 바둑판 두 개를 만든 것과 비슷하다. 모양과 사용법이 같아도 한쪽에 올린 돌이 다른 쪽으로 옮겨가지는 않는다.
여기서 인스턴스(instance)라는 말도 이해할 수 있었다. 코드로 정의한 것을 실행 중에 실제로 만들어낸 개별 대상을 뜻한다. 이 경우 A와 B는 각각 별도의 바둑판 인스턴스다.
특히 브라우저와 MCP 서버가 서로 다른 프로세스에서 실행된다면 각자 별도의 메모리를 사용한다. 프로세스는 실행 중인 프로그램의 단위다. 같은 파일을 불러왔다고 해서 그 안에서 만든 객체까지 자동으로 공유되는 것은 아니다.
그러면 사람은 A에서 돌을 놓고, LLM은 B를 읽는 상황이 생긴다.
둘 다 오류 없이 작동할 수 있다.
그런데 서로 다른 판을 보고 있다.
이 프로젝트에서는 그것만으로도 목적을 이루지 못한 셈이다.
같은 코드를 사용한다는 것은 처리 방법을 공유한다는 뜻이지, 지금 진행 중인 한 판을 공유한다는 뜻은 아니었다.
Core를 공통으로 사용하는 것에 더해, 실행 중인 상태를 공유할 구조가 필요했다.
실행 중인 한 판을 Shared Runtime이 맡도록 했다#
그래서 Shared Runtime을 두기로 했다.
Runtime은 프로그램이 실행되는 환경이나 실행 중의 동작을 가리키는 말이다. 여기서 Shared Runtime이라고 부른 것은 특별한 제품 이름이 아니다. Core의 바둑판 인스턴스를 소유하고 GUI와 MCP의 요청을 함께 처리하는 실행 계층을 가리킨다.
이번처럼 한 판을 공유하는 구성에서는 그 Runtime 안에서 바둑판을 한 번 생성한다.
GUI와 MCP 서버는 각자 새 판을 만들지 않고 같은 Runtime에 요청한다.
사람 → 브라우저 GUI ────────┐
│ HTTP 요청
▼
Shared Runtime
└─ Baduk Core
└─ 하나의 게임 상태
▲
│ HTTP 요청
LLM 쪽 → MCP 서버 ──────────┘
HTTP는 요청을 보내고 응답을 받는 통신 방식이다. 이번에는 브라우저와 MCP 서버가 로컬에서 실행 중인 Runtime에 판 조회나 착수 요청을 전달하는 데 사용했다.
여기서 Core와 Runtime의 역할도 구분할 수 있다.
Core는 바둑판을 어떻게 바꿀 것인가를 처리한다.
Runtime은 그 Core 인스턴스를 어디에서 유지하고, 여러 입구의 요청을 어떻게 연결할 것인가를 맡는다.
착수 요청이 들어오면 Runtime이 Core에 처리를 맡기고, 변경된 상태를 조회하거나 화면 쪽에 전달할 수 있게 한다. Core/UI 분리를 없앤 것이 아니라, 분리한 Core의 실행 위치를 정한 것이다.
따라서 최종 기준이 되는 게임 상태는 Shared Runtime 안의 Core에 있다.
GUI에서 D4에 돌을 놓아도 그 상태가 바뀌고, MCP를 통해 F6에 돌을 놓아도 같은 상태가 바뀐다.
물론 여기서 ‘공유’한다는 것은 GUI와 MCP 서버가 같은 메모리를 직접 들여다본다는 뜻은 아니다. 둘은 통신으로 요청하고 결과를 받는다. 그 요청이 도착하는 게임 상태의 주인이 하나라는 뜻이다.
이 원칙은 Runtime에 연결하지 못했을 때도 유지되어야 했다.
MCP 서버가 연결 실패를 감추려고 임시 바둑판을 새로 만들어버리면 다시 두 판이 된다. 그래서 Runtime을 사용할 수 없으면 오류를 반환하도록 했다.
또 실행 중인 상태를 공유하는 것과, 종료한 뒤에도 기보를 보존하는 것은 별개의 문제다. Shared Runtime을 두었다는 사실만으로 저장과 복원까지 해결되는 것은 아니다.
내가 이번에 정한 것은 우선 지금 함께 사용하는 한 판의 주인이 어디에 있는가였다.
MCP는 LLM이 그 판을 사용할 수 있는 입구다#
MCP는 Model Context Protocol의 약자다. AI 애플리케이션과 외부 도구·데이터를 연결하기 위한 통신 규약이다. 어떤 도구가 있고, 어떤 입력을 받으며, 호출 결과를 어떻게 돌려줄지 정해진 형식으로 주고받는다. 이 기본 구조는 MCP 공식 문서에서 설명한다.
여기서 클라이언트와 서버라는 말도 나왔다.
MCP client는 서버와 연결하여 도구를 호출하는 쪽이고, MCP server는 도구를 제공하고 요청을 처리하는 쪽이다. LLM이 사용할 도구의 호출은 AI 애플리케이션 쪽 클라이언트를 통해 전달된다.
서버라고 해서 반드시 인터넷 어딘가에 있어야 하는 것은 아니다. 이번 MCP 서버처럼 내 컴퓨터에서 실행할 수도 있다.
이번 검증에 사용한 연결을 조금 더 정확하게 그리면 이렇다.
Codex가 사용하는 MCP client
↕ stdio
로컬 MCP server
↕ HTTP
Shared Runtime → Baduk Core
stdio는 표준 입력과 표준 출력을 뜻한다. 로컬 프로세스 사이에서 메시지를 주고받는 MCP 전송 방식이다. MCP는 stdio 외에도 HTTP 기반 전송을 지원한다. MCP 전송 계층 설명
이번 구현에서 MCP client와 MCP server 사이의 통신, 그리고 MCP server가 Runtime에 보내는 HTTP 요청은 서로 다른 구간이다.
MCP 서버가 도구 호출을 받으면 그것을 Runtime에 대한 요청으로 연결한다. MCP라는 규약 자체가 공용 바둑판을 만들어주는 것은 아니다. 같은 Runtime을 사용하도록 구현했기 때문에 상태가 공유된다.
제공한 도구는 세 가지다.
| 도구 | 하는 일 |
|---|---|
get_board | 돌의 위치, 현재 차례, 착수 기록을 읽는다. |
play_move | 전달받은 좌표에 현재 차례의 돌을 놓도록 요청한다. |
reset_game | 돌과 착수 기록을 비우고 흑 차례로 되돌린다. |
play_move("F6")라고 하면 LLM 쪽에서 돌의 색까지 따로 정하지 않는다. Runtime 안의 Core가 현재 차례를 기준으로 색을 결정한다.
지금 이 도구에는 좋은 수를 찾는 기능이 없다. 바둑 엔진도, 바둑 판단을 위한 Skill도 아직 연결하지 않았다.
도구를 통해 F6에 둘 수 있다는 것과, 왜 F6가 좋은 수인지 판단할 수 있다는 것은 다른 능력이다.
이번에는 판을 읽고, 돌을 놓고, 다시 비우는 것까지 만들었다.
같은 상태를 가져도 화면이 저절로 바뀌지는 않았다#
이제 MCP에서 둔 돌은 Shared Runtime에 기록된다.
그런데 브라우저가 예전에 받아둔 상태만 계속 그리고 있다면 어떻게 될까?
서버에는 F6의 백돌이 있지만 내 화면에는 보이지 않는다. 새로고침해서 다시 상태를 받아와야 나타날 수도 있다.
판의 주인을 하나로 정하는 것과, 바뀐 판을 화면에 전달하는 것은 또 다른 문제였다.
그래서 Runtime의 상태가 바뀌면 GUI가 그 변경을 받아 화면을 갱신하도록 했다.
여기에 사용한 방식이 SSE, Server-Sent Events다.
브라우저가 서버와 연결을 열어두면 서버가 그 연결을 통해 이벤트를 계속 보낼 수 있다. 이벤트는 여기서 ‘상태가 바뀌었다’는 소식과 그에 필요한 데이터를 전달하는 메시지라고 생각하면 된다. 브라우저에서는 EventSource라는 API로 받을 수 있다. MDN의 SSE 설명
이번 GUI는 /api/events에 연결하여 상태 변경을 받도록 했다.
MCP로 F6 착수 요청
↓
Shared Runtime 안의 Core가 상태 변경
↓
Runtime이 SSE로 변경된 상태 전달
↓
GUI가 받은 상태로 바둑판을 다시 그림
SSE는 서버에서 브라우저로 보내는 단방향 통로다. 사용자의 착수는 별도의 HTTP 요청으로 보내고, 서버에서 생긴 변경을 받는 데 SSE를 사용한다. MDN의 SSE 통신 방향 설명
그러니 이번 구현에서 SSE는 MCP 호출을 전달하는 통로가 아니다. Runtime의 변경을 브라우저에 알려주는 통로다.
이렇게 하면 내가 둔 수든 MCP로 둔 수든 같은 상태 변경 경로를 거쳐 화면에 나타난다.
다만 ‘같은 판을 본다’는 말이 모든 화면의 픽셀이 정확히 같은 순간에 바뀐다는 뜻은 아니다. 변경 내용이 전달되고 화면이 다시 그려지는 과정은 필요하다.
내가 원한 것은 상대가 돌을 놓을 때마다 새로고침하지 않아도 그 수를 확인할 수 있는 것이었다.
실행 구조를 바꾸는 작업이라 브랜치를 나누었다#
이번 작업은 바둑판에 버튼 하나를 추가하는 정도는 아니었다.
브라우저 쪽에 있던 게임 상태를 Shared Runtime으로 옮기고, GUI와 MCP가 그곳에 연결하도록 바꾸는 작업이었다.
그래서 main에서 바로 진행하지 않고 별도 브랜치를 만들었다.
work/next-update
Git에서 브랜치(branch)는 변경 이력을 별도의 흐름으로 이어갈 수 있게 해준다. 내부적으로는 커밋을 가리키는 가벼운 참조다. 프로젝트 폴더 전체를 복사해 보관하는 것과는 다르다. Pro Git의 브랜치 설명
내 입장에서는 기존 main의 이력을 유지한 채 새로운 구조를 만들고 검사할 수 있다는 점이 중요했다.
그리고 검증한 변경을 main에 통합하는 것이 merge, 머지다.
브랜치가 있다고 해서 코드가 올바르게 만들어지는 것은 아니다. 머지하기 전에 무엇을 확인할지 따로 정해야 한다.
이때 정한 완료 조건을 merge gate라고 불렀다.
Git의 특별한 명령어가 아니라, 변경을 합치기 전에 통과해야 한다고 정한 검증 기준이다. 이번에는 자동 테스트 통과에 더해 실제 브라우저와 MCP 사이의 왕복 동작을 확인하기로 했다.
브랜치에서 구현
↓
자동 테스트
↓
실제 GUI와 MCP로 같은 판을 사용하는지 확인
↓
결과를 근거로 머지 여부 판단
여기서도 두 종류의 상태를 구분할 필요가 있었다.
Git이 관리하는 것은 코드와 문서의 변경 이력이다. Shared Runtime이 관리하는 것은 실행 중인 판의 상태다.
브랜치를 합친다고 해서 서로 다른 프로세스의 바둑판이 하나가 되지는 않는다. 코드의 통합과 실행 상태의 공유는 각각 해결해야 하는 문제다.
자동 테스트와 실제 검증은 무엇이 달랐을까#
구현 뒤에는 테스트도 확인했다.
자동 테스트는 정해둔 입력을 넣고 실제 결과가 기대한 결과와 같은지 프로그램으로 검사하는 것이다.
Core에 한 수를 전달하고 차례가 바뀌었는지 확인하는 것처럼 작은 부분을 검사할 수도 있다. 이런 검사는 단위 테스트에 가깝다.
반면 Runtime을 실행하고 MCP client를 연결한 뒤 같은 상태가 보이는지 검사하면 여러 구성 요소의 연결을 확인하는 통합 테스트가 된다.
이번 통합 테스트는 GUI가 사용하는 요청 경로로 D4에 착수한 뒤, 실제 stdio MCP client를 통해 그 수를 읽고 F6에 다음 수를 두는 흐름을 다뤘다. SSE로 상태가 전달되는지, 두 MCP client가 같은 판을 보는지, 초기화가 공유되는지도 검사하도록 되어 있었다.
Runtime이 없을 때 MCP가 별도의 판을 만들지 않고 실패하는지도 확인하는 항목이었다.
그렇다면 자동 테스트가 통과하면 충분하지 않을까?
여기서 살펴봐야 할 것은 테스트가 실제로 어디까지 실행했는가였다.
GUI와 같은 요청 경로를 호출하는 것과, 브라우저에서 교차점을 클릭해 돌이 그려지는 것까지 확인하는 것은 범위가 다르다.
요청과 상태 전달이 정상이어도 클릭 좌표를 잘못 해석하거나, 받은 상태를 화면에 잘못 그릴 가능성은 남는다. 로컬에서 실제로 실행한 프로그램이 의도한 Runtime에 연결되어 있는지도 보고 싶었다.
물론 브라우저 조작까지 자동화하면 이런 흐름도 테스트할 수 있다. 자동 테스트는 가짜이고 사람이 해보는 것만 진짜라는 뜻은 아니다.
이번에는 이미 마련한 자동 테스트에 더해, 내가 사용할 환경에서 전체 흐름을 직접 확인하기로 했다.
자동 테스트는 같은 조건을 반복해서 검사하는 데 도움이 된다. 직접 검증은 내가 의도한 사용 경험이 실제로 이어지는지 확인하고 이해하는 데 도움이 됐다.
그리고 증거도 구분해야 했다.
터미널에 ‘성공’이라는 로그가 남았다는 것만으로 같은 판을 공유한다고 할 수는 없다. 확인해야 할 것은 실제 돌의 위치, 현재 차례, 착수 기록이었다.
moveHistory는 어떤 수가 어떤 순서로 두어졌는지를 담은 게임 데이터다. 프로그램의 실행 상황을 출력하는 로그와는 역할이 다르다.
이번 검증에서는 그 기록과 눈앞의 판이 서로 맞는지를 보기로 했다.
처음으로 같은 판에 서로 한 수씩 두었다#
먼저 Codex에게 Shared Runtime을 실행하도록 했다.
그리고 브라우저로 접속했다.
빈 9×9 바둑판이 나타났다.
내가 직접 D4를 클릭했다.
첫 수이므로 흑돌이 놓였다.
그다음 Codex에게 MCP client를 통해 현재 판을 확인하도록 했다. 화면을 보고 추측하는 대신 get_board로 실제 상태를 읽게 한 것이다.
읽은 상태에는 D4의 흑돌이 있었고, 현재 차례는 백이었다.
내가 GUI에서 둔 수가 MCP 쪽에도 보였다.
이번에는 반대로 해봤다.
MCP client를 계속 사용해서 F6에 돌을 놓도록 했다.
play_move("F6")
그리고 브라우저를 바라봤다.
새로고침하지 않았는데 F6에 백돌이 나타났다.
내가 둔 D4의 흑돌과 MCP를 통해 둔 F6의 백돌이 같은 판 위에 있었다. 현재 차례도 다시 흑으로 바뀌었다.
다시 MCP로 착수 기록을 확인했다. 순서를 읽기 쉽게 적으면 이렇다.
1. black D4
2. white F6
내가 화면에서 본 순서와 같았다.
마지막으로 reset_game을 호출했다.
브라우저의 돌이 사라졌다. MCP로 다시 읽은 상태에서도 돌과 착수 기록이 비어 있었고, 흑 차례로 돌아와 있었다.
확인한 내용을 정리하면 다음과 같다.
| 순서 | 한 일 | 확인한 결과 |
|---|---|---|
| 1 | GUI에서 D4에 착수 | MCP로 읽은 판에도 흑 D4가 있고 백 차례다. |
| 2 | MCP로 F6에 착수 | 새로고침 없이 GUI에 백 F6가 나타나고 흑 차례다. |
| 3 | 착수 기록 조회 | 흑 D4 다음 백 F6가 하나의 기록에 남아 있다. |
| 4 | MCP로 초기화 | GUI와 MCP 모두 빈 판, 빈 기록, 흑 차례를 확인한다. |
아주 단순한 검증이었다.
F6가 좋은 수인지 알아본 것도 아니고, LLM이 스스로 전략을 세운 것도 아니다. 내가 지정한 좌표로 도구를 사용하게 했다.
그래도 이번에 확인하려던 것은 그 안에 있었다.
내가 GUI에서 둔 돌을 MCP로 읽고, MCP로 둔 돌을 내가 GUI에서 볼 수 있었다.
이번에는 바둑보다 프로그램의 상태를 조금 더 배웠다#
처음에는 GUI 하나와 MCP 하나를 만들면 될 것이라고 생각했다.
그런데 실제로 만들려고 하니 화면을 그리는 문제 뒤에 상태를 관리하는 문제가 있었고, 코드를 공통으로 사용하는 문제 뒤에 실행 중인 대상을 공유하는 문제가 있었다.
같은 상태에 접근하도록 만든 뒤에는 그 변경을 화면에 전달해야 했다.
구조가 만들어진 뒤에는 무엇을 확인해야 작동한다고 말할 수 있는지도 정해야 했다.
나중에 다시 읽을 때는 각 용어를 이번에 마주친 질문과 연결해두면 기억하기 쉬울 것 같다.
| 그때 생긴 질문 | 연결된 개념 | 이번 바둑판에서의 의미 |
|---|---|---|
| 화면과 바둑 처리를 어디서 나눌까? | Core/UI 분리 | 입력·표시와 게임 상태 변경의 책임을 나눈다. |
| 서로 다르게 보이면 어느 쪽이 기준일까? | Source of Truth | Runtime 안의 Core 상태를 최종 기준으로 삼는다. |
| 같은 함수를 쓰면 같은 판일까? | 코드와 인스턴스의 차이 | 같은 생성 함수를 각각 실행하면 별도 판이 생긴다. |
| 한 판을 어디서 계속 유지할까? | Shared Runtime | 하나의 Core 인스턴스에 GUI와 MCP의 요청을 모은다. |
| LLM 쪽은 어떻게 판을 조작할까? | MCP | 조회·착수·초기화를 도구로 제공한다. |
| 상대가 둔 돌을 화면은 어떻게 알까? | SSE | Runtime의 변경을 브라우저에 전달한다. |
| 구조 변경을 언제 합칠까? | 브랜치와 merge gate | 별도 이력에서 작업하고 정한 검증 조건을 확인한다. |
| 어디까지 작동한다고 확인했을까? | 테스트 범위와 실제 검증 | 요청·상태 검사에 더해 클릭과 화면 반영까지 확인한다. |
바둑의 개념을 하나씩 배우면서 프로그램도 함께 만들어보려 했는데, 그 전에 프로그램을 이루는 부분들이 어떻게 같은 대상을 다루는지부터 배우게 됐다.
Shared Board#
현재의 바둑판은 여전히 단순하다.
흑과 백이 번갈아 돌을 놓고, 착수 기록이 남고, 판을 초기화할 수 있다.
아직 활로를 계산하지 못하고, 돌을 잡지도 못한다. 단수나 패, 집을 판단하는 기능도 없다.
다만 나는 GUI로 판을 보고, LLM 쪽은 MCP로 같은 판을 읽는다.
내가 둔 수는 MCP로 확인할 수 있고, MCP로 둔 수는 내 화면에 나타난다. 현재 차례와 착수 기록도 하나의 상태를 기준으로 이어진다.
이렇게 사람과 LLM이 함께 사용할 Shared Board가 준비되었다.