Personal Assistant - 초기 UI, 세션 유지
목차9
TL;DR
- 목적: 개인 맞춤형 데스크톱 어시스턴트 개발
- 핵심
- 이전 대화 내역 복구 방식
- Session-Warming을 통한 지연시간 최소화
개요
가장 처음 구현했던 것은 아무래도 “채팅창 열기 & 채팅 주고받기”와, 앞으로를 위해 다양한 데이터 소스를 연결하는 작업이었다.
특히 채팅창 세션 관리는 잘못 잡으면 두고두고 문제를 일으킬 것이 뻔하였기에, 특히 관심을 기울였던 부분이었다.
시도
초기 설계
내가 가장 처음에 생각했던 구조는 아래 이미지와 같았다:
-
PersonalAssistant + Mascot (시메지)
- 가장 기본이 되는 뼈대로,
CLAUDE.md,Skills,commands등이 모두 여기에 정의됨 - 굳이 Ide나 터미널을 띄우지 않고서도 필요한 기능들을 바로 사용 (즉, UI를 띄우겠다)
- 가장 기본이 되는 뼈대로,
-
Obsidian Vault
- NotebookLM이나 Web Search 결과, 기타 작업 결과를 로컬에 저장
- 일종의 캐시 레이어로, 이전에 했던 동일한 작업이 있다면 여기를 먼저 참고하도록 지정
- 내가 직접 시각적으로 문서 간의 연계를 확인해야 할 때, 보기 편한 툴이기도 함
-
NotebookLM
- 더 깊은 Insight나 Research가 필요할 때 사용하는 외부 소스
notebooklm-py를 사용했는데, 아무래도 Unofficial한 라이브러리이다보니 다른 구글 계정을 연동시켜놓았다.
-
Zapier MCP
- 여러 도구들이 제공하는 MCP를 통합하여 관리할 수 있는 플랫폼
- Gmail, Notion 등, 나중에 필요할 수 있는 요소들과 연동하기 위해 사용중
물론 여기서 각 기능들을 추가하면서 여러 외부 도구들과 연동되기도 하였으나, 큰 틀은 여기서 벗어나지 않도록 하였다. 복잡해질수록 나중에 손대기 더 힘들어질테니까.
구조
이 작은(?) 프로젝트를 시작하면서, NotebookLM 등을 통해 웹에서 다양한 소스들을 모아보았다. 초기에 내가 참고했던 사례는 크게 두가지였다.
- 데스크톱 마스코트 (시메지) - 바탕화면에서 캐릭터가 돌아다니거나 뛰어다니는 바탕화면 미디어. 사실 처음에는 Desktop Mate같이 움직이는 뭔가를 만들고 싶었는데, 거기까지는 내가 할 수 있는 영역이 아닌듯 하여 포기하였다.
- CodeWalkers - Tauri + Rust 기반, 데스크톱 펫이 CLI AI 에이전트를 스폰하고 대화 스트리밍을 파싱
초기에는 내가 써보았던 KMP(Kotlin Multi-platform)를 사용하고자 하였으나, 구현할 때는 CodeWalkers와 NekoAI(시메지) 프로젝트에서 뼈대를 많이 참고시켰기에, 결과적으로 Tauri v2 + Rust 기반의 프로젝트가 되었다.
물론 나는 Rust나 Tauri에 대한 지식이 거의 없었으므로, 가끔씩 코드를 봐도 거의 주석에 의존하고 있는 상황이긴 하다. 이를 해결하기 위해 별도의 워크플로우나 검증 및 테스트 과정을 구축하긴 했는데, 이건 나중 이야기이니 우선은 생략.
아무튼 이 프로젝트는 결과적으로 세 겹으로 구성되게 되었다:
-
Front
- WebView2 + HTML/CSS
- 캐릭터 Sprite, 채팅/말풍선/권한버블 등의 UI 요소
-
Back
- Rust (Tauri v2 core) 기반
- claude 프로세스 스폰, 권한 관리, 스케줄러 등 ‘순수 알고리즘’으로 처리해야 할 영역
-
Brain + Mind
- Personal Assistant Repository
- 구조화된 폴더로, 행동 지침, 스킬, 로그 등을 관장
- 실제로 LLM의 ‘사고’가 필요할 때, Back 쪽에서
claude -p스폰
Problem & Solve
이 과정에서, 몇 가지 문제를 마주했었다.
1. 이전 대화 내역을 어떻게 복구해야하는가?
작년 이맘때 쯤, OpenAI API를 연동하여 간단한 챗봇을 만드는 프로젝트를 해본 적 있었다.
그 당시에는 이쪽 분야에 대한 지식이 없었기에 아예 별도로 세션 메모리를 구현했었고, 이번에도 그래야하나 싶었었다.
그런데 어차피 Claude code가 터미널에서 /resume으로 이전 대화를 불러올 수 있다는 뜻은, 원격(데이터베이스)에서 땡겨오거나, 세션이 디스크에 저장된다는 뜻 아닐까?
역시 찾아보니, 클로드는 모든 세션을 jsonl 포멧으로 저장하고 있었다. 즉, 디스크에 파일만 살아있으면 --resume <id>로 바로 복원할 수 있었다. (이래서 잘 알아보고 해야한다)
물론 이 방법은 어디까지나 ‘과거 채팅 내역을 불러오는’ 과정일 뿐, 실제로 “내가 지난주에 xyz 했던거, 어떻게 되었지?” 와 같은 질의가 날아갈 시, 전체 jsonl을 다 뒤져볼 수가 없는 노릇이다. 여러 채팅방이 생기면 더 곤란하기도 하고.
그래서 grep 전용 메타 데이터를 하나 만든 후, 각 대화의 주요 내용들을 저장하였다.
<!-- 자동 갱신: history-index.py (Stop hook). 손편집 금지 — jsonl에서 재빌드 가능. -->
<!-- 형식: - `날짜` `sid8` `branch` — 제목 ·lazy (요약되면 ·lazy → **topic,…** + 1줄요약) -->
<!-- 검색: 날짜범위 grep '`2026-06-1[3-9]' · 토픽 grep 키워드 · 미요약 grep '·lazy' -->
...
- `2026-07-12` `a139d36e` `master` — 권한 버블 발화 지연 및 응답 속도 개선 ·lazy
즉, <날짜>-<세션 id>-<브랜치>-<제목>-<추가내용> 다섯가지 항목을 저장해둔 것이다. 이때도 무조건 모든 대화를 요약하는 방식이 아닌, 사용자가 명시적으로 “그거 어떻게 되었지”라고 물을 때만 Lazy하게 업데이트되도록 하였다. 내 토큰은 소중하니까.
그리고 여기서, 한가지 중요한 점을 배웠다:
파일을 잘게 쪼개서 나눠놓는 전략은, 그 파일의 모든 텍스트가 input으로 필요할 때 고려. 구조화된 형식의 파일은
grep을 시키는 방식으로 가는 것이 오히려 유리할 수 있음.
grep은 필요한 그 라인만 가져오기에 자연스럽게 input token이 줄어든다는 이 당연한 사실을 왜 이제야 생각했을까!
(사실 일반 claude code에게 작업 시키면 grep을 많이 쓰기는 한다.)
물론 프로젝트 규모가 커진다면 한계가 올 수 있겠으나, 그 전에 문서 분할(yy-MM-dd 단위라던지), 장기적으로는 Vector DB 기반의 RAG도 고려해볼 수 있지 않을까 싶다. 근데 이 상황을 고려할 정도로 프로젝트 규모가 커질까?
아무튼 이 아이디어는, 이후 로깅, Handoff 등의 여러 시스템에서 사용하게 되었다.
2. 응답속도 개선을 위한 Session Warming
초기에는, 단순히 spawn-per-message 방식을 취했었다: 즉, 메시지마다 claude를 새로 스폰하되, 연속성은 --resume으로 유지하는 방식.
즉 새로운 입력이 들어가면 → 자동으로 이전 세션을 다시 켜고 (터미널로 치면 다시 claude —resume) → 입력을 집어넣는 방식이었다.
이 방식은 단순하고 안전하기에 빠른 MVP 구현을 위해 취한 방식이었지만, 매 스폰 시 MCP나 Skill 등을 다시 불러와야하는 일종의 Cold start 방식이라 불필요한 지연이 발생하였다.
그리하여, “한번 스폰된 세션을 열어둔 채 여러 턴을 스트리밍하는 구조”로 변경하였고, 오랜 시간 자리를 비웠을 경우에만 자원 회수를 위해 닫는 방식을 도입하였다(Session warming).
여기서 하나 더 도입한 것이 “지문(Fingerprint)“이다. 만약 Model이 바뀌거나(Opus → Sonnet), 작업 디렉토리, MCP 등이 변경될 경우 refresh가 반드시 필요하다. 이때, 내가 굳이 refresh를 명령하지 않더라도, ‘지문’이 자연스럽게 바뀌었음을 감지한 세션이 알아서 죽었다가 다시 깨어나도록 설계된 것이다.
idle-TTL이라고 이름붙인 이 시간은, 초기에는 10분으로 지정되었었다. 그러나 현재는 복잡한 작업을 처리하는 과정에서 보통 20분 이상 걸리는 경우가 많아졌고, 그 사이에 Host 세션이 idle 상태로 파악되어서 종료당해버리는 문제가 종종 발생하곤 하였기에 45분까지 확장해둔 상태이다.
결과
초기에는 단순하게 “터미널 외의 공간에서, 내가 구독한 Claude랑 소통할 수 있을까?” 라는 질문에서 시작했다.
도중에 UI와 관련된 내용도 조금 신경쓰긴 했는데, 어찌되었든 아래의 기본 기능들이 구현되었다:
- 스프라이트 적용
- 이건 마우스 아이콘을 이미지로 대체해서 잠시 사용하였다. 근데 아직도 사용하고 있음.
- 자연스러운 채팅 주고받기 및 마지막 대화 내용 기억
Comments