여러 프로젝트에서 Claude Code, Codex, Pi를 함께 쓰면 작업 상태를 확인해야 할 창도 늘어난다. Stream Deck의 키마다 작업 하나를 표시해 실행 중인지, 내 응답을 기다리는지 책상 위에서 확인할 수 있게 했다. 같은 상태를 웹에서도 보여 준다.
책상 위의 Stream Deck을 보다가 생각했다. 각 버튼에는 이미 작은 화면이 있다. 여기에 명령 이름 대신 지금 진행 중인 작업을 표시하면 어떨까. 일을 시작하는 버튼만큼, 일이 어떻게 되어 가는지 보여 주는 화면도 쓸모가 있을 것 같았다.
실행 중인 작업의 키에는 작은 표시가 움직이고, 내 응답이 필요한 작업의 키는 주황색으로 표시된다. 마지막 키는 전체 활성 작업 수를 보여 준다. 그 키를 누르면 같은 작업을 더 넓게 펼쳐 놓은 웹 화면이 열린다.
실제 운영에서는 원격 머신의 에이전트가 수행하는 작업의 상태도 함께 수집한다. 이 글에 딸린 예제는 한 Linux 머신의 Ghostty 안에서 실행한 Claude Code, Codex, Pi만 다룬다. 원격 서버나 별도의 작업 관리 시스템 없이 수집에서 화면까지 따라가 볼 수 있도록 범위를 줄였다.
Omarchy에서 작은 화면에
표시할 정보를 골랐다.
Omarchy를 쓰면서 창과 작업공간을 작은 스크립트로 다루는 일이 익숙해졌다. Stream Deck도 이 환경에 연결해 보고 싶었다. 이전 작업에서는 Neo의 키에 작업공간별 창 배치를 그렸다. 화면 전체를 작게 줄인 썸네일은 96픽셀 안에서 알아보기 어려워, 창의 위치와 크기를 단순한 도형으로 표현했다.
그때 배운 점은 작은 화면에 많은 정보를 그대로 넣기보다, 멀리서 알아볼 수 있는 구조를 골라야 한다는 것이었다. 에이전트 모니터에서도 대화 내용을 옮기는 대신 에이전트 종류, 프로젝트, 짧은 작업 제목과 상태만 남겼다.
Omarchy에서는 Linux의 프로세스 정보, 각 CLI가 남기는 상태 신호, Python 장치 라이브러리를 평소 쓰던 터미널과 사용자 서비스에 연결할 수 있었다. 한 번 준비하고 나니 장치를 꽂으면 작업 화면이 나타나는 경험이 특히 좋았다. macOS와 Windows에서도 이런 활용은 가능하다. 운영체제별 차이와 공식 앱을 활용하는 방법은 장치 배치 설명 뒤에서 비교한다.
상태는 한 번 모으고,
화면은 두 곳에 그린다.
처음에는 버튼에 무엇을 그릴지 생각했다. 그런데 웹에도 같은 내용을 보여 주려면 상태를 결정하는 곳부터 분리해야 했다. 장치가 로그를 읽고 웹이 따로 로그를 읽으면, 종료 판단이나 전체 수가 조금씩 달라질 수 있다.
그래서 CLI별 수집기가 신호를 공통 상태로 바꾸고, 서버가 하나의 스냅샷을 만든다. 웹과 장치는 그 스냅샷을 받는다. epoch는 서버가 다시 시작됐는지 구별하고, revision은 같은 서버에서 갱신 순서를 구별한다.
공통 스냅샷을 선택했습니다. 같은 작업 ID와 집계 결과를 두 화면으로 전달합니다.
그림 2. 세 CLI의 신호를 공통 스냅샷으로 바꾼 뒤 Stream Deck과 웹에 전달한다.
공개 예제는 2초마다 로컬 세션을 확인하고 전체 스냅샷을 SSE로 보낸다. 구독자의 처리 속도가 느리면 중간 스냅샷을 쌓아 두지 않고 최신 스냅샷만 전달한다. 새 연결에도 바로 현재 스냅샷을 보낸다. 화면은 빠르게 움직여도 수집까지 같은 속도로 반복할 필요는 없다.
원격 작업도 같은 스냅샷에 모은다.
실제 운영에서는 여러 머신의 작업을 한꺼번에 본다. 각 원격 머신의 작업 관리 시스템이 에이전트와 부모/자식 작업의 상태를 관리하고, 모니터 서버가 그 시스템에 연결한다. 원격 터미널의 화면을 캡처해 읽거나 모델에게 진행 상황을 묻는 구조는 아니다.
모니터 서버 안에는 원격 서버별 수집기가 있다. 각 수집기는 인증된 연결로 WebSocket 이벤트를 구독하고, HTTP 조회로 현재 상태를 주기적으로 대조한다. 운영 구현에서는 상태를 15초마다, 작업 목록 구성을 60초마다 다시 확인하며 목록 변경 이벤트가 오면 더 일찍 조회한다. 로컬 수집 결과도 같은 서버에 합쳐진다.
그림 3. 점선은 서버 사이의 연결이다. Stream Deck USB 출력은 장치를 꽂은 PC에서 수행한다. 저장소의 예제는 로컬 수집 경로만 포함한다.
작업 ID는 서버 ID와 함께 식별하므로 서로 다른 머신의 같은 ID가 섞이지 않는다. 같은 머신에서 원격 관리 대상과 로컬 CLI가 겹치는 경우에는 중복 집계를 제거한다. 한 서버가 끊겨도 다른 수집기는 계속 동작한다. 끊긴 소스의 마지막 상태는 남겨 두고 전체 수를 미확정으로 표시한다.
원격 접속 주소와 인증값은 모니터 서버의 비공개 설정에 둔다. 브라우저와 USB 클라이언트는 공통 상태 서버를 구독하므로 각 원격 서버의 자격증명을 받을 필요가 없다. 이 글의 공개 데모는 예시 데이터로만 동작한다. 내려받는 코드는 로컬에서 실행한 Claude Code, Codex, Pi만 지원하며, 원격 수집기와 접속 설정은 포함하지 않는다.
에이전트에게 묻지 않고,
에이전트가 남긴 신호를 읽는다.
상태를 알아내기 위해 모델에게 “지금 일하고 있나요?”라고 물으면 관찰 자체가 새 작업이 된다. 프로세스가 살아 있다는 사실만으로도 부족하다. 입력을 기다리는 터미널과 열심히 일하는 터미널은 둘 다 살아 있다.
예제는 현재 사용자의 프로세스 중 Ghostty의 자손인 에이전트만 찾는다. PID와 프로세스 시작 시각을 함께 확인해 같은 번호를 재사용한 다른 프로세스와 구별한다. 여러 프로필을 쓰는 경우에도 해당 프로세스의 환경과 열린 파일을 기준으로 상태가 기록된 위치를 찾는다.
| 에이전트 | 읽는 신호 | 이 예제의 범위 |
|---|---|---|
| Claude Code | 로컬 세션 상태 파일 + 관찰 훅 | busy/shell은 실행. 질문, 승인 대기는 훅으로 보완. |
| Codex | 살아 있는 CLI가 열고 있는 rollout JSONL + 선택적 관찰 훅 | 턴 시작/완료/중단, 동기 입력 요청, 명시적 부모 관계. |
| Pi | 확장이 기록하는 상태와 주기적으로 남기는 생존 신호 | agent_start/agent_settled, UI 대기 시작/종료. |
Claude Code의 훅은 상태 메타데이터만 기록하고 아무 결정도 반환하지 않는다. 기존 훅을 지우거나 승인을 대신하지 않는다. PermissionRequest와 Notification은 서로 다른 시점에 발생할 수 있어 둘을 구분해 처리한다. Claude Code 훅 문서
Codex의 rollout 파서는 이 환경의 CLI 0.153.4에서 관찰한 내부 형식에 맞춘 어댑터다. 모든 Codex 인터페이스에 통하는 공개 구독 API로 취급하지 않는다. 이미 열린 터미널을 관찰하려고 택한 방법이며, 새 클라이언트를 만드는 경우에는 turn/started와 turn/completed 같은 알림을 제공하는 공식 App Server도 검토할 수 있다. 이 예제는 App Server를 별도로 띄워 기존 CLI를 감시하는 방식이 아니다. Codex App Server 문서
Pi에서는 종료 신호 선택이 중요했다. agent_end 뒤에도 자동 재시도나 후속 처리가 이어질 수 있다. 그래서 더 이상 자동으로 이어질 작업이 없는 agent_settled를 확인한다. 확장은 2초마다 동작 중임을 기록하고, 8초 넘게 갱신이 없으면 수집기는 상태를 미확정으로 둔다. 이 8초는 작업 완료를 판정하는 시간이 아니다. Pi 확장 문서
// pi-monitor.ts에서
pi.on('agent_start', (_event, ctx) => {
context = ctx; status = 'active'; write(ctx);
});
pi.on('agent_settled', (_event, ctx) => {
status = ctx.isIdle() ? 'idle' : 'active'; write(ctx);
});
CLI 상태 파일과 로그 형식은 버전에 따라 바뀔 수 있다. 검증 기준은 Claude Code 2.1.261, Codex CLI 0.153.4, Pi 0.85.1이다. 읽을 수 없거나 형식을 판별하지 못하면 unknown으로 표시한다. Codex의 승인 대기 보완은 해당 버전에서 훅이 로딩된 세션에 한하며, 모든 종류의 비동기 질문을 완전히 추적하는 예제는 아니다.
움직임은 실행을,
주황색은 내 차례를 알려 준다.
키의 상태를 켜짐과 꺼짐으로만 나누면 어떤 작업이 내 응답을 기다리는지 알아보기 어렵다. 실행 중, 응답 대기, 방금 종료, 상태 미확인을 따로 표현했다. 색만으로 구별하지 않도록 상태 글자와 기호도 함께 넣었다.
실행 중실행 중입니다. 기존 작업 6개와 이 작업 1개를 합쳐 전체 활성은 7개입니다.
그림 4. 바탕의 다른 작업은 실행 5개, 응답 대기 1개로 고정했다. 그래프는 시나리오의 예정된 전환을 보여 주며, 연결을 끊으면 커서 시점의 관찰 상태를 유지한다.
작업이 종료되면 해당 키의 애니메이션이 멈추고, 키는 회색으로 30초 동안 남는다. 막 끝난 일이 무엇인지 볼 시간을 주기 위해서다. 활성 수에서는 종료를 확인한 순간 빠진다. 화면에 키가 남아 있다는 이유로 계속 실행 중으로 세지 않는다. 30초 안에 같은 작업이 다시 시작되면 그 자리를 이어 쓴다.
연결이 끊기거나 상태를 해석하지 못한 경우도 움직임을 멈춘다. 하지만 완료 표시를 하지는 않는다. 마지막으로 확인한 상태를 보존하고 전체 수에 미확정 표시를 붙인다. 처음부터 상태를 모르는 세션은 물음표 키로 나타난다. 숫자 0과 “알 수 없음”은 다른 정보다.
애니메이션은 진행률이 아니다. 실행 중임을 알리는 작은 표시를 장치에서 초당 4프레임으로 움직일 뿐이다. 한 시간 걸릴 작업의 절반이 끝났다는 뜻으로 읽히지 않게 했다. 웹은 같은 의미를 CSS로 표현하고, 사용자가 움직임 줄이기를 선택하면 정지 상태로 보여 준다.
마지막 키에 적힌 숫자는
무엇을 세는가.
작업을 몇 개 맡겨 두었는지 보려면 집계 단위를 먼저 정해야 한다. 운영판은 작업 관리 시스템의 셀 하나를 한 작업으로 센다. 한 셀에 부모와 여러 자식이 있어도 하나다. 로컬 예제에서는 부모 세션 하나가 그 단위가 된다.
자식이 둘 돌아가면 부모 키에 S2를 적는다. 전체 수에 둘을 더하지는 않는다. Codex의 자식 세션은 로그에 명시된 부모 관계에 따라 분류하고, Claude Code의 자식 세션은 관찰 훅으로 표시한다. Pi 예제는 부모 세션의 상태를 보여 준다. 자식 상태를 독립적으로 확인할 수 없는 경우까지 추측해 채우지는 않는다.
예제의 웹 통계 버튼은 현재 실행 중인 작업 수, 응답을 기다리는 작업 수, 종료 후 화면에 남아 있는 작업 수와 열린 세션 수를 보여 준다. 응답을 기다리는 자식이 있으면 해당 부모를 대기 쪽으로 분류해 실행/대기 합계가 전체 활성과 같도록 했다. “오늘 완료한 작업 수”나 “동시에 실행되는 모든 자식의 수”와는 다른 지표다.
장치의 마지막 숫자 키를 짧게 누르면 전체 웹 목록이 열리고, 600ms 이상 누르면 다음 페이지로 이동한다. 초과 표기는 기존 구현의 규칙을 유지했다. Neo에서 12개라면 8+4로 표시한다. 이 표기는 총 12개라는 뜻이고, 화면에 숨겨진 개수와는 다르다. Neo의 실제 작업 자리는 통계 키를 뺀 7개다.
운영판에서는 장기적인 변화를 확인하기 위해 부모 작업 수와 부모/자식 에이전트 수를 따로 저장했다. 로컬 예제는 여기서 범위를 줄여 /metrics에 현재 부모 세션 수를 내보낸다. 과거 값 보관이나 Grafana 설치는 포함하지 않는다. 상태가 미확정이면 수치를 NaN으로 내보내 연결 장애가 “일이 없었던 시간”으로 기록되지 않게 했다.
15키에서도, 8키에서도.
꽂힌 장치에 맞춰 다시 배치한다.
Original로 만든 화면을 Neo에 연결했을 때 처음에는 아무것도 뜨지 않았다. 라이브러리는 Neo를 알고 있었지만, 내 코드가 키 수가 15개인 장치만 선택하고 있었다. 지원 모델 이름을 늘리기보다, 연결된 장치에서 키 수, 배열, 이미지 크기를 읽도록 바꿨다.

