WIKIHarbormaster
개인 제품·2026.09 ~ · 마켓플레이스 발행·기획·개발 단독

Harbormaster

듣고 있는 포트가 어느 프로젝트의 것인지 알려주는 확장.

JavaScriptVS Code Extension APINode.jslsof/psPowerShell CIM

설치 · VS Code 마켓플레이스 · 소스 · github.com/gnldnd11/harbormaster

Harbormaster는?

내 컴퓨터에서 지금 듣고 있는 포트가 어느 프로젝트의 것인지를 보여주는 VS Code 확장이다. 다른 포트 확장이 node (pid 8231)까지 말하는 자리에서, 이건 the-ggi가 5173에서 사흘째 떠 있고 아직 응답한다고 말한다.

여기에 뒤집은 목록이 하나 더 붙는다. 열어 둔 폴더가 선언한 포트 중 아무도 안 띄우고 있는 것이다. 그래서 패널이 "도는 프로세스 목록"이 아니라 내 프로젝트들의 상태판이 된다.

VS Code 마켓플레이스에 saegim.harbormaster로 발행했다. 의존성 0개, 빌드 스텝 0개, 순수 JavaScript다.

위는 떠 있는 것, 아래는 선언해 놨는데 안 떠 있는 것. 포트 옆에 프로젝트 이름이 붙는다.
위는 떠 있는 것, 아래는 선언해 놨는데 안 떠 있는 것. 포트 옆에 프로젝트 이름이 붙는다.

왜 만들었나

dev 서버는 원래 사람이 띄우고 사람이 기억하던 것이었다. 지금은 코딩 에이전트가 백그라운드로 띄우고, 세션은 끝나는데 서버는 안 끝난다. 사흘 뒤 npm run dev가 포트를 물렸다고 하면 터미널을 열어 lsof -i :5173을 친다. 이 왕복이 없애고 싶었던 것이다.

시장을 먼저 봤다. 재밌는 모양이었다.

24K
1등 설치 수
2021
그 1등의 마지막 갱신
8개
최근 1년의 도전자
1천 대
그 여덟이 멈춘 자리

1등과 2등이 5년, 6년 전에 멈춘 죽은 확장인데 아직 1등과 2등이다. 최근 1년에 여덟 개가 새로 나온 걸 보면 수요는 있는데, 그 여덟이 전부 1천 대에서 멈췄다. (2026-09-01 마켓플레이스 기준)

왜 다 멈췄는지는 이름을 보면 대충 짐작이 간다. port-killer, kill-node-process, kill-switch. 전부 "포트 죽이기"다. 그런데 포트를 죽이는 건 일주일에 한 번 있는 일이고, 그때만 쓰는 도구는 기억에 안 남는다. 그리고 화면이 전부 이렇게 생겼다.

3000   node   pid 8231   [kill]
5173   node   pid 8944   [kill]
6379   redis  pid 966    [kill]

어느 게 내 것인지 알 수 없다. 포트가 스무 개 뜨는 사람에게 이 목록은 아무 말도 하지 않는다.

그래서 차별점을 하나로 잡았다. 이 포트가 어느 프로젝트인지 알려주는 것. 여기가 무너지면 만들 이유가 없다는 문장을 저장소 문서에 적어 두고 시작했다.

듣고 있는 포트를 프로젝트 단위로 묶어 보여준다
시스템 프로세스는 접어서 따로 둔다
정말 응답하는지 세 상태로 구분한다
오래 떠 있는 것은 목록 맨 위로 올린다
선언해 놨는데 안 떠 있는 프로젝트를 같이 보여준다
한 번에 끄고, 명령을 보여주고 켠다

어느 프로젝트인지 알아내기

처음 판정은 프로세스의 작업 디렉터리 하나였다. 거기서 위로 올라가며 .git을 찾고 그 폴더 이름을 쓴다. 모노레포는 저장소 이름만 쓰면 스무 개가 다 같아지므로 하위 폴더까지 같이 낸다(saegim-studio/the-ggi).

