본문 바로가기

만들기

폰으로 보고, 알림 받고, 맥미니 켜면 저절로|스테이터스 위젯 제작기 2편

📌 AI 사용량 위젯 제작기 2편입니다. 1편에서 "토큰 0으로 한도 보는 창"을 만들었다면, 이번엔 그걸 아이폰에서 보고, 한도가 차면 알림을 받고, 맥미니를 켜면 저절로 뜨게 키운 이야기예요. 중간중간 삽질도 있습니다. 결론만 궁금하시면 맨 아래 최종 요약으로 가셔도 돼요.

1편 「토큰 0으로 AI 사용량 보는 위젯 만들기」에서 만든 건 맥미니 화면에 떠 있는 창 하나였어요. 저는 맥미니에 모니터를 따로 안 달고 아이패드를 화면으로 쓰거든요(사이드카). Claude Code·Codex 한도, 지금 일하는 중인지, 어떤 모델로 얼마나 썼는지를 AI 모델을 한 번도 부르지 않고 보여 주는 창이죠. 그런데 며칠 써 보니 아쉬운 게 세 가지 생기더라고요.

📎 궁금하신 분들은 지난 「아이패드만으로 맥미니 주모니터 사용하기|사이드카 연결부터 HDMI 더미플러그까지」 포스팅을 참고하세요 → 글 보러 가기

다시 보는 위젯 화면(10/20 아이폰 폭으로 캡처). 이번 편은 이 창을 밖으로 꺼내는 이야기예요

  • 아이패드 화면에서만 보인다: 소파에서 아이폰으로 Claude 일을 시켜 놓으면, 한도 보려고 아이패드를 다시 켜서 맥미니 화면을 띄워야 했어요.
  • 내가 봐야만 안다: 한도가 95%가 돼도 창을 안 보고 있으면 모르고 지나가요.
  • 맥미니를 다시 켜면 꺼져 있다: 재부팅할 때마다 서버를 손으로 다시 켜야 했어요.

이 세 개를 하나씩 풀고, 마지막에 로컬 LLM 사용량까지 같이 세게 만든 순서로 적어 볼게요.

1. 아이폰·아이패드에서 보기: 인터넷엔 안 열고

위젯 화면은 원래 맥미니 안에서 도는 작은 웹 서버가 그려 줘요. 그러니 아이폰 브라우저로 그 주소에 들어가기만 하면 되는데, 문제는 이걸 인터넷에 공개하고 싶지는 않다는 거였어요. 제 AI 사용 기록이 담긴 화면이니까요.

그래서 Tailscale을 썼어요. 제 기기들끼리만 묶인 개인 네트워크(tailnet)를 만들어 주는 서비스인데, 여기에 들어온 제 아이폰·아이패드에서만 위젯 주소가 열리고 바깥 인터넷에서는 안 보여요. 외부 공개 기능(funnel)은 켜지 않았고요.

💡 tailnet: 내 기기끼리만 쓰는 사설 네트워크라고 보시면 돼요. 회사에서도 집 맥미니에 붙을 수 있지만, 로그인한 내 기기가 아니면 주소를 알아도 못 들어와요.

그다음엔 사파리 "홈 화면에 추가"로 앱처럼 띄울 수 있게, 앱 이름·아이콘이 든 설정 파일(manifest)을 붙였어요. 주소창 없이 꽉 찬 화면으로 열려서 진짜 앱 같더라고요. 다른 앱 갔다가 돌아오면 끊긴 실시간 연결을 다시 잇는 코드도 넣었고요.

삽질: 스크롤만 하면 화면이 멈춘다

아이폰에서 써 보니 이상한 일이 생겼어요. 표를 손가락으로 쓱 스크롤하고 나면 숫자가 더 이상 안 바뀌는 거예요. 연결 표시는 초록불 그대로인데요.

원인은 터치 이벤트였어요. 위젯은 "사용자가 누르고 있는 동안엔 화면을 다시 그리지 않는다"는 규칙이 있는데, 아이폰에서 스크롤하면 손을 뗄 때 "뗐다(pointerup)" 신호 대신 "취소됐다(pointercancel)" 신호가 오더라고요. 그래서 위젯은 계속 누르고 있는 줄 알고 갱신을 멈춘 거였어요. 취소 신호에서도 "뗐다"로 처리하게 한 줄 고치니 해결됐어요. 표를 옆으로 밀어 둔 위치가 갱신 때마다 처음으로 튀던 것도 같이 고쳤고요.

