ClawPorts: 내 서버를 파괴하며 탄생한 네온빛 포트 킬러
요약
실수로 모든 프로세스를 종료하는 실수를 방지하기 위해 개발된 Windows용 TCP 포트 관리 도구 ClawPorts를 소개합니다. Electron 기반의 데스크톱 앱으로, 리스닝 중인 포트와 프로세스 정보를 시각화하고 안전한 확인 절차를 통해 개별 포트를 종료할 수 있습니다.
핵심 포인트
- Electron 기반의 단일 화면 Windows 데스크톱 애플리케이션
- 포트별 라벨링 기능을 통해 프로세스 식별 용이성 제공
- 실수 방지를 위한 프로세스 종료 전 확인 모달 설계
- Claude를 페어 프로그래머로 활용한 TDD 방식의 개발 과정
저는 한 번 포트를 비우기 위해 taskkill /F /IM python.exe를 실행했다가, 해당 머신의 모든 Python 프로세스 — 모든 개발 서버와 모든 백그라운드 스크립트가 한꺼번에 — 를 종료해 버린 적이 있습니다. 명령의 의도는 정밀한 수술이었으나 결과는 학살이었습니다. ClawPorts는 그날 저에게 있었기를 바랐던 도구입니다. 실제로 리스닝(listening) 중인 모든 TCP 포트를 나열하고, 어떤 프로세스가 이를 소유하고 있는지 보여주며, 확인 모달(confirmation modal)을 통해 하나씩 차례대로 종료할 수 있게 해줍니다.
이것은 무엇인가
ClawPorts는 단일 화면으로 구성된 Windows Electron 데스크톱 앱입니다. 리스닝(listening) 중인 TCP 포트를 스캔하여 포트 번호, 편집 가능한 라벨, 소유 프로세스 이름, PID, 연결 상태(connection state), 그리고 각 행마다 Kill 버튼이 포함된 테이블로 보여줍니다. 상단 바에는 '66개 포트 열림' 카운터가 있고, 토글 가능한 3초 자동 새로고침 기능과 수동 새로고침(Refresh) 버튼이 있습니다. 스타일링은 네온 신스웨이브(neon-synthwave) 스타일로, 거의 검은색에 가까운 배경 위에 마젠타와 사이언(magenta-and-cyan) 빛이 감도는 디자인입니다. 이는 제가 ClawMonitor에 적용할 계획인 것과 동일한 모습입니다.
라벨(labels) 열은 제가 가장 자주 사용하는 부분입니다. 어떤 행에서든 "라벨 추가(add label)"를 클릭하고 이름을 입력하면 저장됩니다. 저는 항상 잊어버리는 것들을 미리 입력해 두었습니다: 18789는 OpenClaw 게이트웨이, 11434는 Ollama, 그리고 일반적인 3000/5173/8000 개발 서버 의심 포트들입니다. 이러한 기본값들은 첫 실행 시 기록되므로, 테이블이 단순한 포트 번호의 나열이 아니라 제 머신의 지도처럼 읽히게 됩니다.
종료(Killing) 과정은 정확히 한 곳에서 의도적으로 번거롭게 설계되었습니다. 행의 Kill을 누르면 "port 1043의 asus_framework — PID 10340를 종료하시겠습니까?"라는 모달이 나타나며, 취소(Cancel)와 종료(Kill) 버튼이 제공됩니다. 프로세스 하나를 지정하고, 이름을 확인하고, 승인하면 사라집니다. 성공적으로 종료되면 테이블이 즉시 다시 스캔되어 포트가 사라지는 것을 볼 수 있습니다. 이것이 전체 설계 철학입니다: 다시는 실수로 전체 프로세스를 한꺼번에 종료하는 일이 없도록 하는 것입니다.
스택은 엄격한 메인/렌더러 (main/renderer) 분리가 적용된 순수 Electron이며, 렌더러 측에는 프레임워크를 사용하지 않았고, 테스트 스위트(test suite)를 위해 Node의 내장 node:test를 사용했습니다. React도, 번들러(bundler)도 없습니다. PowerShell과 통신하는 단일 화면 유틸리티로서는 이것이 딱 적절한 수준의 메커니즘이었습니다.
구축 방법
저는 서브에이전트 기반의 TDD (테스트 주도 개발)를 사용하여 Claude를 페어 프로그래머 (pair programmer)로 삼아 이를 구축했습니다. 우리는 사양 (spec)을 브레인스토밍하고 계획을 작성했으며, Claude는 별도의 리뷰어 패스 (reviewer passes)를 거치며 테스트 우선 방식으로 모듈을 구현했습니다. 사양과 계획은 docs/superpowers/에 저장되어 있습니다. 저는 최종 빌드를 승인하기 전에 라이브로 실행해 보았습니다.
아키텍처는 Electron 프로세스 경계 뒤에 있는 네 개의 작고 테스트 가능한 모듈로 구성됩니다. portScanner.js는 Get-Process로부터 프로세스 맵을 구축하고, Get-NetTCPConnection -State Listen을 순회하며, 연결당 하나의 정규화된 JSON 행 — {port, pid, processName, exePath, state, startTime} —을 생성하는 PowerShell 스크립트를 실행합니다. 이를 Node 측에서 파싱하고, 포트별로 중복을 제거한 뒤 정렬합니다. killer.js는 taskkill /F /PID를 셸 (shell) 명령으로 실행하고, 출력 텍스트를 검사하여 결과를 ok / access-denied (액세스 거부) / not-found (찾을 수 없음) / error (오류)로 분류합니다. labels.js는 사용자 정의 레이블을 Electron의 userData 디렉토리에 있는 JSON 파일에 영구 저장하며, 파일이 없거나 손상된 경우 초기화된 기본값으로 대체합니다. admin.js는 현재 프로세스가 승격된 토큰 (elevated token)을 보유하고 있는지 Windows에 확인합니다.
모든 것은 컨텍스트 격리된 IPC (context-isolated IPC)를 통해 프로세스 경계를 넘나듭니다. scan-ports, kill-pid, load-labels, save-label, is-admin, relaunch-admin과 같은 채널을 사용하며, 렌더러는 오직 좁은 범위의 프리로드 브릿지 (preload bridge)하고만 통신합니다. 렌더러는 Node나 PowerShell에 직접 접근하지 않습니다. 이러한 분리의 결실은 테스트 스위트입니다. 4개의 파일에 걸쳐 21개의 유닛 테스트 (unit tests)가 있으며, 모두 주입된 러너 (injected runners)를 통해 순수 로직 함수를 대상으로 실행됩니다. 따라서 파싱, 분류 및 레이블 로직을 테스트하는 데 Electron이나 실제 PowerShell이 전혀 필요하지 않습니다.
주의 사항 (The gotchas)
일부 포트는 SYSTEM을 가리키며, 권한 상승(elevation) 없이는 이를 종료할 수 없습니다. 일반 사용자 권한의 ClawPorts는 139, 445번 포트 및 관련 포트들을 점유하고 있는 PID 4(System)를 볼 수 있지만, 이들에 대해 taskkill을 실행하면 "액세스가 거부되었습니다(access is denied)"라는 오류가 반환됩니다. 그렇기 때문에 killer.js는 단순히 성공 또는 실패만을 보고하는 것이 아니라, 실패 유형을 분류합니다. 이를 통해 UI는 "액세스 거부, 관리자 권한이 필요함"과 "해당 PID가 이미 사라짐"을 구분하여 사용자에게 알려줄 수 있습니다. UX 측면에서의 해결책은 관리자 경로를 제공하는 것입니다. admin.js는 WindowsPrincipal 역할 확인을 통해 권한 상승 토큰(elevation token)을 검사하며, 상단 바에는 권한이 상승되었을 때 초록색 ADMIN 배지를 표시하거나, 그렇지 않을 때는 "관리자 권한으로 실행(Run as admin)" 버튼을 표시합니다. 이 버튼은 UAC 프롬프트(Start-Process -Verb RunAs)를 통해 앱을 재실행하며, 권한이 상승된 인스턴스는 마침내 보호된 PID들을 종료할 수 있게 됩니다.
무차별적인 종료(blanket kill)는 버그이므로, 킬러는 의도적으로 범위를 좁게 설정해야 했습니다. taskkill /F /IM <name> 명령어가 이미지 이름(image name)을 기준으로 일치하는 모든 프로세스를 타격하기 때문에, 이 프로젝트 전체가 존재하게 되었습니다. ClawPorts는 오직 PID를 통해서만 종료합니다 — taskkill /F /PID <n>, 즉 모달(modal)에서 확인한 바로 그 정확한 단일 프로세스 하나만을 종료합니다. 스캐너는 여러 포트에서 리스닝(listening) 중인 프로세스가 테이블을 노이즈로 채우지 않도록 포트별로 행을 중복 제거(dedupes)하지만, Kill 액션은 항상 해당 단일 행의 단일 PID로 범위가 제한됩니다. "모두 종료"라는 편의성을 제거하는 것 자체가 이 기능의 핵심이었습니다.
포트와 프로세스의 조인(join)은 데이터 누락 상황에서도 생존해야 합니다. 리스닝 포트(listening ports)를 가져오는 것은 쉽지만, 각 포트를 소유한 프로세스의 이름을 안정적으로 지정하는 작업은 까다롭습니다. 연결 목록을 나열한 시점과 프로세스 상세 정보를 요청하는 시점 사이에 프로세스가 사라질 수 있으며, StartTime과 Path는 프로세스가 살아있더라도 보호된 프로세스(protected processes)의 경우 오류를 발생시킬 수 있습니다. 스캐너는 PowerShell 내부에서 이러한 속성 읽기 작업을 try/catch로 감싸며(빈 문자열이나 null로 대체), Node 파서(parser)는 모든 필드를 강제 형변환(coerce)하여 데이터가 일부만 채워진 행 때문에 테이블이 충돌하는 일이 없도록 합니다. 그 결과, SYSTEM 행조차 깔끔하게 렌더링됩니다. 즉, 포트를 확인하고, PID 4를 확인하며, 왜 Kill 액션에 관리자 권한이 필요한지 이해할 수 있게 됩니다.
출시된 기능
v1 버전이 완성되어 제 컴퓨터에서 실행 중입니다. 전체 루프가 엔드 투 엔드(end-to-end)로 작동합니다. 실제 스캔을 통해 66개의 리스닝 포트를 찾아냈고, Kill 경로를 통해 테스트용 Python 리스너를 종료하는 것을 확인했으며, GUI도 깔끔하게 실행됩니다. 테스트 스위트(test suite)는 스캐너, 킬러, 레이블, 관리자 모듈에 걸쳐 21개의 유닛 테스트(unit tests)로 구성되어 있습니다. scripts/install-shortcut.ps1 스크립트가 있어 번들링된 Electron 바이너리를 가리키는 ClawPorts 바로가기를 바탕화면에 생성하므로, 다른 앱처럼 실행할 수 있습니다.
이것은 개인용 빌드입니다. 제 컴퓨터의 개별 git 리포지토리(repo)에 존재하며 npm start로 실행됩니다. 공개 다운로드는 제공되지 않습니다. 명확한 v2 아이디어들은 기록되어 있지만 현재로서는 범위 외(out of scope)입니다: 아웃바운드(outbound) 및 연결됨(established) 상태의 연결 표시, 프로세스별 행 그룹화, PID 복사 및 브라우저에서 열기 바로가기, 그리고 패키지 설치 프로그램 등이 있습니다. v1에서는 정확히 한 가지만을 원했고, 그것을 얻었습니다. 바로 제가 읽을 수 있는 포트 목록과, 실수로 모든 것에 작동하지 않을 Kill 버튼입니다.
이 프로젝트는 이러한 방식으로 구축된 프로젝트들에 대한 지속적인 시리즈의 일부입니다. 전체 목록은 projects page에서 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기