이게 맥에서는 잘 됐다. 그리고 Windows에서 통째로 죽는다는 걸 알았다. Windows에는 남의 프로세스 작업 디렉터리를 읽는 표준 방법이 없다.

여기서 플랫폼별로 제품을 두 벌 만드는 길이 있었는데, 그러지 않고 문제 정의를 고쳤다. 필요한 건 작업 디렉터리가 아니라 프로젝트 이름이다. 작업 디렉터리는 답이 아니라 증거 하나다.

1. 작업 디렉터리
가장 확실하다. 맥·리눅스에서만 얻을 수 있다
2. 실행 파일 경로
dev 서버는 대개 프로젝트 안에서 돈다 (node_modules/.bin/vite)
3. 명령줄 인자 속 경로
node /home/me/dev/app/server.js
4. 열린 폴더가 선언한 포트
프로세스가 아무 말도 안 해도 편집기는 알고 있다

확신이 높은 것부터 훑고 처음 맞은 데서 멈춘다. 그래서 판정 코드는 한 벌이고 플랫폼을 모른다. 어느 증거로 맞혔는지는 결과에 남는다.

맥에서 작업 디렉터리 증거만 끄는 스위치를 만들어 Windows 상황을 재현해 봤다. 듣는 포트 21개, 판정 2건, 오탐 0건. 스위치를 켜도 껐을 때와 판정 개수도 프로젝트 이름도 같았다. npm run dev가 절대 경로로 vite를 부르기 때문에 3번 증거가 답을 낸다.

여기에 함정이 하나 있었다. "앞쪽 토막일수록 진짜"라고 가정했는데 이런 게 걸렸다.

node server.js --config /Users/.../projects/0to1/x.json   →  0to1 로 판정

첫 토막이 상대 경로라 죽고, 남의 저장소에 있는 설정 파일 경로가 답이 됐다. 공용 설정이나 인증서 경로를 인자로 받는 서버는 흔하다. 그래서 첫 플래그가 나오면 거기서 멈추고, 남은 토막들이 서로 다른 프로젝트를 가리키면 판정을 포기한다. 이때 비교는 저장소 루트가 아니라 이름으로 한다. 모노레포는 루트가 같아서 루트로 비교하면 둘이 같은 답으로 통과한다.

잡음통이 되지 않으려면 경계가 필요하다

.git을 찾아 올라가기만 하면 시스템 프로세스가 프로젝트로 올라온다. 두 번 걸렸다.

  • /opt/homebrew 자체가 git 저장소여서 redis가 homebrew/redis라는 프로젝트가 됐다.
  • 홈 디렉터리에 .git이 있어서 pylance 확장 폴더가 kimhwiung/dist라는 프로젝트가 됐다.

그래서 시스템 경로 아래는 판정하지 않고, 홈 디렉터리를 저장소 루트로 인정하지 않고, 편집기 확장 자리와 툴체인 자리(~/.nvm, ~/.pyenv 같은 것)를 뺀다. 툴체인 제외는 2번 증거를 받으면서 새로 필요해졌다. 작업 디렉터리가 툴체인 폴더를 가리키는 일은 거의 없지만 실행 파일은 거의 항상 거기 있다(~/.nvm/versions/node/v20/bin/node).

Decision #01/var 를 통째로 막지 않는다

시스템 경로를 막다 보면 /var도 막고 싶어지는데, 리눅스 웹 프로젝트가 /var/www/myapp에 사는 건 흔하다. 통째로 막으면 정탐을 죽인다. macOS 임시 자리인 /private/var/folders만 좁혀서 막았다.

경계를 손대면 판정 2건 · 오탐 0건이 유지되는지 다시 재라고 문서에 못박아 뒀다. 그 수치가 경계를 넣은 근거이기 때문이다.