참고로 아이폰 홈 화면 위젯(앱 아이콘 옆 작은 카드)도 생각했는데, 그건 실시간으로 바뀌지 않아서 보류했어요.

2. 맥미니 켜면 저절로 뜨게: launchd

맥에는 로그인할 때 프로그램을 띄워 주고, 죽으면 다시 살려 주는 launchd라는 관리자가 있어요. 여기에 위젯 서버와 창을 등록하는 명령을 만들었어요.

💡 launchd: 윈도우의 "시작 프로그램 + 서비스 자동 재시작"을 합친 맥의 기본 관리자예요. 설정 파일(plist) 하나로 "로그인 때 켜기", "비정상 종료면 다시 켜기"를 정할 수 있어요.

Terminal
# 로그인 때 서버·창 자동 시작 등록
python3 tools/ctl.py install

# 상태 확인
python3 tools/ctl.py status

# 등록 해제(되돌리기)
python3 tools/ctl.py uninstall

서버는 비정상으로 꺼지면 30초 뒤에 다시 켜지고, 창은 로그인 때 한 번만 떠요. 정말 그렇게 도는지 직접 확인했어요.

해 본 것 결과
일부러 끄기(stop) 40초 동안 꺼진 채 유지(내가 끈 건 안 살림)
강제 종료(kill -9) launchd가 다시 띄움
설치 후 데이터 수집 모든 수집 항목 정상, 아이폰 접속도 정상

삽질: 테스트가 진짜 서버를 꺼 버렸다

등록하고 나서 테스트를 돌렸더니 위젯이 꺼졌어요. 알고 보니 "켜기·끄기" 기능을 검사하는 테스트가 임시 서버가 아니라 방금 등록한 진짜 자동 시작 항목을 내려 버린 거였어요.

검사하다가 검사 대상을 끈 사람

그래서 "자동 시작으로 설치돼 있다"는 판정을, 테스트용 포트·경로 설정이 없을 때만 참이 되게 바꿨어요. 이 사고가 다시 안 나도록 그 경우를 검사하는 테스트도 하나 더 붙였고요.

로그인 항목에 "python"이 두 개?

맥 설정의 로그인 항목을 열어 보니 이름이 "python"인 게 두 개 떠 있었어요. 뭐가 뭔지 모르겠더라고요. 그래서 "Agent Status"라는 이름과 아이콘을 가진 작은 앱 껍데기를 만들어 연결했어요. 이제 로그인 항목과 Dock에 "Agent Status"로 보이고, Spotlight에서 이름으로 찾아 열 수도 있어요.

3. 한도 차면 알림: 맥미니 + 아이폰(워치는 따라오는지 미확인)

이제 "안 보고 있어도 알게" 할 차례예요. 규칙은 이렇게 정했어요.

  • 언제: Claude·Codex의 5시간·주간 한도가 80%, 95%를 넘을 때. 같은 창에서 기준값마다 딱 한 번.
  • 리셋되면: 기준을 넘었던 창이 리셋되면 한 번 더 "리셋됐어요".
  • 오래된 값으론 안 보냄: 1편에서 말한 "모르면 0%라고 하지 않기"와 같은 이유예요. 낡은 숫자로 알림이 오면 믿을 수가 없으니까요.
  • 내용: 숫자와 시각만. 대화 내용은 들어가지 않아요.

알림 제목은 한눈에 알아보게 "⚠️ 한도 95% 넘음 · Claude 5시간", "✅ 한도 리셋 · Claude 5시간"처럼 붙였어요. 맥미니(아이패드 화면)는 알림 센터로, 아이폰은 홈 화면 앱의 "알림 받기" 버튼을 눌러 등록하는 웹 푸시(iOS 16.4부터 지원)로 보내요. 워치는 아이폰 알림이 넘어가는 대로 따라가고요.

잠긴 아이폰에 첫 알림이 떴을 때

아이폰 알림이 오는 건 확인했어요. 워치에 넘어오는지는 그때 따로 확인하지 않았어요.

"읽기만 한다" 원칙에 붙인 예외 네 개

1편의 원칙은 "모델을 부르지 않는다"였지만, 위젯은 원래 맥미니 안의 기록을 읽어서 화면에 보여 주기만 하는 프로그램이었어요(밖으로 나가는 건 1편에서 말한 한도 조회와 짧은 핑 정도). 알림을 보내려면 그걸 조금 깨야 해서, 무엇을 깨는지 하나하나 적고 제가 승인했어요.

