흩어진 작업 현황을 한 화면에 모아

2026.08 ~ 2026.09 (운영 중)

여러 Claude Code 세션 중 답변 대기·멈춤 세션을 한 화면과 메뉴바에 보여주는 로컬 대시보드입니다.

과제

  • 세션 목록 CLI는 작업 내용·브랜치·답변 대기 여부를 알려주지 않음
  • 기획 시점 실측: 살아 있는 세션 10개 중 7개가 답변 대기, 1개는 27일째 방치

담당 범위

  • 수집·상태 판별: Java / Spring Boot, 세션 기록 역순 읽기 (1인)
  • 실시간 화면: SSE + Vue 3 대시보드, macOS 메뉴바 앱(SwiftUI)
  • 배포·CI: launchd, 로컬 전용 바인딩 검사 게이트

설계 판단

  • 세션 기록을 끝에서부터 필요한 만큼만 읽음 → 파일이 커져도 속도가 거의 일정 (대안: 전체 파싱)
  • 상태를 레코드 타입 대신 메시지 블록으로 판정 → 도구 결과가 사용자 입력으로 기록되는 함정 회피
  • 외부 전송 없이 로컬에서만 동작하고 CI로 강제 → 세션 기록의 프롬프트·경로 보호

결과 지표

  • 역순 읽기 96ms vs 전체 파싱 1,684ms (17.5배, 기록 421MB 기준)
  • 테스트 54개, 백엔드 1,730줄 대비 테스트 1,238줄
  • 이틀 동안 23커밋, 단독 작업
원문 자세히 보기

과제

여러 프로젝트에서 Claude Code 세션을 동시에 띄워두고, 컨텍스트가 차면 새 세션을 여는 방식으로 쓰다 보니 세션이 계속 쌓였다. claude agents --json은 살아있는 세션 목록은 주지만 무슨 작업 중인지·어느 브랜치인지·내 답을 기다리는지는 알려주지 않는다. 그 정보는 세션 기록 파일에 따로 있다. 이 둘을 이어서 '지금 내 답을 기다리는 세션'과 '멈춰 있는 세션', '컨텍스트가 얼마나 찼는지'를 한 화면에 놓는 것이 목표였다. 기획 시점 실측으로 살아있는 세션 10개 중 7개가 답변 대기 상태였고, 한 세션은 27일째 떠 있었다. 만들고 끝낸 도구가 아니라 2026-09-04 확인 기준 매일 사용 중이며, 쓰면서 나온 불편을 계속 반영하고 있다.

담당 범위

Java 1,730줄 + 프론트 1,697줄 + Swift 894줄 · 테스트 54개 · 문서 2,332줄

1인 개발. (1) 데이터 수집 — claude agents --json을 ProcessBuilder로 실행해 살아있는 세션 목록을 얻고, 세션 기록 JSONL을 역순으로 읽어 제목·마지막 발화·컨텍스트 사용량을 붙이는 수집 계층, (2) 상태 판별 — 레코드 타입이 아니라 message.content의 블록(tool_use / tool_result)까지 봐서 '답변 대기 / 작업 중 / 멈춤 의심'을 가르는 순수 도메인 로직, (3) 성능 — 대용량 기록을 통째로 파싱하지 않고 NIO RandomAccessFile로 파일 끝에서부터 필요한 레코드까지만 읽는 역순 리더와 상한 튜닝, (4) API — @Scheduled 주기 수집 + SseEmitter 실시간 스트림, record DTO, (5) 프론트 — Vue 3 + Vite + Tailwind로 프로젝트별 그룹 화면과 SSE 구독 훅, (6) macOS 메뉴바 앱 — SwiftUI로 답변 대기 수를 메뉴바에 띄우고 SSE를 구독, 백엔드가 죽으면 되살리는 클라이언트, (7) 배포 — 빌드 스크립트와 launchd 에이전트, (8) CI — 빌드·테스트에 더해 '바인딩이 로컬 전용 바인딩인가', '배포 산출물에 세션 데이터가 섞이지 않았는가'를 검사하는 게이트.

보기

선택안 · 대안 · 근거 · 비용