안 떠 있는 것도 보여준다

4번 증거는 양방향이다. 열어 둔 폴더들이 어느 포트를 선언했는지 알고 나면, 그중 아무도 안 띄우고 있는 것도 알게 된다.

이건 프로세스 표를 읽는 확장이 구조적으로 못 하는 일이다. 안 도는 프로젝트는 프로세스 표에 없으니 존재 자체를 모른다. 그래서 이 구획이 붙고 나면 서버를 하나도 안 띄운 상태에서도 화면에 볼 것이 있다.

다만 이걸 기존 목록에 섞지 않고 별도 구획으로 뒀다. 떠 있는 줄은 전부 pid를 가진 실체다. 끄기도 구동 시간도 응답 여부도 전부 그 실체에 매달려 있어서, pid 없는 줄을 섞으면 목록의 모든 열이 "있을 수도 없을 수도 있는 값"이 된다.

묻히지 않게 하는 것이 이 구획 설계의 전부라, 상한을 네 줄로 잡고(떠 있는 게 하나도 없으면 여덟 줄) 나머지는 Show N more로 접었다. 한 폴더가 포트를 셋 선언해도 상한을 독식하지 않게 프로젝트 단위로 돌아가며 뽑는다.

듣고 있는 포트는 이 목록에 절대 넣지 않는다. 넣으면 켜기 버튼이 EADDRINUSE를 낸다. 이 확장이 없애려는 고통을 이 확장이 만드는 꼴이라, 판정이 어디로 갔든 무조건 뺀다.

남의 편집기 안에서 파일을 뒤진다는 것

4번 증거를 얻으려면 열린 폴더의 package.json scripts, Vite·Next 설정, .env를 읽어야 한다. 처음엔 워크스페이스 루트만 봤는데, 모노레포를 통째로 연 사람에게는 이 기능이 통째로 죽었다. 루트 package.json 하나만 보이니까.

그래서 깊이 2까지 내려가게 넓혔고, 넓힌 만큼 상한을 넷 걸었다.

2
탐색 깊이
100
폴더 상한
600
디렉터리 상한
250ms
전체 벽시계

벽시계가 따로 필요한 이유가 있다. 이 파일 읽기가 확장 호스트 스레드에서 동기로 돈다. SSHFS나 SMB 같은 네트워크 마운트에서는 파일 하나 stat하는 데 수십 ms가 걸려서, 폴더 개수 상한만으로는 못 막는다. 목록이 조금 비는 건 되돌릴 수 있고 편집기가 멎는 건 되돌릴 수 없다.

상한에 걸리면 거기서 멈추고 경고를 남긴다. 조용히 자르지 않는다.

읽는 방식에도 규칙을 뒀다. 설정 파일을 실행하지 않는다. 정규식으로 숫자만 뽑는다. 그리고 한 포트를 두 폴더가 선언했으면 그 포트를 지운다. 틀린 답이 모르는 것보다 나쁘다.

숫자만 뽑는 쪽이 오히려 함정이었다. 4번 증거는 다른 증거가 다 죽었을 때 쓰이니까 대상이 대개 시스템 서비스고, 여기서 한 건 틀리면 곧바로 오탐이다. 넷이 걸렸다.

입력읽어버린 값막은 방법
vite --port 12345612345자릿수를 잘라 쓰지 않는다
PORT=12345612345위와 같음
// port: 9999 (old)9999주석을 먼저 걷어낸다
... && cp -p 1234 x1234-p가 포트만 뜻하지 않는다. 알려진 실행기 뒤에서만 본다

되돌릴 수 있는 것과 없는 것

이 확장에서 제일 오래 생각한 자리다.

끄기는 되돌릴 수 없고 남의 프로세스를 죽인다. 그래서 웹뷰가 보낸 pid를 절대 그대로 쓰지 않는다. 신호를 보내기 전에 네 겹을 지난다.