예외 이유와 제한
알림 등록·해제 요청 2개 그 밖엔 여전히 읽기 전용. 애플 푸시 주소만, 2KB 이하, 최대 10개
암호화 라이브러리 설치 웹 푸시는 내용을 암호화하고 서명해야 보낼 수 있어서
애플 푸시 서버로 전송 알림 내용을 처음으로 바깥(애플 푸시 서버)에 보내는 곳. 실패하면 최대 20번까지만 재시도
Claude 설정 파일 수정 아래 설정 패널에서 제가 버튼을 누를 때만

이 기능은 구현 담당 서브에이전트(implementer) 세 개를 동시에 띄워 나눠서 동시에 만들게 했고(푸시 암호화 / 알림 규칙 / 설정 바꾸기), 계획 단계와 구현 단계에서 한 번씩 Codex 리뷰를 받아 지적을 반영했어요. 리뷰를 다른 회사 AI에게 맡기는 방식은 예전에 따로 정리한 적이 있어요.

📎 궁금하신 분들은 지난 「Claude Code에 Codex 리뷰어 붙이기」 포스팅을 참고하세요 → 글 보러 가기

맥미니 위젯 창의 설정 패널: 서브에이전트 모델·노력도까지

맥미니에 뜨는 위젯 창에는 ⚙ 버튼으로 여는 설정 패널도 넣었어요. 설정 파일을 직접 열어 고치던 걸 위젯에서 바로 바꾸는 거예요. 바꿀 수 있는 건 이렇습니다.

맥미니 위젯 창에서 ⚙를 누르면 열리는 설정 패널(10/20 캡처). 팀원마다 모델과 노력도를 따로 고를 수 있어요

설정 패널의 팀원 이름이 서브에이전트 글이랑 다른 건 1편에서 말한 대로 그 사이 팀을 다시 짜서예요(7명 → 지금 8명, 로컬 LLM 담당 text-drafter는 정리). 이 이야기는 따로 정리해서 올릴게요.

  • Claude 기본 모델: 새 대화를 시작할 때 쓰는 기본 모델을 고릅니다(Claude Code 설정 파일의 model 값 하나).
  • 서브에이전트별 모델: 코드 찾는 애, 리뷰하는 애처럼 팀원마다 쓸 모델을 따로 정해요(각 팀원 파일의 model: 줄).
  • 서브에이전트별 노력도: 얼마나 깊게 생각하게 할지예요. low·medium·high·xhigh·max 중에 고르고, "기본값"을 고르면 그 줄을 지워서 대화 쪽 설정을 따르게 해요. 모델이 지원하지 않는 단계를 고르면 그 아래 지원 단계로 내려가요.
  • 데스크톱 앱용 짧은 핑: 1편에서 말한 예외 기능을 켜고 끄고, 주기를 1~60분(기본 2분)으로 바꿔요. 범위 밖 숫자는 저장하지 않고 안내만 해요.
  • 알림 기준값: 몇 %에서 알릴지 정해요(바로 위 알림 기능의 기준값).
  • 자동 시작 켜기·끄기: 다음 로그인부터 적용돼요. 지금 떠 있는 위젯은 끄지 않고요.

설정 파일을 건드리는 거라 안전장치를 꼼꼼히 걸었어요. 바꾸기 전에 뭐가 바뀌는지 보여 주고, 백업을 남기고, 그 값 하나만 바꾸고, 그 값만 되돌릴 수 있게요. 그사이 누가 같은 값을 바꿨으면 되돌리기 전에 알려 주고요. 아이폰 화면에는 이 패널을 넣지 않았어요.

4. 로컬 LLM 사용량도 같이 세기

저는 가벼운 글쓰기 일은 맥미니에 깔린 로컬 LLM(Ollama)에 맡겨요. Claude 토큰을 아끼려고요.

📎 궁금하신 분들은 지난 「로컬 LLM(Ollama gemma4:12b)으로 Claude 토큰 아끼기」 포스팅을 참고하세요 → 글 보러 가기

그런데 위젯 표에는 그 일을 맡은 서브에이전트(그때 이름은 text-drafter, 지금은 정리돼서 없어요)가 "haiku로 일했다"고만 나왔어요. 실제로 글을 쓴 건 로컬 모델인데 말이죠. 서브에이전트가 로컬 모델에 일을 넘기는 중계 역할이라 생긴 일이었어요.