작업 14자리 + 통계 1자리.

작업 7자리 + 통계 1자리. 아래 터치 키로 페이지 이동.
두 이미지는 작업 12개가 담긴 같은 스냅샷을 보여 준다. Original에서는 한 페이지에 표시하고, Neo에서는 두 페이지에 나누어 표시한다. 화면의 배치는 달라도 총수는 변하지 않는다. 케이스 모양은 도식이며, 글에서 읽기 좋도록 키와 정보창을 고해상도에서 다시 그렸다. 실제 장치로 보내는 픽셀을 보존한 Original / Neo 렌더링도 따로 볼 수 있다.
rows, columns = deck.key_layout()
key_count = deck.key_count()
key_size = tuple(deck.key_image_format()['size'])
task_keys = key_count - 1
# 마지막 키는 항상 전체 수. 나머지는 task_keys 단위로 페이지를 만든다.
이미지를 장치에 전송할 때는 라이브러리의 변환 함수를 거친다. 같은 PNG 크기라도 장치별 회전, 반전과 전송 형식이 다를 수 있기 때문이다. 키별 이미지 해시를 비교해 이미지가 달라진 키의 이미지 데이터만 USB로 전송한다. StreamDeck 장치 API
장치를 빼고 다른 모델을 꽂으면 재검색해서 배열과 버튼 상태를 새로 만든다. 두 모델을 같은 코드로 지원하는 기능이다. 여러 장치를 한꺼번에 꽂았을 때는 먼저 찾은 장치 하나를 사용하며, 두 USB 장치에 동시에 출력하지는 않는다.
macOS와 Windows에서도 상태 화면을 만들 수 있다.
두 운영체제에서도 같은 종류의 상태 디스플레이를 만들 수 있다. 구현 난이도를 비교하려면 키에 그림을 보내는 일과 에이전트의 상태를 알아내는 일을 나눠 봐야 한다. 이 글의 로컬 수집기는 Linux에 맞춰져 있지만, Stream Deck의 화면을 제어하는 방법은 여러 운영체제에 열려 있다.
공식 앱에서도 플러그인을 통해 작업 상태를 표시할 수 있다. Elgato SDK는 키별로 setTitle과 setImage를 제공하며, 실행 중에 만든 SVG나 PNG도 받을 수 있다. 미리 정한 키 이미지에는 GIF 애니메이션도 쓸 수 있다. 다만 setImage에 움직이는 GIF를 넘겨 재생하는 방식은 지원하지 않으므로, 데이터에 따라 움직이는 화면은 이미지 갱신 방식을 별도로 설계해야 한다. 키 이미지·제목 API, 애니메이션과 갱신 지침.
이 모니터에 필요한 추가 개발은 작업을 발견하고, 상태를 판정하고, 빈 키에 배치하고, 전체 수를 세는 로직이다. 버튼에 실행 명령과 아이콘을 배치하는 것만으로는 이 동작이 생기지 않는다. 공식 앱을 쓴다면 수집기의 공통 스냅샷을 받는 플러그인과 장치별 프로필을 만들 수 있다. SDK는 연결·해제 이벤트를 제공하고, 플러그인에 프로필을 묶어 배포할 수도 있다. 사용자가 키마다 작업을 수동으로 등록해야 하는 구조로 만들 필요는 없다. 장치 이벤트, 프로필 배포.
이 글에서 택한 직접 제어 방식도 Linux 전용은 아니다. python-elgato-streamdeck은 공식 앱 없이 장치를 제어하며, HIDAPI 설치 안내에 Linux·macOS·Windows가 모두 나온다. Elgato도 장치와 통신하는 HID 프로토콜을 공개하고 있다. macOS와 Windows에서는 공식 앱의 플러그인으로 통합할지, 이 예제처럼 별도 프로그램이 화면을 맡을지 선택할 수 있다. Python 라이브러리, 운영체제별 HIDAPI 준비, 공식 HID 문서.
| 환경 | 장치에 표시하는 경로 | 이 예제를 옮길 때 필요한 작업 |
|---|---|---|
| Linux / Omarchy | HIDAPI로 직접 출력. 라이브러리와 USB 접근 권한을 준비한다. | 현재 검증한 환경. /proc 기반 수집기와 기존 사용자 서비스를 연결했다. |
| macOS | 공식 앱 + 플러그인, 또는 HIDAPI 직접 출력. 후자는 Homebrew 등으로 HIDAPI를 설치한다. | Linux 프로세스 탐색을 macOS용으로 바꾸거나, CLI 훅·확장이 세션과 상태를 직접 등록하게 만든다. |
| Windows | 공식 앱 + 플러그인, 또는 HIDAPI 직접 출력. Python 직접 제어에는 맞는 HIDAPI DLL을 준비한다. | 네이티브 Windows와 WSL 중 CLI가 실행되는 곳에 맞는 수집기가 필요하다. USB 출력은 Windows 쪽에 둘 수 있다. |
Linux에서 개발이 편했던 이유는 /proc에 있었다. 프로세스 관계와 시작 시각, 환경과 열린 파일을 함께 확인해 이미 실행 중인 CLI와 상태 파일을 연결할 수 있었다. 이 탐색 부분은 다른 OS용으로 바꿔야 한다. Ghostty도 macOS와 Linux에서 제공되지만, macOS에 Ghostty가 있다는 이유만으로 Linux용 수집기가 그대로 작동하지는 않는다. 이식한다면 훅이나 확장이 공통 형식으로 세션을 등록하게 만드는 방법도 검토할 만하다. Linux 프로세스 정보, Ghostty 지원 환경.
Windows에서 에이전트를 WSL 안에 실행한다면, 수집기는 WSL에 두고 USB 출력은 Windows에 두는 구성을 고려할 수 있다. Windows 프로그램에서 WSL의 서버에 접속하는 경로가 있으므로 스냅샷을 그 사이로 전달하는 방식이다. 현재 예제의 Ghostty 자손 프로세스만 찾는 조건도 실제 실행 방식에 맞게 바꿔야 한다. USB까지 WSL에 넘기려면 별도의 usbipd-win 설정이 필요하고, 연결된 동안 Windows에서는 그 USB 장치를 사용할 수 없다. 따라서 이 경우에는 수집과 출력을 나눈 현재 구조가 도움이 된다. WSL 네트워크 연결, WSL USB 연결.
한 번 준비하면 장치만 꽂아도 화면이 나타난다.
내가 만족한 것은 한 번 설정한 뒤의 사용감이었다. 운영 환경에서는 사용자 서비스가 계속 실행되고, 장치가 없으면 기다리다가 연결되면 다시 연다. Original 대신 Neo를 꽂아도 키 수와 해상도를 읽어 작업 자리와 페이지를 다시 만든다. 매번 앱을 열어 버튼마다 작업을 지정할 필요가 없다.
여기에는 최초 준비가 있다. Linux에서도 HIDAPI 설치와 USB 접근 권한 설정이 필요하고, 수집기와 출력 프로그램을 실행해야 한다. 공개 예제는 이 과정을 따라 실행하는 코드이며, 자동 시작 서비스까지 설치하는 패키지는 아니다. Omarchy에서 이미 쓰던 서비스와 스크립트에 연결하기 쉬웠고, 그 위에 장치 재검색과 자동 배치를 구현한 덕분에 이후의 사용이 plug & play에 가까워졌다.
macOS와 Windows에서도 설치 과정에 수집기, 자동 시작, 장치별 프로필 또는 직접 출력 프로그램을 묶으면 비슷한 경험을 목표로 할 수 있다. 다른 사람에게 배포할 때는 공식 앱의 장치 관리와 플러그인 설치 경로를 활용하는 쪽이 편할 수도 있다. 내 환경에서는 화면 전체와 재연결을 Python 코드 하나에서 다루는 방식이 잘 맞았다. 이 선택의 이점은 운영체제 이름보다 평소 작업 환경과 얼마나 자연스럽게 연결되는가에 있었다.
운영체제 비교는 2026년 9월 16일 확인한 공식 SDK·HID 문서와 라이브러리 문서에 근거한다. 다른 OS의 구성은 이식 방안이며, macOS·Windows에서 이 예제를 실행해 검증했다는 뜻은 아니다. 공개 예제의 지원 범위는 Linux의 Ghostty 안에서 실행한 Claude Code, Codex, Pi로 유지한다.
노트북 한 화면에도
같은 작업을 펼쳐 놓는다.
작업이 많으면 물리 키 몇 개로는 전체를 한 번에 볼 수 없다. 웹에서는 Stream Deck의 표시 방식을 유지하면서 화면 전체를 작업 버튼으로 채웠다. 에이전트 표시, 프로젝트, 제목, 상태, 마지막 통계 버튼이 그대로 대응한다.