겹막는 것
확장이 직접 잰 목록에 그 줄이 있는가메시지 위조, 그리고 이미 사라진 줄
이 편집기나 그 부모 프로세스가 아닌가확장이 자기를 죽이는 것
확인 창오조작
시작 시각을 그 자리에서 다시 재서 대조pid 재사용. 확인 창은 무한정 열려 있을 수 있다

두 번째 겹은 실제로 필요했다. 확장 호스트도 포트를 듣는다. 실측에서 Code Helper (Plugin) 하나가 세 포트로 떴고, 확장 개발 중이면 그 프로세스의 작업 디렉터리가 워크스페이스라 Other가 아니라 목록 맨 위에 뜬다. 조상 체인을 전부 제외하는 것으로 막았다.

신원을 확인할 수 없으면 확인 창을 띄우기 전에 거절한다. 시작 시각을 못 재면 네 번째 겹이 할 일이 없어져 안전장치가 셋이 되는데, 사용자는 그걸 모른 채 끄게 된다. Windows에서 프로세스 표를 못 받았을 때가 그 경우이고, 그때는 경고 문구가 끄기가 꺼졌다는 것도 같이 말한다.

그리고 "못 쟀다"와 "이미 없다"를 하나로 합치지 않는다. 합치면 PowerShell이 타임아웃에 걸렸을 때 멀쩡히 살아 있는 서버에 이미 종료됐다고 말하게 된다. 동작은 안전한 쪽이지만, 그건 확인하지 않고 단정하는 것이다.

Decision #02켜기에는 확인 창을 두지 않는다

켜기도 물어보면 안전해 보이지만 그렇지 않다. 둘 다 모달로 물으면 모달의 뜻이 "위험"에서 "확인"으로 희석되고, 끄기의 모달이 약해진다. 되돌릴 수 없는 쪽만 물어본다.

대신 위조 방어는 끄기와 같은 구조다. 웹뷰는 포트 하나만 보내고, 실행할 폴더와 스크립트는 확장이 자기 목록에서 다시 꺼낸다. 웹뷰가 보낸 경로는 쳐다보지 않는다.

켜기를 넣고 나서 한 번 되돌린 게 있다. 처음엔 "명령을 추측하지 않는다"며 package.json scripts에서 직접 온 포트에만 버튼을 달았는데, 실측해 보니 아홉 줄 중 여덟 줄이 버튼을 잃었다. 포트를 설정 파일에 적는 게 오히려 흔했다.

근거 둘로 되돌렸다. 켜기는 되돌릴 수 있다. 터미널이 열리고 눈앞에서 돈다. 틀리게 켰으면 닫으면 그만이다. 그리고 이건 추측이 아니라 연결이다. 포트가 vite.config.ts에 있고 같은 폴더 package.json에 dev가 있으면, 그 dev가 그 vite를 띄운다.

대신 숨기지 않는다. 버튼에 적힌 문구가 실제로 돌 명령 그대로다(Start npm run dev in nara-hbd). 그리고 스크립트 이름은 디스크에서 온 문자열이라 dev; rm -rf ~ 같은 이름도 만들 수 있어서, 안전한 글자로만 이루어진 이름이 아니면 실행 대상에서 뺀다.

문구가 한 플랫폼에서 거짓이었다

발행 직전에 잡은 것이다.

끄기 확인 창에 Sends SIGTERM이라고 적혀 있었다. Windows에는 SIGTERM이 없다. 거기서는 taskkill을 쓰고, 그건 그 프로세스가 띄운 것까지 트리째 끝낸다. npm run dev로 띄운 서버가 Windows에서는 자식을 데리고 죽고 나머지 둘에서는 안 그런다. 그 말은 어디에도 없었다.

되돌릴 수 없는 유일한 동작의 문구가 세 플랫폼 중 하나에서 거짓이었다. 한 문장으로 뭉뚱그리면 둘 중 하나는 반드시 거짓이 되므로, 확인 창도 로그도 플랫폼별로 갈라 쓰게 고쳤다.

