KataGo와 훈련 대국을 두다가 전화가 왔다. 통화를 마치고 돌아오니 브라우저가 꺼져 있었다. 다시 열었을 때 깨달았다. 50수 넘게 둔 대국이 사라졌다는 것을.
Streamlit의 세션 스테이트는 휘발성이다. 브라우저를 닫으면 모든 게 날아간다. 알고는 있었지만, 직접 겪으니 다르다.
문제: 세션의 덧없음
Streamlit 앱에서 게임 상태는 st.session_state에 저장된다.
st.session_state.moves = [("B", "Q16"), ("W", "D4"), ...]
st.session_state.board = [[...], [...], ...]
st.session_state.engine = KataGoGTP(...)
편리하다. 하지만 브라우저 탭을 닫는 순간, 새로고침하는 순간, 모든 게 초기화된다.
대국 종료 후에는 SGF로 저장하도록 해뒀지만, 진행 중인 대국은? 그냥 사라진다.
해결: 매 수마다 저장하기
방법은 단순하다. AI가 응수할 때마다 현재 상태를 파일로 저장한다.
AUTOSAVE_FILE = DATA_DIR / "_autosave_inprogress.sgf"
AUTOSAVE_META = DATA_DIR / "_autosave_meta.txt"
def autosave_game():
"""진행 중인 대국을 자동 저장."""
engine = st.session_state.get("engine")
if not engine:
return
sgf = engine.game_state.to_sgf()
AUTOSAVE_FILE.write_text(sgf, encoding="utf-8")
# 메타 정보: 색상, 난이도, 저장 시각
meta = f"{st.session_state.player_color}n{st.session_state.level}n{datetime.now().isoformat()}"
AUTOSAVE_META.write_text(meta, encoding="utf-8")
AI가 수를 둘 때마다 이 함수를 호출한다. 파일 두 개가 계속 덮어써진다.
_autosave_inprogress.sgf: 현재까지의 기보_autosave_meta.txt: 내 돌 색상, AI 난이도, 저장 시각
복구: 이어하기 vs 새로 시작
앱을 열 때 저장된 대국이 있는지 확인한다.
def load_autosave():
if not AUTOSAVE_FILE.exists():
return None
sgf = AUTOSAVE_FILE.read_text()
meta = AUTOSAVE_META.read_text().split("n")
# 24시간 지난 저장은 무시
save_time = datetime.fromisoformat(meta[2])
if (datetime.now() - save_time).total_seconds() > 86400:
clear_autosave()
return None
return {"sgf": sgf, "player_color": meta[0], "level": meta[1]}
저장된 대국이 있으면 선택지를 보여준다.
⏸️ 진행 중이던 대국이 있습니다 (5급, 47수)
[▶️ 이어하기] [🗑️ 새로 시작]
“이어하기”를 누르면 SGF를 파싱해서 수순을 복원한다.
def start_game(resume_sgf=None):
engine = KataGoGTP(level=st.session_state.level)
engine.start()
engine.new_game()
if resume_sgf:
# SGF에서 수순 추출 후 재현
for color, coord in parse_sgf_moves(resume_sgf):
engine.play_move(color, coord)
sync_board_from_engine()
정리 타이밍
자동 저장 파일은 언제 삭제할까?
- 정상 종료: 기권, 게임 오버 → 완료된 기보로 저장 후 삭제
- 퇴실: 사용자가 나가기 버튼 클릭 → 저장 후 삭제
- 새로 시작: 이어하기 거부 → 즉시 삭제
- 24시간 경과: 오래된 저장은 자동 무시
def end_game():
if st.session_state.engine:
st.session_state.engine.stop()
st.session_state.game_started = False
clear_autosave() # 정리
결과
이제 대국 중에 브라우저를 닫아도 괜찮다. 전화가 와도, 실수로 탭을 닫아도, 컴퓨터가 재시작되어도.
다시 앱을 열면 “이어하기” 버튼이 기다리고 있다.
사소해 보이지만, 이런 안전장치가 있어야 마음 편히 대국에 집중할 수 있다. 50수 날린 경험이 없었다면 만들지 않았을 기능이다.
—
다음에는 “물리기” 버튼이 한 번만 작동하던 문제를 다룰 예정이다. Streamlit의 버튼 상태와 plotly_events의 stale click 문제. 예상보다 깊은 토끼굴이었다.