그리드는 현재 가로/세로 크기와 버튼 수로 행과 열을 정한다. 목록 자체에는 스크롤을 만들지 않는다. 검색하면 표시하는 키만 줄어들고, 전체 활성 수는 유지된다. 운영판에서는 별도 노트북에도 웹 모니터를 계속 띄워 둔다. 공개 예제의 서버는 루프백 주소만 사용하므로, 예제를 실행한 같은 컴퓨터의 브라우저에서 본다.
웹은 같은 작업 ID의 DOM을 재사용한다. 매번 카드 전체를 새로 만들면 상태 갱신 때마다 실행 애니메이션도 처음부터 다시 시작되기 때문이다. 이렇게 하면 데이터를 갱신할 때 같은 작업의 애니메이션이 처음부터 다시 시작되는 일을 막을 수 있다. 작업이 아주 많아지면 한 화면에 담는 대신 글자가 작아지는 한계는 남는다.
운영판의 작업 키는 해당 터미널이나 원격 작업 창을 여는 동작까지 연결되어 있다. 공개 예제에서는 작업 키를 누르면 웹의 해당 작업 상세가 열린다. 특정 창 관리자나 개인 창 선택 규칙을 설치하지 않아도 화면 흐름을 재현할 수 있다.
화면을 만들며 배운 것을
다음 구현에 적용한다.
기존 Codex 세션을 다시 읽어 보니 요구가 확장되는 지점이 분명했다. 처음에는 원격 작업의 활성 셀을 키마다 올리고 마지막에 총수를 표시하려 했다. 여기에 같은 내용을 보여 주는 서버와 웹이 붙었고, 로컬 터미널 수집, 상태별 애니메이션, 메트릭, 장치 자동 전환이 차례로 이어졌다.
공개 예제는 그 순서를 그대로 따라가지 않는다. 앞서 겪은 문제를 처음부터 고려해 더 적은 단계로 같은 구조를 만들 수 있게 했다.
- 상태와 집계 단위부터 고정한다. 실행/대기/종료/미확인을 정하고 부모 하나를 한 키로 센다. 화면과 숫자가 이 정의를 공유하게 한다.
- 가상 데이터로 끝까지 연결한다. CLI 설치 없이 스냅샷 한 장을 서버, 웹, USB 렌더러에 전달한다. 상태 수집과 표시 문제를 따로 확인할 수 있다.
- 실제 수집기를 하나씩 붙인다. Claude의
shell, Pi의 재시도 뒤 종료, Codex의 부모 관계처럼 도구마다 다른 의미를 어댑터에서 처리한다. - 입력 오류와 예외 상황부터 확인한다. 긴 제목, 줄바꿈, 불완전한 JSON을 입력하고, 상태 파일이 없거나 연결이 끊긴 상황도 확인한다. 정상 데이터에서 화면이 잘 보이는지만 확인하고 끝내지 않는다.
- 같은 데이터로 웹과 두 장치를 비교한다. 전체 수, 대기 상태를 강조하는 방식, 종료 후 키가 남는 동작이 일치하는지 확인한다. 페이지 수는 장치별 작업 자리 수에 맞는지 확인한다. 마지막에 실제 USB 전송 결과와 손으로 버튼을 눌러 확인한 결과를 따로 기록한다.
가장 분명한 실패는 전체 키가 반복해서 깜빡이던 문제였다. 애니메이션 주기나 USB 연결이 원인처럼 보였지만, 실제 원인은 제목 안의 줄바꿈이었다. 한 줄 텍스트 너비를 재는 함수에서 발생한 예외가 장치 루프 밖으로 전달되면서, 장치의 밝기를 끄고 재연결하는 동작이 반복됐다. 버튼 입력 상태도 그때마다 초기화됐다.
# 한 줄 폭 측정 전에 공백을 정규화한다.
title = ' '.join(cell['title'].split())
# 렌더 오류는 이 키의 대체 화면으로 한정한다.
# USB 재연결은 장치 연결 오류를 처리할 때 수행한다.
image = renderer.render(key, size, cell=cell, phase=phase)
수정에서는 줄바꿈을 정규화하고, 키마다 예외를 격리했다. 오류가 난 키에는 DISPLAY ERROR를 남기고 다른 키의 표시와 입력은 계속 처리한다. 당시 장애 데이터는 두 해상도와 여덟 애니메이션 위상으로 총 160프레임을 재생해 오류가 사라졌음을 확인했다. 공개 예제에는 원본 작업 제목 대신 같은 조건을 만드는 합성 테스트가 들어 있다.
버튼을 누르는 동안에도 데이터가 갱신될 수 있다. 누르기 시작할 때의 작업을 기억하고, 손을 뗄 때 그 자리에 다른 작업이 있으면 버튼 동작을 취소한다. “키 위치”만 기억해서 갱신 직후 엉뚱한 작업을 여는 일을 막기 위한 규칙이다.
로컬 세 종류만 남긴
작은 실행 예제.
Agent Deck 저장소에 수집기, 상태 서버, Stream Deck 클라이언트와 웹 화면을 함께 정리했다. README에서 설치, 에이전트별 설정, 장치 연결을 순서대로 따라갈 수 있다.
처음 실행하면 가상 작업을 보여 주므로 USB 장치나 에이전트 설치 없이 웹 화면부터 확인할 수 있다. 실제 수집은 Linux의 Ghostty 안에서 실행한 Claude Code, Codex, Pi를 대상으로 한다. 서버는 같은 컴퓨터에서만 접속할 수 있으며, 실제 수집 모드에서는 작업 제목과 프로젝트 이름이 화면에 표시된다.
검증한 범위와
실물에서 더 확인할 것
장치의 키 이미지를 정확히 만들었다고 해서 책상 위 LCD에 표시된 내용도 잘 읽히는 것은 아니다. 자동 검증 결과만으로 실제 사용까지 확인했다고 쓰지는 않았다.
| 대상 | 근거와 범위 |
|---|---|
| 이 글의 이미지 | 합성 작업 데이터 + 공개 예제의 키 렌더 함수. 소개 이미지는 큰 해상도에서 다시 그렸고 원해상도 렌더링을 별도 보존했다. 웹은 같은 데이터의 브라우저 캡처. 실물 사진은 아님. |
| 공개 예제 | 상태/입력/장치 회귀 테스트, Pi 실제 확장 로더, HTTP/SSE, 브라우저 검증. |
| 기존 운영 기록 | Original과 Neo의 실제 USB 전송, Original → Neo → Original 가상 교체, 장애 데이터 160프레임 재생을 확인. |
| 별도 확인이 필요한 것 | 사용자가 물리 버튼을 직접 눌렀을 때의 동작과 실제 LCD 화면이 얼마나 읽기 쉬운지. 이번 글을 작성하면서 운영 장치를 다시 조작하지는 않았다. |
Stream Deck과 웹은 같은 스냅샷을 받아 작업 상태와 전체 활성 수를 보여 준다. Stream Deck에서는 작업마다 키 하나를 사용하고, 응답이 필요한 작업은 주황색으로 표시한다. 여러 터미널을 열어 보던 작업 상태를 책상 위의 작은 화면에서 확인할 수 있게 했다.