살았는지는 셋으로 가른다

열린 포트가 작동하는 앱을 뜻하지는 않는다. 그래서 짧은 HTTP 요청을 보내고 답을 셋 중 하나로 낸다.

상태뜻
upHTTP 응답이 왔다. 상태 코드까지 보여준다
listeningTCP는 붙었는데 HTTP 응답이 없다. HTTP 서버가 아니거나 멈췄다
not responding연결이 거부됐다

둘로 가르면 안 된다. redis 같은 비-HTTP 서버가 매번 죽은 것으로 뜬다. 확실한 "죽었다" 신호는 연결 거부 하나뿐이고, 나머지 에러는 전부 listening이다.

두드리기가 실패해도 행이 사라지지는 않는다. 상태만 바뀐다.

기본은 프로젝트로 판정된 것만 두드린다. 시스템 서비스까지 10초마다 두드리면 얻는 것 없이 남의 프로세스에 요청만 쌓인다. 같은 이유로 패널이 접혀 있으면 두드리기를 아예 멈춘다. 그렇다고 완전히 멈추면 상태바 숫자가 굳으니, 숨어 있는 동안에는 1분에 한 번 목록만 훑는다.

화면에서 뺀 것들

처음 만든 패널을 열고 3초 안에 세 가지가 보였다. 제목이 두 번 나오고 새로고침 버튼이 두 개다. 글자가 전부 잘려 있다(9d · H..., long-runni...). 노란 경고가 붙은 줄이 여섯 갠데 전부 redis, Discord, ControlCenter라 손댈 것이 하나도 없다.

셋 다 같은 원인이었다. 한 줄이 다섯 조각을 들고 있고, 그중 판단에 쓰이는 건 한둘뿐이다.

그래서 패널이 답할 질문을 한 문장으로 적고, 거기 기여하지 않는 것을 상시 노출에서 뺐다. 셋이 나갔다.

  • 상시 pid. pid가 필요한 순간은 끄기 하나뿐이고, 그 순간에 확인 창이 보여준다. 상시로 두면 매 줄 아홉 자를 먹으면서 판단에는 안 들어간다.
  • 프로젝트 줄의 명령 이름. 판정된 줄에서 node는 정보량이 0이다. 다만 판정 안 된 줄에서는 그게 유일한 신원이라 Other 쪽에는 남겼다.
  • long-running 문구. 오래 떠 있다는 건 문구 대신 목록 맨 위로 올려서 말한다. 위치가 색보다 강한 신호다.

배를 그렸다가 뺀 이야기

이름이 항만장이라 은유가 그대로 맞는다. 정박한 배가 지금 듣고 있는 서버, 오래 정박한 배가 잊고 방치한 dev 서버, 내보내기가 끄기다.

그래서 상태 표식을 배 세 척으로 만들었다. 11 x 12px에 선체 사다리꼴과 1px 돛대, 응답하면 채우고 아니면 윤곽선, 죽었으면 28도 기울이는 식이다. 코드까지 들어갔고 실기 패널에서 확인했다.

배로 안 읽혔다. 새싹이나 모종삽으로 보였다.

원인이 둘이었다. 폭 11px에 높이 12px이라 세로로 서 있었다. 배는 가로로 길어야 배인데 아래가 좁은 사다리꼴 위에 세로선이 서면 그건 줄기 달린 싹이다. 그리고 돛대라는 은유부터 틀렸다. 항만장이 관리하는 건 화물선이지 범선이 아니다.

여기서 더 파고들 게 있었다. 1차 안은 상태 셋을 배 하나에 몰아서 지웠다. 그러면 배가 정보를 나르는 부품이 되고, 정보를 나르려면 모양이 셋으로 갈려야 하니 채움·윤곽선·기울임이라는 제약이 붙고, 그 제약이 다시 배를 배답게 그릴 자유를 뺏는다.