세션 기록을 통째로 파싱하지 않고, NIO RandomAccessFile로 파일 끝에서부터 필요한 레코드까지만 역순으로 읽는다. 읽을 상한(DEFAULT_MAX_BYTES)은 상한별 소요 시간과 정보 손실을 표로 뽑아 정했다

대안파일을 순차로 전부 파싱해 마지막 레코드를 찾거나, 수집 결과를 DB에 캐시해 두고 증분만 갱신하는 안근거필요한 것은 '마지막 user/assistant 레코드' 하나인데 기록 파일은 계속 자란다(2026-09-04 실측 138개 파일 총 421MB, 단일 최대 21MB). 앞에서부터 읽으면 파일이 커질수록 선형으로 느려지지만, 뒤에서 읽으면 파일 크기와 거의 무관해진다. 캐시를 두면 무효화 시점을 따로 관리해야 하는데 5초 주기 수집에는 과하다.비용상한을 넘어가는 위치에 필요한 레코드가 있으면 제목이나 마지막 발화를 못 찾는다 — 그래서 상한을 하나로 못 정하고 손실과 시간을 같이 재는 벤치마크를 남겼다(실측: 64K에서 발화 없음 56건 → 1024K에서 3건, 대신 67ms → 136ms). 또 역순 읽기는 줄 경계를 직접 다뤄야 해서 순차 읽기보다 코드가 까다롭고, 인코딩·개행 처리 실수가 조용한 버그로 남는다.

세션 상태를 레코드 타입(user/assistant)이 아니라 message.content의 블록까지 보고 판정한다 — assistant이면서 tool_use면 작업 중, text로 끝나면 답변 대기, user이면서 tool_result면 작업 중

대안레코드 타입만으로 '마지막이 assistant면 답변 대기'로 단순 판정하는 안근거도구 결과가 type: "user"로 기록된다. 이걸 사용자 입력으로 오인하면 '작업 중'과 '답변 대기'가 정확히 뒤바뀌는데, 이 도구의 존재 이유가 바로 그 둘을 가르는 것이라 판정이 틀리면 도구 전체가 무의미해진다. 함정이 user·assistant 양쪽에 대칭으로 있다.비용판정이 기록 파일의 내부 포맷에 묶였다. Claude Code가 레코드 구조를 바꾸면 조용히 틀린 상태를 보여주게 되고, 컴파일도 테스트도 그걸 잡아주지 못한다(형태가 바뀌어도 JSON 파싱은 통과한다). 그래서 판정부를 외부 의존 없는 순수 로직으로 떼어 JUnit으로 고정해 두었지만, 그건 '규칙이 유지되는 한' 맞다는 보장일 뿐이다.

중앙 서버·계정·외부 전송을 두지 않고 백엔드를 로컬 전용 바인딩에만 바인딩한다. 그리고 그 원칙을 문서가 아니라 CI 게이트로 검사한다 — '바인딩이 로컬 전용 바인딩인가', '배포 산출물에 세션 데이터가 섞이지 않았는가'

대안여러 기기의 세션을 한 서버로 모아 어디서든 보게 하거나, 최소한 LAN 바인딩으로 열어 다른 기기에서 접속하게 하는 안근거세션 기록에는 프롬프트 전문·절대 경로·브랜치명·PR 정보가 들어 있다. 남의 기계 데이터를 한곳에 모으면 유출 시 그 책임을 지게 되는데, 개인용 도구가 그 책임을 질 이유가 없다. 그리고 '외부로 안 나간다'는 문서에 적어두면 다음 커밋에서 조용히 깨질 수 있어 게이트로 만들었다.비용다기기에서 한 번에 보는 기능을 포기했다 — 기기마다 따로 띄워야 한다. 조회 전용 원칙도 함께 세워서 세션 종료·재개를 앱에서 못 한다(메뉴바 앱은 kill 명령을 클립보드로 복사해 주는 선까지만 한다). 잘못 눌러 작업이 날아가는 위험 대비 편의가 작다고 봤지만, 쓰는 사람 입장에서는 한 단계 번거로운 것이 맞다.

사용 기술

TypeScriptVueViteTailwind CSSVitestPlaywright