그래서 Claude 작업 기록에서 로컬 LLM을 부른 기록과 결과 끝에 붙는 통계 한 줄(모델 이름, 토큰 수, 초당 속도)만 골라 읽게 했어요. 본문은 저장하지 않아요. 이제 표에 "(로컬 LLM)" 행이 따로 생겨서 어떤 모델이 몇 번, 토큰을 얼마나 썼는지 보여요. 일을 맡겨 놓고 서브에이전트가 로컬 모델을 안 부르고 끝내 버리면 "로컬 LLM 미호출 N"이라고 따로 표시해요.

하나 고민한 게 있었어요. 처음엔 "Claude 한도와는 상관없으니까" 로컬 토큰을 비율·합계에서 뺐는데, 다시 생각해 보니 제가 알고 싶은 건 "어떤 에이전트가 어떤 모델로 토큰을 얼마나 쓰나"였어요. Claude든 GPT든 로컬이든요. 그래서 합계에 넣었어요. 하드웨어 카드에는 Ollama 상태 줄도 붙여서 지금 어떤 모델이 메모리에 올라와 있는지, 몇 분 뒤 내려가는지도 보이게 했어요.

⚠️ 한계: Ollama는 "지금 생성 중"인지 알려 주지 않아요. 그리고 처음엔 스크립트가 Ollama를 직접 부르면 Claude 기록에 안 남아 표에 안 잡혔어요. 나중에 Ollama 자체 로그를 읽게 바꿔서, 짝이 없는 호출은 "직접 호출 (로컬 LLM)"으로 따로 세요.

주의사항

  • 공개 범위: 사용 기록이 담긴 화면은 인터넷에 열지 말고 내 기기끼리만 보게 하세요. 저는 Tailscale tailnet 안에서만 열었어요.
  • 테스트와 실제 환경 분리: 자동 시작을 등록한 뒤엔 테스트가 진짜 항목을 건드리지 않는지 꼭 확인하세요. 저는 한 번 당했어요.
  • 원칙의 예외는 적어 두기: "토큰 0", "읽기 전용" 같은 원칙을 깰 땐 무엇을 왜 깨는지 문서에 적고 직접 승인하세요. 나중에 헷갈리지 않아요.

마치며

아이패드 화면에서만 보던 창이 이제 아이폰에서도 보이고, 한도가 차면 먼저 알려 주고, 맥미니를 켜면 알아서 떠요. 만드는 데 며칠이 걸렸지만 제일 크게 바뀐 건 한도를 확인하러 가지 않아도 된다는 점이에요. 알림이 올 때만 보면 되거든요.

이 위젯은 그 뒤로 AI들이 올린 결정할 일(결재함)과 진행 상황(현황판)까지 한 화면에 모은 "본부" 앱으로 커졌어요. 그 이야기는 회사처럼 굴리게 된 이야기와 같이 따로 써 볼게요.

최종 요약

✅ 결론만 보고 싶은 분

1. 아이폰·아이패드: Tailscale tailnet 안에서만 열고, 홈 화면 앱으로. 스크롤하면 멈추던 버그는 pointercancel 처리로 해결.

2. 자동 시작: launchd에 서버(죽으면 30초 뒤 재시작)와 창 등록, "Agent Status" 앱 껍데기로 이름·아이콘 정리.

3. 알림: 80%·95% 넘을 때와 리셋 때 한 번씩, 맥미니 알림 센터 + 아이폰 웹 푸시. 오래된 값으론 안 보냄.

4. "읽기만" 원칙의 예외 네 개(등록 요청 2개·암호화 라이브러리·애플 푸시 전송·설정 수정)는 적어 두고 직접 승인.

5. 로컬 LLM: 호출 기록과 통계 한 줄만 읽어 "(로컬 LLM)" 행으로, 합계에도 포함.

· · ·

도움이 됐다면 공유 부탁드립니다! AI 한도 알림, 어떤 방식으로 받고 계신지 댓글로 알려 주세요.

#클로드코드 #ClaudeCode #위젯 #Tailscale #맥미니

퇴근 후 맥미니

애 둘 키우는 평범한 직장인 아빠. 퇴근 후 맥미니로 이것저것 만들다 막힌 걸 그대로 적습니다.

방명록에 질문 남기기