Decision #03하나의 그림에 은유와 판정을 동시에 시키지 않는다

둘을 갈랐다. 상태는 점이 진다. 모양 셋이 갈리기만 하면 되고 은유를 질 필요가 없다. 채운 원, 고리, X 셋은 실루엣이 전부 다르다. 배는 그림 노릇만 한다. 상태를 안 지므로 모든 행에서 같아도 되고, 배답게만 그리면 된다.

그러고 나서 배에게 물었다. 모든 행에서 똑같은 그림이 220px 폭에서 26px을 먹을 값을 하는가.

계산은 이랬다. 상태를 부두선 위로 옮기면 이름에 쓸 폭이 119px에서 138px로 는다. 거기 배를 도로 넣으면 112px이다. 번 것을 다 쓰고 7px을 더 꾼다. 약 네 자 차이인데, 그 네 자가 claude-usage-monitor 같은 이름을 자른다.

이 확장의 유일한 차별점이 프로젝트 이름이다. 이름이 잘리는 건 여덟 개가 실패한 그 자리로 되돌아가는 것이다. 그래서 배를 행에서 뺐고, 나중에는 빈 화면의 배마저 뺐다. 220px에서 70px을 먹으면서 새로 알려주는 게 하나도 없었다.

은유는 부두선이 혼자 진다. 1px 세로선 하나가 항구 노릇을 하고, 폭은 0px을 먹는다. 그리고 상태 점이 그 선 위에 마디처럼 앉는다.

Decision #04listening 을 노란색에서 뺐다

VS Code 기본 테마의 경고 노랑을 라이트 사이드바 배경에 놓고 재 보니 2.70:1이었다. 비텍스트 그래픽 요소 기준 3:1에 미달한다. VS Code 자신의 경고 노랑도 2.93:1이라 폴백 값을 손봐도 소용이 없다.

대비보다 중요한 이유가 하나 더 있었다. listening은 경고가 아니다. redis 6379는 영원히 listening이지만 완벽히 건강하다. 노랑을 칠하면 멀쩡한 redis를 매번 경고로 표시하는 것이고, 그러면 진짜 볼 일이 있는 줄의 색이 희석된다.

무채색으로 내렸다. 색이 붙은 줄 = 판정이 난 줄이라는 규칙이 대신 선명해졌다.

색을 정보의 주 채널로 쓰지 않는 건 고대비 테마 때문이기도 하다. 거기서는 세 표식이 전부 한 색이 되고, 그 환경에서 색은 정보를 하나도 안 나른다. 그때 고리가 강제로 메워져 채운 원과 같아지지 않도록 CSS로 한 번 더 못박아 뒀다.

배가 지금 사는 자리는 마켓플레이스 아이콘 하나다. 128px에는 자리가 있어서 야간 항만에 정박한 컨테이너선을 그렸고, 용골과 수면을 같은 높이에 두어 배가 물에 얹히게 했다. 32px로 줄여서 확인했는데, 컨테이너 높이를 서로 다르게 한 게 조건이었다. 같은 높이 넷이면 그 크기에서 띠 하나로 합쳐진다.

기술 스택

  • 런타임: VS Code Extension API (^1.75) · Node.js · 순수 JavaScript (의존성 0, 빌드 없음)
  • 수집: macOS lsof·ps / Linux lsof·ss·/proc / Windows netstat·PowerShell Win32_Process
  • 화면: 웹뷰 (인라인 SVG 아이콘, VS Code 테마 변수) · 상태바 아이템
  • 배포: VS Code 마켓플레이스 (saegim.harbormaster) · 공개 미러 레포 · MIT

