노트버디 개발기 2: 기존 Python 프로젝트를 Anna 앱으로 옮기기
TL;DR
- 기존 Python 게임 규칙은 유지하고, 사용자가 조작할 화면과 Anna에 연결할 부분을 추가했습니다.
- 게임 규칙에서는 다음 상태를 계산하고, Anna 연결 부분에서는 사용자별 저장과 도구 실행을 맡도록 나눴습니다.
- 화면이 잘 열리는지뿐 아니라, 행동이 실행되고 앱을 다시 열어도 기록이 남는지까지 확인해야 했습니다.
들어가며
1편에서는 Anna에서 받은 제안과 노트버디가 어떤 앱인지 소개했습니다. 이번에는 기존 notebook-pets를 Anna 안에서 실행되도록 연결한 이야기를 해보려 합니다.
사용자 입장에서 친구에게 밥을 주는 건 버튼 한 번 누르는 일인데요. 만드는 쪽에서는 생각보다 챙길 게 있었습니다. 앱을 닫았다 열어도 밥을 먹은 기록이 남아야 하고, 통신 오류로 다시 눌렀다고 두 번 먹은 것으로 처리해서도 안 되니까요.
이런 부분들을 하나씩 연결하면서 지금의 구조로 정리하게 됐습니다.
프로젝트 README에도 사용하는 실제 앱의 데모 화면입니다. Mori의 그림은 Anna를 통해 생성했고, 대화창은 아직 대화를 시작하기 전입니다.
아래 내용은 2026년 10월 19일, Anna 1.4.13의 구현을 기준으로 적었습니다. 처음부터 이 구조를 다 잡아두고 시작한 건 아니고요. 기능과 실행 환경이 늘어나면서 조금씩 역할을 나눈 결과입니다.
기존 Python 규칙을 그대로 쓰고 싶었습니다
Anna 공식 개발자 안내를 보면 HTML, JavaScript, CSS로 만든 화면과 실행 도구를 함께 제공할 수 있습니다. 노트버디에서는 화면을 JavaScript로 만들고, 기존 게임 규칙은 Python으로 유지했습니다.
이때 화면과 규칙 사이에 Anna 연결 부분을 두었습니다. 코드에서 누가 어떤 일을 하는지 먼저 보면 이해하기 편할 것 같습니다.
| 부분 | 맡는 일 |
|---|---|
앱 화면 / anna/bundle/ |
친구와 상태 표시, 버튼과 대화, 그림 입력, AI 요청 |
Anna 연결 / notebuddy_plugin.py |
게임 도구 요청 수신, 사용자별 저장 읽기와 조건부 저장 |
Python 실행 / game_worker.py |
요청을 공통 규칙에 전달하고 화면에 필요한 결과 구성 |
공통 게임 규칙 / engine/game_api.py |
돌봄과 성장, 보상 조건을 검사하고 다음 상태 계산 |
Anna의 실행 도구 체계는 Executa라고 부릅니다. 앱 화면이 Host API를 통해 Python 도구를 호출하는 방식인데요. 게임 행동이 규칙과 저장소에 연결되는 흐름을 그려보면 아래와 같습니다.
이렇게 나눠두니 돌봄 규칙을 고칠 때 화면이나 Anna 저장 API까지 함께 고칠 일이 줄어듭니다. 저장 방식이 바뀌어도 포만감이나 성장 조건을 플랫폼마다 다시 작성할 필요가 없고요. 이미 만든 규칙을 계속 활용하려면 이 구분이 필요했습니다.
먹이 버튼을 한 번 눌러보면
이미 친구가 있는 사용자가 먹이 버튼을 눌렀다고 가정해 보겠습니다. 화면 뒤에서는 이런 순서로 처리됩니다.
- 화면에서 행동의 요청 ID를 만들고 게임 도구를 호출합니다.
- 도구가 해당 사용자의 저장 데이터와 저장 버전을 읽어옵니다.
- Python 규칙에서 지금 가능한 행동인지 확인하고 다음 상태를 계산합니다.
- 읽어왔던 저장 버전이 그대로인지 확인하면서 결과를 저장합니다.
- 저장에 성공한 결과를 화면에 보여주고, 설정에 따라 AI 반응을 요청합니다.
규칙을 호출하는 부분만 꺼내보면 아래와 같습니다. 인증과 저장 처리는 생략한 예시입니다.
candidate = transition(
state,
"feed",
context,
actor_id="1",
request_id=request_id,
)
# candidate.state는 계산 결과다.
# 실제 저장 여부와 저장 충돌 처리는 Anna 연결 계층이 담당한다.
context에는 규칙 데이터, 현재 시각, 난수와 ID 생성 방법을 전달합니다. 공통 API는 파일을 열거나 Anna에 접속하지 않고, 받은 상태로 다음 상태를 계산합니다. 현재 Anna 도구도 이 API에 상태를 직접 넘기고 있습니다.
저장할 때는 그사이에 다른 행동이 처리됐을 가능성도 봐야 하는데요. 그래서 읽어왔던 저장 버전인 ETag를 함께 보냅니다. 이미 데이터가 바뀌었다면 새로 읽고 다시 계산합니다. 같은 행동을 재시도할 때는 같은 요청 ID를 쓰고, 처리 기록을 게임 상태와 함께 저장해서 중복 적용을 막습니다.
물론 이걸로 동시 실행 문제가 모두 해결되는 건 아닙니다. 저장 데이터가 아예 없는 최초 생성은 기존 데이터를 갱신할 때와 조건이 달라서 별도로 다뤄야 합니다. 자세한 처리는 Anna 도구의 저장 코드에 모아뒀습니다.
또 하나 신경 쓴 건 밥을 먹은 결과와 AI의 답변을 따로 처리하는 것입니다. 밥은 이미 먹었는데 답변이 늦었다고 돌봄 결과까지 취소되면 곤란하겠죠. 그래서 저장된 행동은 유지합니다. AI가 “배불러!”라고 말했다는 이유만으로 수치를 바꾸지도 않고요.
다른 사용자의 친구와 섞이지 않게
친구 상태와 대화, 그림은 Anna의 저장 기능인 APS를 이용합니다. 데이터마다 쓰임이 달라서 보관 위치를 나눴습니다.
| 데이터 | 보관 방식 |
|---|---|
| 친구 상태, 성장, 아이템, 행동 처리 기록 | 인증된 사용자 범위의 도구 저장소 |
| 최근 대화와 그림을 찾기 위한 기록 | 사용자별 앱 저장소 |
| 원본 참고 그림과 생성한 초상화 | 파일 저장소에 보관하고 경로를 기록 |
앞의 코드에는 actor_id="1"이 들어 있는데요. 이걸 보면 모든 사용자가 같은 친구를 공유하는 건 아닌지 궁금할 수 있습니다. 여기서 1은 각 사용자에게 분리된 저장 데이터 안에서 쓰는 내부 슬롯입니다. 사용자를 나누는 건 Anna의 인증과 저장 범위가 담당합니다. 화면에서 다른 사용자 ID나 임의의 게임 상태를 보내는 방식으로 만들지는 않았습니다.
그림을 보관할 때도 생성 응답의 임시 URL만 남겨서는 안 됩니다. 파일 자체를 저장하고 경로를 기록해 둬야, 다음에 앱을 열었을 때 다운로드 주소를 다시 받아 보여줄 수 있습니다. 친구 그림이 계속 남아 있으려면 이 부분도 챙겨야 했습니다.
화면이 열려도 도구 연결은 따로 봐야 하더라고요
앱 화면이 정상적으로 보이는데 게임 도구 연결은 실패하는 경우가 있었습니다. UI 번들과 Python 실행 도구가 서로 다른 배포 대상이기 때문인데요.
게임 도구를 수정했다면 Windows와 Linux 실행 패키지도 맞춰야 합니다. 업로드 목록에서 새 버전을 확인하는 데 더해, 설치된 화면이 어떤 도구를 호출하고 실제로 어떤 버전이 실행 중인지까지 확인해야 했습니다. 돌봄 행동을 하고 앱을 다시 열었을 때 결과가 남아 있는지도 함께 봤고요.
개발 환경에서 생긴 문제도 있었습니다. Windows 개발 연결에서는 한글 JSON이 CP949로 처리돼 깨지거나, 저장 전에 오류가 난 적이 있습니다. 개발 실행 환경에 PYTHONUTF8=1, PYTHONIOENCODING=utf-8을 명시한 뒤 한글이 들어간 동작을 다시 확인했습니다.
이 경우는 당시 개발 브리지의 인코딩 문제였습니다. 앱 자체의 동작과 혼동하지 않도록 검사 기록에도 구분해서 남겨뒀습니다.
다음 기능을 붙일 준비까지
게임 규칙에서 파일과 플랫폼 호출을 떼어두니, 별도 웹 버전에서도 같은 Python 규칙을 재사용할 수 있는 기반이 생겼습니다. 저장과 AI 연결은 각 실행 환경에 맞춰 붙이면 되고요. 다만 규칙을 공유한다고 사용자 데이터까지 공유되는 건 아닙니다. Anna와 웹의 진행 상황은 자동으로 동기화되지 않습니다.
이번 작업을 하면서 버튼을 눌러 결과가 나오는 것과, 그 결과가 제대로 저장되는 것을 함께 봐야 한다는 점을 다시 느꼈습니다. 친구를 키우는 앱이다 보니, 다음에 들어왔을 때도 기록이 그대로 남아 있는 게 중요하더라고요.
이렇게 연결을 정리한 뒤에는 친구를 만나는 과정으로 넘어갔습니다. 3편에서는 그림 한 장으로 초상화와 성격을 만들고, 함께한 경험을 진화에 반영한 이야기를 해보겠습니다.

Comments