# WritingDeveloper — Full Content > Full-text version of every blog post by Sihyeong Lee (이시형). For a compact > index (posts, projects, links), see https://writingdeveloper.blog/llms.txt # Blog Posts (Korean) --- ## JuiceBar 만들기: PC 전력을 전기요금으로 바꾸기 URL: https://writingdeveloper.blog/blog/building-juicebar Published: 2026-08-27 Author: 이시형 와트 숫자는 잠깐 보면 재미있습니다. 전기요금은 실제로 행동을 바꾸게 합니다. 그래서 [JuiceBar](https://github.com/writingdeveloper/JuiceBar)를 만들었습니다. PC가 쓰는 전력을 Windows 트레이에서 보고, 그 값을 현재 청구 주기의 비용으로 바꾸는 앱입니다. 트레이 아이콘은 연료 게이지처럼 차오르고, 열어 보면 이번 주기 비용과 오늘 사용량, 예상 비용, 최근 추세가 나옵니다. 화면보다 어려웠던 것은 그 숫자가 무엇을 의미해야 하는지 정하는 일이었습니다. ## Windows는 PC 전체 벽전력을 주지 않습니다 콘센트에서 실제로 빠져나가는 와트를 한 번에 돌려주는 Windows API는 없습니다. 대신 하드웨어가 일부 부품의 값을 보여줍니다. Windows 11에서는 Energy Meter Interface로 CPU 패키지 에너지를 읽을 수 있습니다. 그 경로를 쓸 수 없는 환경에서는 PawnIO를 통해 Intel RAPL이나 AMD SMU 기반 CPU 센서로 폴백할 수 있습니다. 외장 GPU는 NVIDIA NVML 또는 AMD ADL을 사용하고, 노트북에서는 ACPI 배터리 충방전 값도 활용할 수 있습니다. 하지만 메인보드, RAM, 드라이브, 팬, 그리고 파워서플라이 손실까지 센서가 전부 보여주지는 않습니다. 여기서 그냥 "전체 소비전력"이라고 부르면 구현은 간단해지지만 숫자는 덜 정직해집니다. 그래서 JuiceBar는 측정되는 것과 추정해야 하는 것을 분리합니다. ```text P_wall = (P_measured + B) / η ``` `P_measured`는 실제 센서에서 읽은 값의 합입니다. `B`는 직접 측정할 수 없는 부품의 기준 전력이고, `η`는 파워서플라이 효율입니다. ## 보정값을 숨은 상수로 두지 않았습니다 `B`와 `η`는 PC마다 다릅니다. 고정된 추정값 하나를 박아 두면 편하지만 신뢰하기 어렵습니다. 그래서 데스크톱에서는 콘센트 전력계나 전력 측정 스마트플러그에서 두 번 읽은 값을 넣어 모델을 보정할 수 있게 했습니다. 노트북에서는 배터리로 동작할 때의 방전 전력이 전체 시스템을 보는 기준값 역할을 할 수 있습니다. 아직 보정하지 않았더라도 앱은 보수적인 기본값으로 동작합니다. 대신 화면에 **not calibrated**라고 표시합니다. 추정값을 측정값처럼 보이게 만드는 것보다 어느 부분부터 추정인지 드러내는 쪽을 택했습니다. ## 전력보다 더 복잡했던 것은 요금제였습니다 처음 알고 싶었던 것은 와트가 아니라 돈이었습니다. 전기요금은 고정 단가일 수도 있고, 사용량 구간에 따라 달라질 수도 있고, 시간대별 요금일 수도 있습니다. 기본료와 세금이 붙고 청구 주기 시작일도 다릅니다. 모든 경우를 폼의 수십 개 입력칸으로 만들고 싶지는 않았습니다. 대신 JuiceBar가 고정 JSON 스키마를 요구하는 프롬프트를 만들어 줍니다. 이 프롬프트를 ChatGPT, Gemini, Claude 같은 도구에 붙여 넣고 돌아온 JSON을 앱에 넣을 수 있습니다. 설명 문장이 JSON 주변에 있어도 파싱하되, 잘못된 청구일이나 구간 순서, 이상한 세율 형태는 저장 전에 거부합니다. AI는 요금제를 구조화하는 입력 편의 기능일 뿐이고, 실제 요금 계산은 앱의 결정론적 코드가 맡습니다. ## 사용 이력은 로컬에 남깁니다 JuiceBar가 필요한 사용 이력은 로컬 SQLite에 저장됩니다. 보정값, 센서 선택, 요금제, 언어, 사용 이력은 앱이 설치된 각 PC의 데이터입니다. 별도 JuiceBar 계정도 없고 이 데이터를 보관하는 서버도 없습니다. 앱은 .NET 10 WPF로 만든 Windows 트레이 프로그램이고 GitHub Releases에서 self-contained 실행 파일로 배포합니다. 저장소는 MIT 라이선스로 공개했습니다. 이 프로젝트에서 마음에 드는 부분은 특별한 알고리즘을 발명했다는 데 있지 않습니다. 어디까지가 측정이고 어디부터가 추정인지 정하고, 그 차이를 계산식에서 UI까지 숨기지 않았다는 데 있습니다. --- ## 제 홈서버는 노트북입니다 URL: https://writingdeveloper.blog/blog/building-my-homelab Published: 2026-07-17 Author: 이시형 홈서버라고 하면 랙에 꽂힌 서버 본체와 깜빡이는 LED를 떠올리기 쉽습니다. 제 홈서버는 윈도우 노트북입니다. RTX 4080이 달려 있고, 하이퍼바이저 없이 Docker 컨테이너와 Python venv를 호스트에 그대로 올려 씁니다. 여기에 Oracle Cloud의 우분투 VM 하나를 더해 두 대를 Tailscale 사설망으로 묶었습니다. 구성은 이게 전부입니다. 이 두 대 위에서 이미지·영상·3D·음성·음악 생성 파이프라인이 돌고, 시크릿 매니저와 프로젝트 관리 도구가 돌고, 로컬 LLM이 돕니다. 어떻게 나눴고 왜 그렇게 나눴는지를 한 번 정리해 두고 싶었습니다. ## 무거운 것은 집에, 항상 켜 있어야 하는 것은 클라우드에 분담의 기준은 단순합니다. GPU가 필요한 작업은 전부 노트북이 맡습니다. 생성 파이프라인의 컴퓨팅과 데이터, 프론트까지 이 한 대에 모여 있습니다. 문제는 노트북이 24시간 켜 두는 기계가 아니라는 점입니다. 그래서 항상 살아 있어야 하는 것들은 Oracle Cloud의 우분투 VM으로 보냈습니다. 시크릿 매니저인 **Infisical**과 프로젝트 관리 도구인 **Plane**이 거기서 돕니다. 덕분에 노트북을 꺼도 시크릿과 프로젝트 보드는 살아 있습니다. 서버 한 대에 전부 몰아넣는 구성보다 덜 근사해 보일 수는 있습니다. 다만 각자 잘하는 일만 맡기고 나니, 어느 쪽도 무리하지 않습니다. ## 모든 길은 Tailscale로 노트북과 VM, 그리고 개발용 기기들은 전부 Tailscale 메시 위에 있습니다. 제 기기들끼리 오가는 트래픽은 이 사설망 안에서만 움직입니다. 외부에서 노트북에 접속할 때도 이 사설망을 통합니다. 공개가 필요한 화면은 따로 있습니다. 그런 것들만 Caddy 리버스 프록시와 DuckDNS 동적 DNS로 바깥에 노출합니다. 생성 결과를 확인하는 갤러리처럼 화면 자체는 열어 두되 아무나 보면 곤란한 것들은 basic auth로 한 번 더 잠가 둡니다. 어떤 주소인지는 여기 적지 않겠습니다. 공개 범위를 좁히는 이야기를 하면서 주소를 적는 것도 이상하니까요. ## 시크릿과 프로젝트 관리를 직접 돌린다는 것 Infisical에는 여러 프로젝트의 API 키와 환경변수가 모여 있고, Plane에는 프로젝트 보드가 있습니다. 관리형 SaaS를 쓰면 되는 일을 굳이 직접 돌리는 셈입니다. 공짜는 아닙니다. 요금 대신 운영으로 냅니다. 지켜봐야 할 서비스가 둘 늘었다는 뜻이고, 업데이트도 제 몫이라는 뜻입니다. 그 대신 시크릿이 어디에 있는지, 누가 볼 수 있는지를 제 손으로 정합니다. 지금 규모에서는 이 교환이 남는 장사라고 생각하고 있습니다. ## 지켜보는 방법 서비스 생존은 **Uptime Kuma**가 지켜봅니다. 상태 점검은 [ai-4080-ops](/projects)라는 내부 툴킷의 PowerShell 스크립트들이 맡습니다. GPU와 포트, Funnel(Tailscale의 선택적 공개 노출 기능), Caddy의 상태를 확인하는 스크립트들입니다. 노트북 조작은 Tailscale 너머에서 SSH로 하고, 원격으로 보내는 PowerShell 코드는 따옴표가 깨지지 않도록 EncodedCommand로 감싸는 래퍼를 거칩니다. 애초에 Claude Code 세션에서 이 서버를 바로 조작하려고 만든 도구들입니다. Grafana도 Prometheus도 없습니다. 대시보드가 필요할 만큼 서비스가 많지 않고, 스크립트와 Uptime Kuma로 아직 충분합니다. 백업 체계도 따로 없습니다. 이쪽은 충분해서가 아니라 아직 못 갖춘 것이라, 숙제로 적어 둡니다. ## 이 인프라 위에서 도는 것들 이 구성은 인프라를 먼저 만들고 쓸 곳을 찾은 결과가 아닙니다. 돌릴 것이 먼저 있었고, 인프라가 따라왔습니다. 노트북에서는 [studios](/projects)가 돕니다. 이미지·영상·3D·음성·음악 다섯 모달리티를 Claude가 MCP 툴로 생성하고 검수하는 개인 생성 팩토리로, GPU 중재와 SQLite 자산 스토어를 공유 커널로 두고 FastAPI와 React 갤러리까지 한 모노레포에 담아 300개 이상의 테스트로 고정해 두었습니다. 자연어를 최적화된 프롬프트와 워크플로 JSON으로 바꿔 주는 [ComfyUI Web](/projects)도, 로컬 LLM인 Ollama도 같은 노트북에서 돕니다. 그러니까 이 글의 인프라는 전시용이 아니라 생활용입니다. [요즘 만들고 있는 것들](/blog/recent-builds-2026)에서 소개한 프로젝트 상당수가 이 두 대 위에서 만들어졌습니다. 지금 구성이 최종이라고는 생각하지 않습니다. 오늘 필요한 만큼 돌아가고 있고, 모자라는 순간이 오면 그때 다시 바꿀 생각입니다. --- ## 발모벽을 고치려고 만든 앱, 아직 못 고친 이야기 URL: https://writingdeveloper.blog/blog/building-dont-touch Published: 2026-06-10 Author: 이시형 저는 발모벽(트리코틸로마니아)이 있습니다. 머리카락이나 눈썹을 뽑는 강박 행동인데, 겪어보지 않으면 이해하기 어려운 지점이 하나 있습니다. 손이 머리로 올라가는 순간을 스스로 인지하지 못한다는 것입니다. 뽑고 나서야 "아, 또 뽑았네"가 됩니다. 의지의 문제라기보다 인지의 사각지대 문제에 가깝습니다. 인지하지 못하는 게 문제라면, 인지시켜주는 기계를 옆에 두면 되지 않을까. 그게 [Don't Touch](https://github.com/writingdeveloper/dont-touch-electron)의 출발이었습니다. ## 만든 것 Electron 데스크톱 앱입니다. 웹캠으로 손과 얼굴을 실시간 추적하다가, 손이 설정한 영역(두피, 눈썹, 볼 등)에 닿는 순간 즉시 알림을 줍니다. 감지 영역은 세밀하게 조절할 수 있고, 일일 통계와 연속 기록으로 흐름을 추적합니다. 기술적으로 가장 중요하게 정한 것은 모든 영상 처리를 기기 안에서 끝낸다는 것이었습니다. MediaPipe Vision이 브라우저 수준의 런타임에서 충분히 돌아가기 때문에 가능했습니다. 하루 종일 내 얼굴을 보고 있는 앱입니다. 이 영상이 단 한 프레임이라도 밖으로 나간다면, 제가 사용자라도 안 씁니다. 영상은 저장도 전송도 되지 않고, 감지 결과만 남습니다. ## 효과의 정직한 기록 여기부터는 홍보가 아니라 기록입니다. 노트북으로 일하던 시기에는 어느 정도 효과가 있었습니다. 알림이 울리면 손을 내렸고, 적어도 "내 손이 지금 어디 있는지"를 하루에 수십 번 인지하게 됐습니다. 그런데 데스크톱으로 돌아오면서 상황이 바뀌었습니다. 카메라가 없었습니다. 앱은 그대로인데 쓸 수 없는 환경이 됐고, 그렇게 한동안 방치됐습니다. 도구는 의지보다 환경을 탄다는 걸 만든 사람이 몸으로 증명한 셈입니다. 레딧에 올려본 적이 있습니다. 잠깐 반응이 있다가 금세 조용해졌습니다. 그러던 어느 날, 개인 이메일로 기능 요청이 한 통 왔습니다. 요청은 구체적이었습니다. 알림음을 여러 언어로 더 다양하게 해줬으면 좋겠고, 원하는 mp3나 wav 파일을 직접 추가할 수 있게 해달라는 거였습니다. 러시아어를 쓰시는 분 같아서, 알림음을 AI TTS로 여러 언어 생성해 넣으면서 러시아어도 함께 추가했습니다. 바로 업데이트해서 답장을 보냈고, 그분의 피드백까지 받을 수 있었습니다. 다운로드 수 같은 지표보다 그 메일 한 통이 훨씬 무거웠습니다. 같은 문제를 겪는 사람이 어딘가에 있고, 제가 만든 게 그 사람의 하루에 실제로 끼어 있다는 뜻이니까요. ## 한계도 그대로 적어두기 분명히 해두고 싶은 게 있습니다. 발모벽은 심리적 불안 기제에서 비롯됩니다. 이 앱이 잡아주는 건 행동의 표면이지, 그 밑의 불안이 아닙니다. 꾸준히 쓰면 도움이 될 거라고 생각하지만, 앱만으로 고치기는 어렵다는 게 제 솔직한 생각입니다. 근거는 간단합니다. 만든 사람인 저도 아직 못 고쳤습니다. 증상이 생활에 지장을 줄 정도라면, 앱이 아니라 전문 상담과 치료가 먼저입니다. 그래도 이 앱은 계속 둘 생각입니다. 인지의 사각지대를 비춰주는 거울이 하나쯤 있는 게 없는 것보다는 낫다는 걸, 노트북 시절에 겪어봤으니까요. --- ## 어기면 돈이 자선단체로 가는 금주 앱 URL: https://writingdeveloper.blog/blog/building-sobriety-app Published: 2026-06-10 Author: 이시형 > **업데이트 (2026-07):** 이 글의 Expo 버전은 이후 PWA로 다시 만들어 **[Drymora](https://drymora.writingdeveloper.blog)** 로 출시했고, Google Play에도 올렸습니다. 이 최초 시도는 [프로젝트 묘지](/graveyard)에 기록으로 남겨 두었습니다. 저는 지금 금주를 시도하고 있습니다. 이 앱은 그 시도에서 나왔습니다. 전에 효과를 봤던 방법이 하나 있습니다. 음주측정기를 사서, 친구와 약속을 했습니다. 측정할 때마다 결과를 캡처해서 보낼 것, 안 보내면 벌금을 낼 것. 우스워 보이는 장치인데, 꽤 잘 작동했습니다. 돌이켜보면 작동한 이유는 측정기의 정밀도가 아니었습니다. 잃을 돈이 있었고, 지켜보는 사람이 있었다는 것. 행동경제학에서 commitment device라고 부르는 구조를, 저는 측정기 한 대와 친구 한 명으로 어설프게 조립해 쓰고 있었던 겁니다. 그 어설픈 장치를 제대로 만들어보자는 게 이 앱의 출발이었습니다. ## 구조: 돈을 걸고, 매일 증명한다 구조는 단순합니다. 금액을 스테이크로 걸고, 매일 셀피로 체크인합니다. 지키면 아무 일도 일어나지 않습니다. 실패하면 스테이크가 자선단체로 자동 기부됩니다. 돈을 그냥 잃는 게 아니라 좋은 곳에 빼앗기는 셈입니다. 아깝지만 떳떳한 손실이라는 점이 저에게는 중요했습니다. Expo로 모바일 앱을, Fastify로 백엔드를 만드는 중이고, 313개 테스트와 CI까지는 갖춰 둔 상태입니다. 아직 만들고 있는 단계라는 것도 그대로 적어둡니다. ## 돈을 한 번도 쥐지 않는 설계 이 앱에서 가장 중요한 설계 결정은 플랫폼이 사용자 돈을 절대 보관하지 않는다는 것입니다. Stripe Connect의 비수탁(non-custodial) 구조로, 스테이크는 플랫폼 계좌를 거치지 않고 처리됩니다. 솔직히 말하면 이 방향은 제가 처음부터 그린 그림이 아닙니다. 설계를 상의하던 Claude Code가 가장 강하게 추천한 구조였습니다. 듣고 보니 명확했습니다. 1인 개발자가 남의 돈을 보관하는 순간 따라오는 무게(신뢰, 환불 분쟁, 보안)는 기능 하나의 무게가 아닙니다. 돈을 아예 쥐지 않으면 그 문제들의 대부분이 애초에 생기지 않습니다. AI의 추천이어도 맞는 추천은 맞다고 적어둡니다. ## 가장 약한 고리: 셀피는 속일 수 있다 이 글에서 가장 정직해야 할 부분입니다. 셀피 체크인은 뚫립니다. 다른 사람이 대신 찍을 수도 있고, 마음먹고 속이려는 사람을 기술로 다 막을 방법을 저는 아직 찾지 못했습니다. 생각해 보면 음주측정기 시절에도 '캡처해서 보내기'는 기술적으로 허술했습니다. 그런데도 작동했던 건, 캡처를 받는 쪽이 친구였기 때문일 겁니다. 속이려면 속일 수 있었지만, 친구를 속이는 비용이 벌금보다 컸던 거죠. 앱은 그 친구의 자리를 대체해야 하는데, 그게 기술 문제가 아니라는 걸 만들수록 알게 됩니다. 이 부분은 아직 고민 중이라고만 적어두겠습니다. ## 분명히 해둘 것 하나 분명히 해둘 것이 있습니다. 가벼운 절주 다짐에는 이런 장치가 도움이 될 수 있지만, 알코올 의존이 의심되는 수준이라면 이건 앱의 영역이 아닙니다. 전문 상담과 치료가 먼저입니다. 지금은 제가 사용자 1호입니다. 음주측정기와 친구로 조립했던 그 장치가 앱이 되어도 작동하는지, 제 몸으로 먼저 확인하고 있습니다. --- ## 박사도 아닌 내가 멘탈헬스 앱을 만들어도 될까 URL: https://writingdeveloper.blog/blog/healframe-safety-pipeline Published: 2026-06-10 Author: 이시형 2026년, 도서관에서 주디스 허먼의 『트라우마』(원제 *Trauma and Recovery*)를 빌려 읽었습니다. 원래 정신 상담과 분석 쪽에 관심이 많기도 했고, 이 모델이 낯설지 않게 읽히는 개인적인 이유도 있었습니다. 그 이야기는 여기까지만 하겠습니다. 책에서 가장 오래 남은 건 회복이 안전의 확보, 기억과 애도, 그리고 다시 연결되기라는 세 단계를 거친다는 구조였습니다. 읽다 보니 이 구조가 그대로 글쓰기 도구의 뼈대가 될 수 있겠다는 생각이 들었습니다. 지금 어느 단계에 있는지에 따라 써야 할 글이 다르다면, 단계에 맞는 프롬프트를 건네는 도구를 만들 수 있지 않을까. 그렇게 시작한 것이 [HealFrame](https://healframe.app)입니다. ## 가장 무거운 기능은 가장 안 보이는 기능 AI가 글쓰기를 안내하는 멘탈헬스 앱에서 기술적으로 가장 무거운 부분은 멋진 프롬프트가 아닙니다. 사용자가 쓴 글에서 위기 신호를 읽어내는 일입니다. HealFrame은 Gemini로 입력 글을 GREEN/AMBER/RED 세 단계로 분류하는 위기 감지 파이프라인을 돌립니다. 설계에서 제일 신경 쓴 건 실패의 방향이었습니다. 입력 판정은 안전한 쪽으로 닫아뒀습니다(fail-closed). 분류가 애매하거나 시스템이 흔들리면 일단 위기로 봅니다. 반대로 출력은 열어뒀습니다(fail-open). 안전 장치가 오작동했다고 해서 사용자에게 가야 할 응답까지 막지는 않습니다. 위기를 놓치는 비용과 과잉 감지의 비용은 무게가 다르니까, 시스템이 실패하는 방향도 한쪽으로 기울어야 한다고 봤습니다. 검증도 같은 논리로 짰습니다. LLM-judge 평가 하니스를 만들어 파이프라인을 반복해서 돌리는데, 통과 기준은 하나입니다. 평가 세트 안에서 위기 신호 누락이 하나라도 나오면 실패로 칩니다. 다른 지표는 양보해도 이 기준만은 양보하지 않았습니다. 물론 이건 평가를 통과하기 위한 기준이지, 실제 세상에서 누락이 0이라고 증명된 건 아닙니다. LLM으로 LLM을 평가하는 이상 judge 자체가 틀릴 수 있다는 순환적인 한계도 있습니다. 그래서 평가가 닿지 못하는 빈틈을 마지막에 받치는 게 앞의 fail-closed 설계입니다. 불확실하면 위기로 간주하는 그 원칙이 마지막 층인 셈입니다. ## 그런데, 이게 정말 작동하는 걸까 여기까지는 엔지니어링 이야기고, 솔직한 이야기는 지금부터입니다. 위기 감지 파이프라인은 저만 만드는 게 아닙니다. 수많은 AI 회사들이 비슷한 것을 만들고 있고, 훨씬 많은 인력과 데이터로 만듭니다. 그런데도 현실에서는 여전히 많은 사람들이 자살을 시도하고, 실행합니다. 제 테스트 하니스가 전부 통과한다는 것과, 실제 어떤 사람의 가장 어두운 밤에 이 시스템이 작동한다는 것 사이에는 제가 증명할 수 없는 거리가 있습니다. 그리고 더 근본적인 질문이 있습니다. 저는 박사가 아닙니다. 임상가도 아닙니다. 책을 읽고, 관심이 있고, 경험이 있는 개발자일 뿐입니다. 사람의 마음을 건드리는 앱을 그런 사람이 만들어도 되는 걸까. 이 질문은 개발 내내 사라지지 않았고, 지금도 사라지지 않았습니다. ## 답 대신 지키는 선 저는 이 질문에 아직 답하지 못했습니다. 대신 만들면서 지키는 선을 몇 개 정했습니다. 첫째, 이 앱은 치료가 아니고, 치료라고 말하지 않습니다. 회복 단계에 맞춘 글쓰기를 돕는 도구, 거기까지입니다. 둘째, 가장 위험한 실패(위기 누락)에는 0의 허용치를 두고, 그것을 감과 선의가 아니라 평가 하니스로 강제합니다. 셋째, 모른다는 사실을 잊지 않습니다. 전문가가 아니라는 불안은 없애야 할 감정이 아니라, 이 도메인에서 계속 신중하게 만들게 해주는 안전장치에 가깝다고 생각하게 됐습니다. 만들어도 되는가. 여기엔 아직 확신이 없습니다. 그래도 확신 없이 조심하며 만드는 쪽이 이 영역에선 차라리 나을지도 모르겠다고, 요즘은 그렇게 생각합니다. --- > 혹시 지금 견디기 힘든 시간을 보내고 있다면, 혼자 견디지 않아도 됩니다. **자살예방 상담전화 109**, **정신건강 위기상담 1577-0199**에서 24시간 전문 상담을 받을 수 있습니다. --- ## 공개 API가 없어서 만든 '정직한 추정기' URL: https://writingdeveloper.blog/blog/rentrights-honest-estimator Published: 2026-06-10 Author: 이시형 LA에 처음 왔을 때 룸쉐어를 했습니다. 몇 달 뒤 렌트비가 확 올랐고, 저는 그냥 더 내고 살았습니다. 항의할 생각을 못 한 게 아니라, 그 인상이 합법인지 아닌지를 판단할 방법 자체를 몰랐습니다. 임대료 규제는 주마다, 시마다, 심지어 건물마다 다르고, 그걸 일반인이 알아내는 건 거의 불가능합니다. 몇 년이 지난 지금도 사정은 비슷합니다. 제 임대 계약은 만료까지 3개월 남았고, 미국 렌트비는 여전히 살벌하고, 사업을 하는 입장에서 수입은 거의 없는 상황입니다. 다음 갱신 때 집주인이 얼마를 올릴 수 있는지가 저에게는 추상적인 질문이 아닙니다. 그때 문득 그 시절 생각이 났습니다. 이런 걸 알려주는 도구가 있었다면, 적어도 어딘가에 도움을 요청해볼 생각은 했을지도 모르겠다고. 그래서 만든 것이 [RentRights](https://github.com/writingdeveloper/rentrights)입니다. 주소를 넣으면 거기에 어떤 임대료 규제(LA City RSO, 캘리포니아주 AB1482, LA County 규정 RSTPO/JCO)가 적용될 가능성이 높은지 추정해주는 오픈소스 웹앱입니다. ## 문제: 확정 데이터가 존재하지 않는다 만들기 시작하면서 바로 부딪힌 현실이 있습니다. "이 주소는 RSO 대상입니다"라고 확정해주는 공개 API가 없습니다. RSO 등록부는 있지만 프로그램이 질의할 수 있는 형태가 아닙니다. 선택지는 둘이었습니다. 확정 못 하니까 안 만들거나, 추정할 수 있는 데이터로 추정하거나. 저는 후자를 골랐고, 조건을 하나 달았습니다. 추정이라는 사실을 절대 숨기지 않는 것. Census와 LA County Assessor의 공개 데이터에서 건축 연도 같은 단서를 모아 적용 가능성을 계산하되, 결과는 언제나 "추정"으로 표시합니다. 확신을 팔기 시작하는 순간 이 도구는 도움이 아니라 위험이 됩니다. 그래서 이름도 스스로 '정직한 추정기(honest estimator)'라고 부릅니다. ## 편향의 자백 룰 엔진에는 명시적인 방향성이 하나 있습니다. 건축 연도가 경계선에 걸리거나 데이터가 모호할 때, 엔진은 세입자 보호 쪽으로 기웁니다. "보호 대상일 수 있으니 확인해보세요"가 "해당 없음"보다 낫다는 판단입니다. 거창한 정의감에서 나온 원칙이라고 쓰고 싶지만, 솔직히 말하면 제가 세입자라서 그렇게 된 것 같습니다. 임대인이 만들었다면 다른 방향으로 기울었을지도 모릅니다. 중요한 건 그 편향을 숨기지 않는 것이라고 생각했습니다. 이 기울기는 룰 엔진에 명시적으로 코딩되어 있고, 198개의 테스트로 고정되어 있습니다. 누구든 코드를 열면 이 도구가 어느 쪽으로 기우는지 확인할 수 있습니다. 편향이 없는 도구인 척하는 것보다, 편향을 코드에 적어두고 테스트로 묶어두는 쪽이 더 정직하다고 믿습니다. ## 공개 데이터 통합은 신뢰가 전부 이런 도구는 한 번 틀린 답을 주면 끝이라, 견고함에 시간을 많이 썼습니다. Assessor 쿼리는 프로파일링해 보니 경로에 따라 13~55초까지 걸리는 경우가 있어서, 인덱스 기반 쿼리로 1초 안에 떨어지는 폴백 경로를 만들었습니다. 외부 API 호출은 사용자 입력이 쿼리 문자열에 그대로 보간되지 않도록, 허용된 파라미터만 화이트리스트로 통과시켜 쿼리가 변조될 여지를 줄였습니다. 화려한 부분은 아니지만, "렌트비 올랐는데 이거 맞나요"를 검색하다 들어온 사람에게 60초 로딩과 깨진 응답은 그냥 닫는 버튼과 같습니다. ## 그때의 나에게 이 앱이 법률 자문을 대체할 수는 없습니다. 목표는 더 소박합니다. 그 시절의 저처럼 인상 통지를 받고도 그게 정당한지조차 모른 채 그냥 더 내기로 하는 사람이, 최소한 "물어볼 곳은 있는지" 찾아볼 마음이라도 먹게 하는 것. 추정기가 할 수 있는 일은 거기까지지만, 거기까지는 제대로 하고 싶습니다. --- ## 요즘 만들고 있는 것들 URL: https://writingdeveloper.blog/blog/recent-builds-2026 Published: 2026-06-07 | Updated: 2026-06-10 Author: 이시형 > **업데이트 (2026-07):** 이 글은 2026년 6월 시점의 기록이고, 이후 두 가지가 바뀌었습니다. > > "감정을 적어 3D 화염으로 태워 보내는 익명 앱"이라고 적은 Minddump는 **[Kindling](https://kindling.writingdeveloper.blog)** 으로 다시 만들었고, 원본은 [프로젝트 묘지](/graveyard)에 남겨 두었습니다. > > 아래 **Voice Studio**는 이제 독립 레포가 아닙니다. 모달리티마다 스튜디오를 따로 두다 보니 GPU 한 대를 여러 스튜디오가 두고 다퉜고, 자산 스토어와 갤러리도 제각각 흩어졌습니다. 그래서 이미지·영상·3D·음성·음악 다섯 모달리티를 공유 커널(GPU 중재, 자산 스토어, 썸네일) 위에 올려 [studios](/projects) 모노레포 하나로 합쳤습니다. 아래 문단에서 "흩어져 있던 스크립트들을 GPU 작업 큐 하나로 묶었다"고 적었는데, 같은 정리를 스튜디오 단위로 한 번 더 한 셈입니다. 최근 몇 달은 유독 많이 만든 시기였습니다. 분야도 제각각이라 한 번 정리해두고 싶었습니다. 거창한 출시 소식이라기보다는, 요즘 제 손이 어디에 가 있었는지에 대한 기록에 가깝습니다. 프로젝트마다 깊은 이야기는 딥다이브로 따로 쓰고 있으니, 여기서는 전체 그림만 가볍게 훑겠습니다. ## AI를 도구로 끌어다 쓰기 **ComfyUI Web**은 셀프호스팅 이미지·영상 생성 플랫폼인데, 재미있는 부분은 Claude Code를 '프롬프트 엔지니어'로 쓴다는 점입니다. 한국어나 영어로 적은 요청을 최적화된 프롬프트와 ComfyUI 워크플로 JSON으로 자동 변환하고, 그 위에 작업 큐와 공유 갤러리 같은 멀티유저 기능을 얹었습니다. **Voice Studio**는 영상에서 특정 인물의 목소리를 골라 GPT-SoVITS 모델을 자동으로 파인튜닝하는 로컬 스튜디오입니다. 보컬 분리 → 화자 분리 → 전사 → 학습으로 흩어져 있던 스크립트들을 GPU 작업 큐 하나로 묶어 클릭 한 번에 돌아가게 만들었습니다. ## 내 불편을 직접 푸는 앱 **KL125 Controller**는 TP-Link Kasa 스마트 전구를 LAN으로 직접 제어하는 윈도우 트레이 앱입니다. 모니터의 대표 색을 실시간으로 뽑아 전구에 반영하는 앰비언트 모드(필립스 휴 싱크를 전구 하나로 흉내 낸 셈입니다)를 붙였습니다. 사실 제가 쓰려고 만들었습니다. 마음을 다루는 앱도 둘 만들고 있습니다. 하나는 트라우마 회복 단계를 따라가는 글쓰기 도구이고, 하나는 감정을 적어 3D 화염으로 태워 보내는 익명 앱입니다. 둘 다 제 경험에서 출발했고, 그래서 더 조심스럽게 만들고 있습니다. ## 그냥 만들어보고 싶어서 **Argus Fusion**은 정보기관 감시실을 콘셉트로, 지진·항공기·위성·사이버 취약점 같은 공개 데이터 10여 개를 실시간으로 모아 Three.js 지구본 위에 시각화한 웹앱입니다. **Hoverslam**은 스페이스X의 메카질라 부스터 캐치에서 영감받은 실시간 멀티플레이 게임입니다. 수어사이드 번(착륙 직전 마지막 순간에 역추진으로 감속하는 기동) 물리는 Claude Code에 관련 논문과 공개 SpaceX 자료를 읽혀 구현하고, 그 데이터로 보정했습니다. 둘 다 "이거 만들면 재밌겠다"에서 시작했습니다. ## 왜 이렇게까지 만드냐면 이렇게 늘어놓고 보니 공통점이 보입니다. 대부분 제가 궁금하거나 불편한 것에서 출발했고, AI 도구 덕분에 혼자서도 도메인을 넘나들 수 있게 됐다는 점입니다. 출시와 완성에 대한 압박은 조금 내려놓고, 만드는 과정 자체를 기록으로 남겨보려 합니다. 전체 목록과 데모·코드 링크는 [프로젝트 페이지](/projects)에 정리해 두었습니다. 약속했던 딥다이브도 하나씩 쓰고 있습니다. [박사도 아닌 내가 멘탈헬스 앱을 만들어도 될까(HealFrame)](/blog/healframe-safety-pipeline), [발모벽을 고치려고 만든 앱(Don't Touch)](/blog/building-dont-touch), [공개 API가 없어서 만든 '정직한 추정기'(RentRights)](/blog/rentrights-honest-estimator), [어기면 돈이 자선단체로 가는 금주 앱(Sobriety App)](/blog/building-sobriety-app). --- ## Keystatic CMS 도입기: 파일 기반을 고집한 이유 URL: https://writingdeveloper.blog/blog/introducing-keystatic-cms Published: 2026-02-23 | Updated: 2026-06-10 Author: 이시형 이 블로그의 글은 전부 git 저장소 안의 MDX 파일입니다. 새 글을 쓰려면 폴더를 만들고, frontmatter를 손으로 타이핑하고, 형식을 하나라도 틀리면 빌드가 깨졌습니다. 글쓰기보다 파일 관리가 먼저 오는 구조였고, 그게 글을 안 쓰게 되는 핑계가 되고 있었습니다. 그래서 CMS를 붙이기로 했습니다. 그런데 진짜 문제는 "어떤 CMS냐"였습니다. ## 후보들, 그리고 탈락 이유 **Notion이나 headless CMS(Contentful, Sanity류)**: 글의 원본이 외부 서비스의 DB로 들어갑니다. 이 블로그는 제 포트폴리오이기도 해서, 콘텐츠의 단일 원본이 제 저장소 밖에 있는 게 싫었습니다. 서비스가 문을 닫거나 가격 정책이 바뀌면 글이 인질이 됩니다. **DB 기반 자체 어드민**: 만들 수야 있지만, 개인 블로그 하나 때문에 DB와 어드민을 운영하는 건 배보다 배꼽입니다. **파일 기반 CMS(Keystatic, TinaCMS, Decap)**: 콘텐츠는 그대로 git에 남고, 그 위에 편집 UI만 얹습니다. 기존 MDX 읽기 로직을 한 줄도 바꿀 필요가 없습니다. 결국 이 부류에서 골랐고, 그중 Keystatic이 TypeScript로 스키마를 정의하고 Next.js App Router에 라우트 하나로 올라간다는 점에서 가장 마찰이 적어 보였습니다. ## 도입 과정 도입 자체는 반나절 작업이었습니다: 1. `@keystatic/core`와 `@keystatic/next` 설치 2. `keystatic.config.ts`에 기존 frontmatter 스키마(title, excerpt, publishedAt, category, tags, coverImage, faqs)를 그대로 옮긴 collection 정의 3. App Router에 `/keystatic` Admin UI 라우트 추가 4. i18n 미들웨어 matcher에서 `/keystatic` 경로 제외. 이걸 빼먹으면 어드민이 로케일 리다이렉트에 휘말립니다 스토리지는 기본 local 모드로 두었습니다. `npm run dev` 하나 띄우면 `/keystatic`에서 편집한 내용이 곧바로 로컬 파일에 쓰이고, 커밋은 평소처럼 제가 합니다. 환경 변수를 설정하면 GitHub 모드로 전환되도록 해뒀지만, 혼자 쓰는 블로그에는 local이 충분합니다. ## 이 블로그 특유의 문제: ko/en 쌍 이 블로그의 글은 한국어와 영어 두 파일이 한 쌍입니다(`content/posts/ko/<슬러그>`, `content/posts/en/<슬러그>`). 그런데 Keystatic에는 "이 두 글이 같은 글의 번역"이라는 개념이 없습니다. 결국 `posts-ko`, `posts-en` 두 개의 컬렉션으로 나눠 정의했고, 두 글의 슬러그를 맞추고 내용을 동기화하는 일은 여전히 사람 몫입니다. CMS가 해결해 준 것은 "형식 실수"이지 "운영 규칙"이 아니라는 걸 도입하고 나서야 분명히 알게 됐습니다. ## 달라진 것, 달라지지 않은 것 **달라진 것**: frontmatter 오타로 빌드가 깨질 일이 없어졌습니다. 스키마가 TypeScript라 카테고리 같은 필드는 선택지에서 고르면 끝입니다. 브라우저에서 바로 쓰고 고칠 수 있습니다. **달라지지 않은 것**: ko/en 쌍 맞추기, 번역, 이미지 정리. 글쓰기에서 정말 오래 걸리는 부분은 CMS가 건드리지 못합니다. 솔직히 도입한 뒤에도 "WordPress처럼 검증된 물건을 놔두고 이게 맞나" 하는 의심이 한동안 남아 있었습니다. 그럴 때마다 처음의 기준으로 돌아갑니다. 글이 전부 제 저장소 안의 평문으로 남는다는 것. 그 기준 위에서는 아직 이 선택이 맞습니다. 사실 CMS가 글을 대신 써주지는 않습니다. 글을 안 쓰는 핑계 하나를 줄여줬을 뿐인데, 저한테는 그 정도로도 도입한 값은 했습니다. --- ## 베풂에 대한 생각 URL: https://writingdeveloper.blog/blog/thoughts-about-giving-back Published: 2023-06-25 | Updated: 2026-06-10 Author: 이시형 미국에 온 지 석 달 반이 됐습니다. 돌아보면 이 기간은 받기만 한 시간이었습니다. 누군가의 차를 얻어 타고 여행을 다녔고, 누군가의 조언 덕에 보험료를 아꼈고, 가끔은 얻어먹는 외식이 그 주의 가장 좋은 한 끼였습니다. 혼자 왔다고 생각했는데, 혼자 버틴 날은 사실 거의 없었습니다. 직장에서도 그랬습니다. 동료들이 제가 적응하는 걸 정말 많이 도와줬습니다. 물론 사람 사이의 일이라 부딪히는 순간도 있었습니다. 처음엔 그게 오래 마음에 남았는데, 어느 순간 받아들이게 됐습니다. 완벽한 사람은 없고, 저를 도와준 사람들도 저와 부딪힌 사람들도 대개 같은 사람들이었습니다. 그렇다면 어느 쪽을 기억할지는 제가 고르는 수밖에 없습니다. ## 『권력의 법칙』이 만든 긴장 요즘 『권력의 법칙』(The 48 Laws of Power)을 읽고 있고, 거의 다 읽었습니다. 이 책은 모든 호의를 거래로 봅니다. 호의에는 의도가 있고, 빚은 언젠가 청구되며, 그 역학을 모르는 사람은 당한다고 말합니다. 솔직히 유용한 책입니다. 세상에 그런 사람들이 있다는 걸 부정할 수는 없으니까요. 그런데 읽는 내내 마음 한쪽이 거북했습니다. 책의 렌즈로 지난 석 달 반을 다시 보면, 저를 도와준 사람들의 호의에도 계산이 있었어야 합니다. 아무리 되짚어 봐도 그렇지 않았습니다. 차를 태워준 동료도, 보험을 알려준 사람도, 저에게서 받아낼 것이 있어서 그런 게 아니었습니다. 책이 틀렸다기보다는, 책이 설명하지 못하는 사람들이 제 주변에 실제로 있었습니다. ## 그래서, 갚는다는 것 "세상에 공짜 점심은 없다"는 말은 아마 맞을 겁니다. 그런데 저에게 점심을 사준 사람들은 한 번도 계산서를 내민 적이 없습니다. 그 차이를 어떻게 이해해야 할지 한참 생각했습니다. 지금의 제 결론은 이렇습니다. 받은 것을 그 사람에게 되갚는 건 거래입니다. 받은 것을 다음 사람에게 건네는 건 베풂입니다. 저를 도와준 사람들도 아마 언젠가 누군가에게 받았던 것을 저에게 건넸을 겁니다. 지금의 저는 줄 수 있는 게 별로 없습니다. 여전히 받는 시기라는 걸 인정합니다. 다만 이 빚의 목록을 잊지 않으려고 이렇게 적어둡니다. 언젠가 제가 가진 것 — 아마도 기술일 텐데 — 으로 그때의 저처럼 막막한 누군가에게 같은 것을 건넬 수 있으면 좋겠습니다. 그게 어떤 모습일지는 아직 모르겠습니다. 모른다는 것도 그대로 적어둡니다. --- ## ChatGPT로 영어 공부하기, 그리고 영어 면접에서 배운 것 URL: https://writingdeveloper.blog/blog/studying-english-with-chatgpt Published: 2023-04-23 | Updated: 2026-06-10 Author: 이시형 한국을 떠나온 지 1개월 3주가 됐습니다. 그동안 많은 일이 있었지만, 결국 가장 크게 와닿는 건 역시 영어입니다. 미국 사람들과 대화는 됩니다. 상대가 하는 말도 대체로 알아듣습니다. 그런데 정확한 뉘앙스를 자연스럽게 잡아내는 건 여전히 정말 어렵습니다. 노스캐롤라이나는 억양 때문에 영어가 잘 안 들린다는 말을 듣고 왔는데, 막상 와 보니 저는 별 차이를 못 느끼겠습니다. ## AI 도구로 영어 배우기 요즘은 ChatGPT와 DeepL로 영어를 공부합니다. 이 도구들을 쓰다 보면 정말 세상이 바뀌고 있구나 싶습니다. 제 방법은 이렇습니다. 먼저 일기를 한국어로 쓰고, 그걸 다시 영어로 씁니다. DeepL 번역기로 맞는지 확인하고, 마지막으로 DeepL Write로 다른 단어나 표현은 없는지 살펴봅니다. 어릴 때 학원에서 영어를 오래 배웠는데, 시간도 돈도 많이 들었습니다. 지금은 AI 도구만 있으면 공짜로, 그것도 꽤 효율적으로 언어를 배울 수 있게 됐습니다. ## 면접에서 깨달은 것 얼마 전 한 회사와 면접을 봤는데, 영어를 더 해야겠다는 걸 절감했습니다. 영어만의 문제도 아니었습니다. 어떻게 말할지, 무엇을 말하고 무엇을 말하지 않을지 고르는 법도 배워야 했습니다. 면접이 끝나고 제가 했던 말들을 되짚어 보니, 굳이 안 해도 됐을 말들이 떠올랐습니다. 면접 경험이 부족한 탓도 있고, 미국의 대화 문화를 아직 잘 모르는 탓도 있었을 겁니다. ## 연습할 방법 찾기 결국 영어를 쓰는 사람들과 자꾸 말을 해봐야 하는데, 지금 환경에서는 그게 쉽지 않습니다. 회사에 스페인어를 쓰는 동료들이 많기 때문입니다. 그래서 혼잣말로라도 영어를 하기로 했는데, 의외로 이 방법도 나쁘지 않습니다. 아니면 마이크로 대화할 수 있는 크롬 확장 프로그램을 쓰는 방법도 있습니다. 저는 "Talk-to-ChatGPT"라는 확장 프로그램을 쓰고 있습니다. ChatGPT에 대고 말을 하면 정말 누군가와 대화하는 기분이 듭니다. AI로 언어를 배우는 이 방식은 저에게는 이미 큰 전환점이 됐고, 앞으로 더 좋아질 일만 남았다고 생각합니다. --- # Blog Posts (English) --- ## Building JuiceBar: Turning PC Power Into an Electricity Bill URL: https://writingdeveloper.blog/en/blog/building-juicebar Published: 2026-08-27 Author: 이시형 A watt number is interesting for a few minutes. A bill is something I actually make decisions around. That was the reason I built [JuiceBar](https://github.com/writingdeveloper/JuiceBar), a Windows tray app that watches PC power use and turns it into the running cost of the current billing cycle. The tray icon behaves like a fuel gauge. Open it and the app shows the cycle cost, today's usage, a projection, and the recent trend. The UI was the easy part. The harder question was what the number should mean. ## Windows does not give you whole-PC wall power There is no Windows API that simply returns the watts coming out of the wall socket. Hardware exposes pieces of the picture instead. On Windows 11, JuiceBar can read CPU package energy through the Windows Energy Meter Interface. Where that path is unavailable, CPU sensing can fall back to Intel RAPL or AMD SMU through PawnIO. Discrete GPUs use NVIDIA NVML or AMD ADL. On a laptop, ACPI battery charge and discharge give another useful measurement path. But none of those sensors tells me the draw of the motherboard, RAM, drives, fans, or power-supply losses. Pretending otherwise would make the app look simpler while making the number less honest. So JuiceBar keeps the two things separate: what it can measure, and what it has to model. The wall-power model is: ```text P_wall = (P_measured + B) / η ``` `P_measured` is the sum of the readings the machine can expose. `B` is the baseline draw of the parts that are not directly measurable, and `η` is power-supply efficiency. ## Calibration is part of the product, not a hidden constant Those last two values are machine-specific. A fixed guess would be convenient but hard to trust. For a desktop, JuiceBar therefore accepts two external readings — for example from a plug-in wattmeter or an energy-monitoring smart plug — and uses them to calibrate the model. On laptops, battery discharge can provide a whole-system reference while the machine is unplugged. If calibration has not happened, the app still works with conservative defaults, but it says **not calibrated** in the interface. I would rather expose uncertainty than turn an estimate into a fake measurement. ## The other half is the tariff Power measurement alone still does not answer the question I started with. Electricity tariffs can be flat, tiered, or time-of-use. They can have fixed monthly charges and taxes. Billing cycles do not necessarily start on the first day of the month. I did not want to build a setup form with a field for every possible tariff shape. Instead, JuiceBar writes a prompt with a fixed JSON schema. You can paste that prompt into an assistant, then paste the returned JSON into the app. The parser ignores explanation around the JSON but validates the data before accepting it: invalid billing days, malformed tiers, and obviously wrong tax-rate shapes are rejected instead of quietly becoming a bad bill. The assistant is only a convenience for filling the schema. The actual tariff calculation remains deterministic code in the app. ## Local history, no account JuiceBar keeps the usage history it needs in local SQLite. Calibration, channel selection, tariff, language, and history belong to the machine running the app. There is no JuiceBar account and no server holding that data. The application itself is a .NET 10 WPF tray app and is published as a self-contained Windows executable through GitHub Releases. The repository is MIT licensed. What I like most about the project is that the interesting engineering work is not a novel algorithm. It is deciding where a number stops being measured, making the estimate explicit, and carrying that distinction all the way to the UI. --- ## My Home Server Is a Laptop URL: https://writingdeveloper.blog/en/blog/building-my-homelab Published: 2026-07-17 Author: 이시형 Say "homelab" and people picture a rack of servers with blinking LEDs. My home server is a Windows laptop. It has an RTX 4080 in it, and there's no hypervisor — Docker containers and Python venvs run straight on the host. Add one Ubuntu VM on Oracle Cloud, tie the two together over a Tailscale mesh, and that's the whole setup. On top of these two machines run my image, video, 3D, voice, and music generation pipelines, a secrets manager, a project management tool, and a local LLM. I wanted to write down how the pieces are split, and why. ## Heavy things at home, always-on things in the cloud The dividing rule is simple. Anything that needs the GPU goes to the laptop — the generation pipelines' compute, data, and frontend all live on this one machine. The problem is that a laptop is not a machine you leave running 24/7. So the things that must always be alive moved to the Ubuntu VM on Oracle Cloud: **Infisical**, the secrets manager, and **Plane**, the project management tool, run there. I can shut the laptop down and my secrets and project boards stay up. It looks less impressive than one big server that does everything. But with each machine doing only what it's good at, neither one is straining. ## Everything travels over Tailscale The laptop, the VM, and my dev machines all sit on a Tailscale mesh. Traffic between my machines moves only inside this private network. When I reach the laptop from outside, it's through this mesh too. A few screens do need to be public. Only those go out through a Caddy reverse proxy and DuckDNS dynamic DNS. Things like the gallery where I review generation results — pages I want reachable but not open to everyone — get one more lock with basic auth. I won't write the addresses here. It would be a strange way to end a story about keeping the public surface small. ## What self-hosting the secrets and the project board actually means Infisical holds the API keys and environment variables for my projects; Plane holds the project boards. These are things a managed SaaS would happily do for me, and I run them myself anyway. It isn't free. I pay in operations instead of fees — two more services to watch, updates that are now my job. In exchange, I decide where the secrets live and who can see them. At my current scale, I still think that trade comes out ahead. ## How I keep watch **Uptime Kuma** watches whether services are alive. Health checks are handled by the PowerShell scripts in [ai-4080-ops](/en/projects), an internal toolkit — they check the GPU, ports, Funnel (Tailscale's selective public-exposure feature), and Caddy. The laptop itself is driven over SSH across Tailscale, and PowerShell headed for the machine goes through a wrapper that ships it as an EncodedCommand so quoting doesn't break in transit. These tools were built so a Claude Code session could operate the server directly in the first place. There's no Grafana, no Prometheus. There aren't enough services to justify a dashboard, and scripts plus Uptime Kuma are still enough. There's also no backup system yet — and that one isn't "enough", it's simply not built. I'm writing it down as homework. ## What actually runs on this This setup didn't start as infrastructure looking for a use. The things to run came first, and the infrastructure followed. The laptop runs [studios](/en/projects) — a five-modality personal generation factory where Claude drives image, video, 3D, voice, and music through MCP tools, generating and inspecting the results. It has a shared kernel for GPU arbitration and a SQLite asset store, a FastAPI + React gallery in the same monorepo, and 300+ tests holding it all in place. [ComfyUI Web](/en/projects), which turns natural language into optimized prompts and workflow JSON, runs on the same laptop, and so does Ollama, the local LLM. So the infrastructure in this post is not for show — it's lived-in. A good share of the projects from [What I've Been Building Lately](/en/blog/recent-builds-2026) were built on these two machines. I don't think this setup is final. It runs as much as I need today, and when it stops being enough, I'll change it then. --- ## I Built an App to Fix My Hair-Pulling Habit (Not Fixed Yet) URL: https://writingdeveloper.blog/en/blog/building-dont-touch Published: 2026-06-10 Author: 이시형 I have trichotillomania — a compulsive hair-pulling condition. There's one thing about it that's hard to understand unless you've lived it: **you don't notice your hand going up.** You only realize after it's done — "ah, I did it again." It's less a willpower problem than a blind spot in awareness. If the problem is not noticing, then maybe a machine that notices for you would help. That was the start of [Don't Touch](https://github.com/writingdeveloper/dont-touch-electron). ## What I built It's an Electron desktop app. The webcam tracks your hand and face in real time, and the moment your hand enters a zone you've configured — scalp, eyebrows, cheeks — it alerts you instantly. Detection zones are finely adjustable, and daily stats and streaks let you see the trend. The most important technical decision was that **all video processing stays on the device.** MediaPipe Vision runs well enough in a browser-grade runtime to make that possible. This is an app that watches your face all day. If even a single frame of that left the machine, I wouldn't use it myself. Nothing is recorded or transmitted — only the detection events remain. ## An honest record of whether it worked From here on, this is a record, not a pitch. While I worked on a laptop, it genuinely helped. The alert would fire, my hand would come down, and at minimum I became aware of where my hands were dozens of times a day. Then I moved back to a desktop — which had no camera. Same app, but an environment where it couldn't run, and so it sat abandoned for a while. The person who built the tool proved, firsthand, that tools depend on environment more than willpower. I posted it on Reddit once. A brief flicker of attention, then quiet. Then one day, a feature request arrived in my personal email. The request was specific: more varied alert sounds, in multiple languages, and a way to add your own mp3 or wav files. They seemed to be Russian, so when I generated the new voice alerts with AI TTS, I made sure Russian was one of the languages. I shipped the update right away, replied, and even got their feedback afterward. That one email weighed more than any download count — it meant someone with the same problem exists, and something I made is actually part of their day. ## Writing down the limits too I want to be clear about something. Trichotillomania comes from an underlying anxiety mechanism. What this app catches is the surface of the behavior — not the anxiety underneath it. I think consistent use helps, but my honest view is that an app alone won't fix it. The evidence is simple: I built it, and mine isn't fixed yet. And if the condition is disrupting your life, professional counseling and treatment — not an app — is the right first step. I'm keeping the app around anyway. A mirror pointed at your blind spot is clearly better than nothing — laptop-era me can vouch for that. --- ## I'm Building a Sobriety App That Donates My Money If I Fail URL: https://writingdeveloper.blog/en/blog/building-sobriety-app Published: 2026-06-10 Author: 이시형 > **Update (2026-07):** The Expo version described here was later rebuilt as a PWA and shipped as **[Drymora](https://drymora.writingdeveloper.blog)**, now also on Google Play. This first attempt is memorialized in the [project graveyard](/en/graveyard). I'm attempting sobriety myself right now. This app came out of that attempt. There's one method that actually worked for me before. I bought a breathalyzer and made a deal with a friend: send a capture of every reading, and pay a fine if I didn't. It looks like a silly contraption, but it worked surprisingly well. Looking back, the reason it worked wasn't the precision of the device. **There was money to lose, and someone watching.** The thing behavioral economics calls a commitment device — I had duct-taped one together out of a breathalyzer and a friend. Building that clumsy contraption properly is where this app started. ## The structure: stake money, prove it daily The structure is simple. You put up an amount as a stake and check in every day with a selfie. Keep the streak, and nothing happens. Fail, and the stake is automatically donated to charity. You don't just lose the money — it gets taken by a good cause. A loss that stings but that you can stand behind: that distinction mattered to me. I'm building the mobile app with Expo and the backend with Fastify, with 313 tests and CI already in place. It's still in progress — I'm writing that down as plainly as everything else. ## A design that never touches the money The most important design decision in this app: **the platform never holds the user's money.** It's a non-custodial Stripe Connect flow, so the stake is processed without ever passing through the platform's account. Honestly, this wasn't the picture I drew first. It was the structure Claude Code — which I was consulting on the design — recommended most strongly. Once I heard it, it was obvious. The moment a solo developer holds other people's money, the weight that follows — trust, refund disputes, security — isn't the weight of one feature. If you never hold the money, most of those problems never exist. Even when it comes from an AI, a good call is a good call, and I'm noting that. ## The weakest link: a selfie can be faked This is the part of the post that has to be the most honest. The selfie check-in can be beaten. Someone else can take the photo for you, and I haven't found a technical way to stop someone determined to cheat. Come to think of it, the breathalyzer-era "send a capture" was technically flimsy too. It still worked — probably because the person receiving the capture was a friend. I could have cheated, but the cost of lying to a friend was higher than the fine. The app has to fill that friend's seat, and the more I build, the clearer it gets that this isn't a technology problem. For now, all I can write is that I'm still wrestling with it. ## One line I'm drawing To be clear about one thing: a device like this can help with a casual commitment to drink less, but anything that looks like alcohol dependence is not an app's territory. Professional counseling and treatment come first. For now, I'm user number one. Whether the contraption I once assembled out of a breathalyzer and a friend still works as an app — I'm finding out on myself first. --- ## Should Someone Like Me Be Building a Mental Health App? URL: https://writingdeveloper.blog/en/blog/healframe-safety-pipeline Published: 2026-06-10 Author: 이시형 In 2026 I borrowed Judith Herman's *Trauma and Recovery* from the library. I've long been interested in counseling and psychoanalysis, and there were personal reasons the model didn't read as foreign to me. I'll leave that part there. What stayed with me was the structure: recovery moves through three stages — establishing safety, remembrance and mourning, reconnection. Reading it, I kept thinking the structure could be the skeleton of a writing tool. If what you should be writing differs by which stage you're in, then a tool could hand you prompts that match your stage. That's how [HealFrame](https://healframe.app) started. ## The heaviest feature is the least visible one In a mental health app where AI guides your writing, the technically heaviest part isn't clever prompts. It's reading crisis signals in what the user writes. HealFrame runs a Gemini-based crisis-detection pipeline that classifies each entry as GREEN, AMBER, or RED. The most important design decision was asymmetry. **Input classification fails closed** — if the classification is uncertain or the system is degraded, it errs toward the safe interpretation and treats the entry as a crisis. **Output fails open** — a misbehaving safety check is not allowed to block the response a user needs from reaching them. The cost of missing a crisis and the cost of over-flagging are not symmetric, so the system's failure directions shouldn't be either. Validation follows the same logic. I built an LLM-judge eval harness that repeatedly tests the pipeline against one non-negotiable bar: **zero missed crisis signals on the eval set.** Every other metric is up for discussion; that tolerance isn't. I want to be precise about what that bar is, though — it's a passing criterion for my eval set, not proof of zero misses in the wild, and an LLM judging an LLM carries an obvious circularity: the judge itself can be wrong. That's exactly why the asymmetry above matters — for whatever the evals don't reach, the fail-closed input layer that treats uncertainty as crisis is the last layer underneath. ## But does it actually work? That was the engineering part. Here's the honest part. I'm not the only one building crisis detection. Plenty of AI companies are building it, with far more people and far more data. And still, out in the world, people keep attempting and completing suicide. Between "my eval harness passes" and "this system works on the darkest night of a real person's life" there is a distance I cannot prove across. And underneath that sits a more basic question. I'm not a PhD. I'm not a clinician. I'm a developer who read a book, cares about this, and has some lived experience. Is someone like that allowed to build an app that touches people's minds? That question never went away while I was building. It still hasn't. ## Lines instead of answers I haven't answered the question. What I have instead is a set of lines I hold while building. First, this app is not therapy, and it never claims to be — it's a tool that supports stage-appropriate writing, and that's where it stops. Second, the most dangerous failure (a missed crisis) gets a zero-tolerance bar, enforced by an eval harness rather than by good intentions. Third, I try not to forget that I don't know. I've come to see the anxiety of not being an expert less as something to get rid of and more as a safety mechanism — the thing that keeps me building carefully in a domain that demands it. I still have no certainty that I should be building this. But lately I've come to think that, in a domain like this, building carefully without certainty may be safer than building with it. --- > If you're going through a hard time right now, you don't have to carry it alone. In the US, call or text **988** (Suicide & Crisis Lifeline). Elsewhere, [findahelpline.com](https://findahelpline.com) lists free, confidential services by country. --- ## There Was No Public API, So I Built an Honest Estimator URL: https://writingdeveloper.blog/en/blog/rentrights-honest-estimator Published: 2026-06-10 Author: 이시형 When I first came to LA, I lived in a room-share. A few months in, the rent jumped — and I just paid it. It's not that I didn't think of pushing back; **I had no way of even knowing whether the increase was legal.** Rent regulation differs by state, by city, sometimes by building, and figuring out which rules cover you is close to impossible for an ordinary person. Years later, not much has changed. My lease has three months left, US rent is as brutal as ever, and as someone running my own business, my income is close to nothing right now. What my landlord can raise at the next renewal is not an abstract question for me. And that's when the old memory came back: if a tool like this had existed back then, I might at least have worked up the nerve to ask someone for help. So I built [RentRights](https://github.com/writingdeveloper/rentrights) — an open-source web app that takes an address and estimates which rent-control regime most likely applies: LA City RSO, California's AB1482, or LA County rules (RSTPO/JCO). ## The problem: definitive data doesn't exist The first wall I hit was this: there is no public API that can tell you, definitively, "this address is covered by RSO." A registry exists, but not in a form a program can query. That left two options: don't build it because you can't be certain, or estimate from the data you can get. I chose the second, with one condition attached: **never hide the fact that it's an estimate.** The app gathers signals like construction year from Census and LA County Assessor open data and computes a likelihood — and the result is always labeled as an estimate. The moment a tool like this starts selling certainty, it stops being help and becomes a hazard. That's why it calls itself an *honest estimator*. ## Confessing the bias The rules engine has one explicit lean: when a construction year lands on a boundary or the data is ambiguous, the engine **tilts toward tenant protection.** "You may be covered — worth checking" beats "not applicable." I'd love to write that this came from some grand principle of justice, but honestly, I think it leaned that way because I'm a tenant. If a landlord had built it, it might lean the other way. What mattered to me was not hiding the lean: the tilt is coded explicitly into the rules engine and pinned down by 198 tests. Anyone can open the code and see which way this tool leans. I believe writing your bias into the code and locking it with tests is more honest than pretending you don't have one. ## With public data, trust is everything A tool like this is finished the first time it gives someone a wrong answer, so I spent real time on robustness. Profiling showed some Assessor queries taking 13–55 seconds on certain paths, so I built an indexed-query fallback that returns in about a second. External API calls never interpolate user input straight into the query string — only whitelisted parameters get through, which shrinks the surface for query tampering. None of this is glamorous — but for someone who landed here after searching "my rent went up, is this allowed," a 60-second load or a broken response is just a reason to close the tab. ## For the me back then This app can't replace legal advice, and the goal is humbler than that. It's for the person I was — holding a rent-increase notice, not even knowing whether it was legitimate, deciding to just pay — to walk away with **at least the nerve to look up whether there's somewhere to ask.** That's as far as an estimator can take you. I want it to do that part properly. --- ## What I've Been Building Lately URL: https://writingdeveloper.blog/en/blog/recent-builds-2026 Published: 2026-06-07 | Updated: 2026-06-10 Author: 이시형 > **Update (2026-07):** This post is a snapshot from June 2026, and two things have changed since. > > Minddump — described here as the anonymous app that burns a written feeling in a 3D fire — was later rebuilt as **[Kindling](https://kindling.writingdeveloper.blog)**, and the original is memorialized in the [project graveyard](/en/graveyard). > > **Voice Studio** below is no longer its own repo. Keeping a separate studio per modality meant several of them contending for a single GPU, with the asset store and gallery scattered across repos. So I put image, video, 3D, voice, and music on a shared kernel — GPU arbitration, one asset store, thumbnails — and merged them into a single [studios](/en/projects) monorepo. It's the same cleanup the Voice Studio paragraph below describes for its own scripts ("a scattered set of scripts... into a single GPU job queue"), done one level up. The past few months have been an unusually productive stretch of building, across domains that don't have much in common — so I wanted to gather it all in one place. This is less a launch announcement than a record of where my hands have been. The deeper stories live in separate per-project deep dives — this post just covers the whole picture at a glance. ## AI as a tool I reach for **ComfyUI Web** is a self-hosted image and video generation platform, and the fun part is that it uses Claude Code as a "prompt engineer." It turns a request written in Korean or English into an optimized prompt and ComfyUI workflow JSON, with a job queue and a social gallery layered on top. **Voice Studio** is a local studio that picks a speaker out of any video and automatically fine-tunes a GPT-SoVITS model. I collapsed a scattered set of scripts — vocal separation, diarization, transcription, training — into a single GPU job queue that runs end to end with one click. ## Apps that scratch my own itch **KL125 Controller** is a Windows tray app that drives a TP-Link Kasa smart bulb directly over the LAN. I added an ambient mode that samples my monitor's dominant color in real time and mirrors it to the bulb — Philips Hue Sync, faked with a single bulb. I built it because I wanted to use it. I'm also building two apps about the mind: one is a journaling tool that follows the stages of trauma recovery, the other an anonymous app where you write out a feeling and burn it away with a 3D fire animation. Both started from my own experience, so I'm building them more carefully than the rest. ## Just because it sounded fun **Argus Fusion** is a web app styled as an intelligence watch floor — it pulls 10+ live public feeds (earthquakes, aircraft, satellites, cyber vulnerabilities) and visualizes them on a Three.js globe. **Hoverslam** is a real-time multiplayer game inspired by SpaceX's Mechazilla booster catch. The suicide-burn physics — the last-second retro-propulsive landing burn — came from having Claude Code digest the relevant papers and public SpaceX data, then calibrating against that data. Both began with "that would be fun to build." ## Why I build this much Laid out like this, a pattern shows up: almost everything started from something I was curious about or annoyed by, and AI tools are what let one person move across domains. I'm letting go of some of the pressure to launch and finish, and trying instead to keep a record of the building itself. The full list, with demo and code links, lives on the [projects page](/en/projects). The deep dives I promised are landing one by one — [Should Someone Like Me Be Building a Mental Health App? (HealFrame)](/en/blog/healframe-safety-pipeline), [I Built an App to Fix My Hair-Pulling Habit (Don't Touch)](/en/blog/building-dont-touch), [There Was No Public API, So I Built an Honest Estimator (RentRights)](/en/blog/rentrights-honest-estimator), and [I'm Building a Sobriety App That Donates My Money If I Fail (Sobriety App)](/en/blog/building-sobriety-app). --- ## Adopting Keystatic CMS — Why I Insisted on File-Based URL: https://writingdeveloper.blog/en/blog/introducing-keystatic-cms Published: 2026-02-23 | Updated: 2026-06-10 Author: 이시형 Every post on this blog is an MDX file inside a git repository. Writing a new one meant creating a folder, hand-typing frontmatter, and breaking the build if I got a single field wrong. File management came before writing — and it was becoming my excuse not to write. So I decided to add a CMS. The real question turned out to be *which kind*. ## The candidates, and why they lost **Notion or a headless CMS (Contentful, Sanity, etc.)**: the content's source of truth moves into someone else's database. This blog doubles as my portfolio, and I didn't want the canonical copy of my writing living outside my repository. If the service shuts down or changes its pricing, the posts become hostages. **A custom admin on a database**: buildable, but running a database and an admin panel for one personal blog is the tail wagging the dog. **A file-based CMS (Keystatic, TinaCMS, Decap)**: content stays in git exactly as it is, with an editing UI layered on top. Not a single line of my existing MDX-reading logic has to change. This is the category I went with, and Keystatic looked like the least friction within it — the schema is defined in TypeScript, and it mounts into the Next.js App Router as a single route. ## The setup Adoption itself was half a day's work: 1. Install `@keystatic/core` and `@keystatic/next` 2. Define a collection in `keystatic.config.ts` mirroring the existing frontmatter schema (title, excerpt, publishedAt, category, tags, coverImage, faqs) 3. Add the Admin UI route at `/keystatic` in the App Router 4. Exclude `/keystatic` from the i18n middleware matcher — skip this and the admin gets caught in locale redirects I kept storage in local mode. With `npm run dev` running, anything I edit at `/keystatic` is written straight to local files, and I commit as usual. There's an env-var switch to GitHub mode, but for a one-person blog, local is plenty. ## This blog's particular problem: ko/en pairs Posts here exist as a Korean–English pair (`content/posts/ko/` and `content/posts/en/`). Keystatic has no concept of "these two entries are translations of the same post." I ended up defining two separate collections, `posts-ko` and `posts-en`, and keeping the slugs aligned and the content in sync is still a human job. What the CMS solved was *formatting mistakes*, not *operational rules* — something I only saw clearly after adopting it. ## What changed, what didn't **Changed**: frontmatter typos can no longer break the build. The schema is TypeScript, so fields like category are a dropdown. I can write and edit straight from the browser. **Unchanged**: keeping ko/en in sync, translating, wrangling images. The parts of writing that actually take time are beyond any CMS. Honestly, even after adopting it, a doubt lingered for a while: was it right to pass on something as battle-tested as WordPress for this? Whenever it comes back, I return to the original criterion — every post stays as plain text inside my own repository. On that criterion, this is still the right call. Honestly, a CMS doesn't write the posts for me. It just removed one excuse not to write — and for me, even that much made it worth adopting. --- ## Thoughts about giving back URL: https://writingdeveloper.blog/en/blog/thoughts-about-giving-back Published: 2023-06-25 | Updated: 2026-06-10 Author: 이시형 It's been about three and a half months since I arrived in the United States. Looking back, this has been a season of receiving. I traveled in other people's cars, saved on insurance because of someone's advice, and some weeks, a meal someone bought me was the best meal I had. I thought I came here alone, but there have been very few days I actually got through alone. It was the same at work. My colleagues did a lot to help me find my footing. There was friction too, of course — it's people, after all. At first those moments stayed with me longer than they should have, but at some point I accepted it: nobody is perfect, and the people who helped me and the people I clashed with were mostly the same people. If that's true, which side I choose to remember is up to me. ## The tension from *The 48 Laws of Power* I've been reading *The 48 Laws of Power* and I'm nearly done. The book treats every favor as a transaction. Kindness has an agenda, debts get called in, and people who don't see the mechanics get played. Honestly, it's a useful book — I can't pretend people like that don't exist. But something about it sat wrong with me the whole way through. Looking back at my three and a half months through the book's lens, the people who helped me must have been running some calculation. I've gone over it again and again, and they weren't. The colleague who drove me around, the person who walked me through insurance — there was nothing they stood to collect from me. It's not that the book is wrong; it's that the people it can't explain were right there around me. ## So what does paying it back mean "There's no free lunch" is probably true. And yet the people who bought me lunch never once handed me a bill. I spent a long time trying to make sense of that gap. Here's where I've landed, for now. Returning a favor to the person who gave it is a transaction. Passing it on to the next person is generosity. The people who helped me were probably handing me something they once received from someone else. Right now I don't have much to give. I'm still in the receiving season of this, and I can admit that. But I'm writing down this ledger of debts so I don't forget it. Someday I'd like to hand someone — someone as lost as I was — the same thing, probably with whatever skills I end up having. I don't know yet what shape that takes. I'm writing down that I don't know, too. --- ## Learn English with ChatGPT and experience the use of English in interviews URL: https://writingdeveloper.blog/en/blog/studying-english-with-chatgpt Published: 2023-04-23 | Updated: 2026-06-10 Author: 이시형 It has been 1 month and 3 weeks since I arrived from South Korea. A lot has happened since then, but the most obvious thought has been about English. I can talk to natives in the US and usually understand what they are saying. But still, it's really hard to know the exact meanings naturally. People in North Carolina say it's hard to hear English with their accents, but I don't feel like there's that much of a difference. ## Learning English with AI Tools Nowadays, I learn English with ChatGPT and DeepL translator. Their AI technologies are really amazing and it will change the world I believe. I use a few methods to learn English. First, I just write my diary in Korean language and then I write it in English. And check with DeepL translator and finally, use the DeepL Write function to check alternative words or sentences. When I was young, I learned English in the academy for a long time, and it takes a lot of time and money. But it seems that for now, with some AI tools, it could be free and efficient ways to learn some languages. ## Interview Experience Recently, I interviewed at a company and I felt that I need to learn English more. Not only that, I have to learn how to talk and how to choose what to talk about. After the interview, I remembered what I said in the interview and I realized that in the interview I said things that I didn't need to say. This happened because of lack of interview experience and understanding of the culture of speaking in the US. ## Finding Ways to Practice To solve this problem, I need to talk with English speakers. But still it's really hard to practice in this environment because, in this company, so many people use Spanish. So I have to speak English myself, and actually this method looks fine. Or I can use some Google Chrome extensions to talk with a microphone. I use the "Talk-to-ChatGPT" extension for this. It can help to use a mic in ChatGPT and I can feel like talking to someone. This kind of AI-powered language learning has been a game changer for me, and I believe it will continue to improve over time.