왜 이 스택

  • 의존성 0, 빌드 없음: 확장이 남의 프로세스를 끄니까, 배포물과 소스가 1:1로 대조되는 편이 낫다고 봤다. 크랩과 같은 결이다.
  • 수집 층만 플랫폼을 안다: 판정·두드리기·끄기 모듈은 vscode를 부르지 않는다. 그래서 node로 그대로 돌려볼 수 있고, 빌드 스텝이 없어 타입 체크도 번들 검증도 없는 프로젝트에서 그게 유일한 검증 수단이 된다.
  • 웹뷰: 트리 뷰로는 두 행 밀도와 hover 액션, 구획별 다른 밀도를 못 만든다.

검증

빌드가 없으니 타입 체크가 없다. 대신 vscode를 안 쓰는 다섯 모듈을 node로 직접 돌려서 잰다. 작업 디렉터리 증거만 끄는 환경 변수가 있어서, 맥에서 Windows 상황을 재현해 폴백 사슬을 잴 수 있다. 판정 경계를 손대면 켠 값과 끈 값을 둘 다 재야 한다. 한쪽만 재면 폴백이 죽은 걸 못 잡는다.

끄기의 pid 재사용 방어처럼 재현하기 어려운 경합은, 테스트 모드에서 살아 있는 객체를 꺼내 시작 시각을 흔들어 만든다. 좀비 프로세스는 perl로 만든다. node나 sh는 자식을 곧바로 거둬 가서 좀비가 안 생긴다.

정직한 한계

Windows와 리눅스 수집 경로는 코드만 있고 실기로 돌려본 적이 없다. netstat, PowerShell Win32_Process, taskkill, 리눅스 ss와 /proc 전부 그렇다. 이건 확장 소개와 CHANGELOG에도 그대로 적어뒀다. 미리 밝히면 이슈로 오고 숨기면 별점으로 온다.

HTTPS로만 듣는 서버는 listening으로 뜬다. http로만 두드린다. 마일스톤을 닫으려고 의도적으로 미룬 것이다.

Windows에는 작업 디렉터리 증거가 없다. 남은 셋이 답을 지지만, 셸에서 상대 경로로 직접 실행한 경우처럼 명령줄 증거가 죽으면 4번 증거에 기대게 된다.

선언 포트 탐색은 깊이 2까지다. 그보다 깊은 모노레포는 못 본다. 넓히면 잡음통이 되고, 그게 여덟 개가 실패한 방식이라 일부러 안 넓혔다.

발행한 지 얼마 안 돼서 쓸 만한 사용 지표가 없다.

회고

이 확장의 값은 포트 목록을 하나 더 만든 데 있지 않다. 세 가지다.

죽은 1등이 있는 자리에 들어가면서, 먼저 나온 여덟 개가 왜 멈췄는지를 읽었다. 그리고 차별점을 하나로 좁혔다. 좁힌 것을 지키려고 배 그림을 행에서 빼고 이름에 쓸 19px을 지켰다. 차별점을 정하는 것보다 그걸 침범하는 것을 계속 거절하는 쪽이 어려웠다.

플랫폼 제약 앞에서 제품을 두 벌 만들지 않고 문제 정의를 고쳤다. Windows가 작업 디렉터리를 안 준다는 사실에 부딪쳤을 때, "작업 디렉터리가 답"이라는 전제를 버리고 "증거 하나"로 내렸다. 그 덕에 판정 코드가 한 벌이고 플랫폼을 모른다.

되돌릴 수 있는 것과 없는 것을 갈라서 다르게 대했다. 끄기는 네 겹에 확인 창까지, 켜기는 확인 창 없이 명령 전문 공개. 그리고 되돌릴 수 없는 그 동작의 문구가 한 플랫폼에서 거짓이었다는 걸 발행 직전에 잡았다.

그 아래를 받치는 건 결국 평범한 것들이다. 편집기를 안 멎게 하려는 벽시계 상한, 잘린 출력을 성공으로 안 쓰는 분기, 좀비를 정지 실패로 안 부르는 판정, 접힌 패널에서 남의 서버를 안 두드리는 예의.