읽기 진행률:0%

내 도구의 알림이 얼마나 헛것인지 재봤습니다. 지표가 네 번 틀렸고, 매번 과대추정이었습니다

·Dev

재는 방법: 새로 저장하는 것이 없습니다

측정에 상태 파일을 새로 두는 안은 처음에 기각했습니다. 저장하면 그게 또 낡기 때문입니다. 애초에 이 문제가 "도구의 상태가 현실보다 낡아서" 생긴 건데, 그걸 재려고 또 낡을 파일을 만드는 건 말이 안 됩니다.

대신 이미 있는 걸 씁니다. 이 도구는 스캔 결과(snapshot.json)를 매번 커밋합니다. 그러니까 그 파일의 git 이력이 곧 시계열입니다. 2026-07-27 부터 258판이 쌓여 있었습니다. (이 글의 수치는 전부 그 시점 실행값입니다. 스캔할 때마다 판이 늘어나니 총계는 조금씩 움직입니다 — 아래 비율들은 다시 돌려도 같았습니다.)

정의는 이렇습니다.

  • 구간(streak): 한 작업이 "미완" 으로 연속 관측된 판들
  • 분모: 그 구간 동안 울린 알림
  • 분자: 그 구간에 아무 진전도 없이 종결된 경우 그 구간의 알림 전부 (진짜로 일이 있어서 끝났다면 그 사이에 뭔가 움직였을 테니까요)
  • 판정 불가: 아직 안 끝난 구간. 분모에서 뺍니다 — 판정 불가를 분모에 숨기면 비율이 좋아 보입니다

사람이 알림에 "이건 헛것이었다" 라고 표시하는 새 행동도 요구하지 않습니다. 그런 건 아무도 안 하니까요.

첫 결과는 60.9% 였습니다. 열 번 울리면 여섯 번이 헛것이라는 뜻입니다.

함정 ① 기간을 잘라 비교하면, 최근일수록 유리하게 나옵니다

60.9% 는 전 기간 뭉뚱그린 수라 쓸 데가 없었습니다. 중간에 탐지기를 한 번 고쳤는데 (스쿼시 머지를 못 보던 버그였습니다) 그게 효과가 있었는지 이 수로는 볼 수 없으니까요.

그래서 시계열을 그 날짜로 잘라 각각 계산했습니다.

구간 판수 오탐률
교체 전 148 54.7%
교체 후 109 26.0%
최근 한 달 71 2.0%

54.7% → 26.0% → 2.0%. 극적인 개선처럼 보입니다. 그리고 이건 못 쓰는 수입니다.

구간은 종결될 때만 판정됩니다. 그런데 시계열을 잘라 버리면, 잘린 구간은 닫힐 시간 자체가 없어서 전부 "판정 불가" 로 빠집니다. 최근 구간일수록 닫힌 게 적고, 닫힌 것만 분모에 들어가니 구조적으로 낮게 나옵니다.

판정 불가 수가 그걸 말해 주고 있었는데 제가 안 봤습니다. 824 → 1242 → 855. 비율이 좋아지는 동안 못 재고 있는 양은 오히려 늘고 있었습니다.

전체 시계열로 돌리고 구간을 시작일로 귀속하는 쪽으로 고쳤습니다.

함정 ② 경계를 걸친 구간을 어디에 넣느냐가 결론을 뒤집습니다

시작일로 귀속하니 교체 전이 54.7% 에서 68.5% 로 올라갔습니다. 개선 폭이 더 커 보였지만, 여기서 한 번 멈췄습니다. 왜 올라가지?

범인은 다섯 개 구간이었습니다.

39판  2026-08-19 → 2026-08-25   onreorderitem-migration
39판  2026-08-19 → 2026-08-25   pr-8
39판  2026-08-19 → 2026-08-25   storybook-10-vite-8
39판  2026-08-19 → 2026-08-25   ssh-timeout-test-unhandled-rejection
29판  2026-08-24 → 2026-08-25   pre-1.6.0-revert-20260824

시작은 교체 전이고 종결은 교체 당일입니다. 그리고 다섯이 전부 같은 날 닫혔습니다. 그날 일이 끝난 게 아닙니다. 고쳐진 탐지기가 "이것들은 이미 끝나 있었다" 를 발견한 것입니다. 구 탐지기가 스쿼시 머지를 못 봐서 쌓인 부채가 하루에 청산된 거죠.

(이 시점 지표는 다섯을 전부 오탐으로 셌습니다. 그중 하나는 다음 절에서 뒤집힙니다 — 최종 표에 경계 걸침이 4/5 로 나오는 게 그래서입니다.)

이걸 시작일로 귀속하면 교체 전이 부풀고, 종결일로 귀속하면 교체 후로 들어가 개선이 통째로 뒤집힙니다. 어느 쪽이든 같은 다섯 구간을 한쪽에 숨기는 것입니다. (이 시점 판수로는 185판이었습니다. 아래 함정 ③·④ 에서 두 번 더 교정돼서 최종 표에는 179판으로 나옵니다 — 글 안의 수치가 절마다 다른 것은 낡은 게 아니라 그 단계에서 실제로 잰 값입니다.)

그래서 코호트를 둘이 아니라 으로 갈랐습니다. 교체 전 · 경계 걸침 · 교체 후. 경계 걸침은 전후 비교에 넣지 않습니다 — 그건 탐지기 품질이 아니라 교체가 일으킨 일회성 효과니까요.

함정 ③ 커밋만 봐서, 푸시로 반응한 것을 소음으로 셌습니다

앞에서 분자를 "아무 진전도 없이 종결된 구간" 이라고 적었는데, 처음 구현은 진전을 커밋 하나로만 봤습니다. lastCommit 이 그대로면 아무 일도 없었다고 친 겁니다.

그런데 아까 그 다섯 중 하나인 pre-1.6.0-revert 의 스냅샷 추이를 보니 이랬습니다.

2026-08-25  status=pushing  ahead=0  pushed=false   lastCommit=2026-08-24
2026-08-25  status=paused   ahead=0  pushed=true    lastCommit=2026-08-24

알림을 보고 푸시로 반응했습니다. pushed: false → true 가 스냅샷에 그대로 찍혀 있는데, 지표는 lastCommit 만 비교하니까 "커밋 안 움직였으니 오탐" 으로 셌습니다. 알림이 제 역할을 했고 사람도 반응했는데 소음으로 집계된 겁니다.

여기서 소름이 돋았던 건, 제가 이 지표를 만든 계기로 코드 주석에 적어 둔 문장이 "5건을 알렸는데 4건이 거짓이었다" 였다는 점입니다. 사람인 저는 그때 4건이라고 옳게 판단했고, 지표는 5건이라고 셌습니다. 그 1건 차이가 정확히 이것이었습니다.

그래서 진전 신호를 넓혔습니다. 커밋뿐 아니라 푸시·머지·ahead 감소까지 봅니다. 18개 구간이 오탐에서 정상으로 뒤집혔습니다. 아까 그 다섯 중 pre-1.6.0-revert 도 여기서 정상이 됩니다 — 경계 걸침이 5/5 에서 4/5 가 되는 지점입니다.

알림이 울리는 동안 그 작업들이 실제로 dev → staging → production 으로 머지되고 있었습니다. 소음이 아니라 진행 중이었던 거죠.

정직하게 덧붙이면, 18구간이 전부 merged: false → true 를 포함합니다. 푸시 신호 단독으로 뒤집힌 구간은 0개였습니다. 위 pre-1.6.0-revert 도 푸시와 머지가 같은 날 둘 다 찍혀서, 머지 검사만 넣었어도 뒤집혔을 겁니다. 그러니까 이 절의 발견을 준 것은 푸시였지만, 실제로 수치를 움직인 것은 머지 검사입니다.

여기 작은 함정이 하나 더 있었습니다. pushed도중에 추가된 필드라 옛 판에는 undefined 입니다. "값이 바뀌었나" 로 세면 필드가 생긴 날 전 이력이 통째로 가짜 활동이 되어 오탐률이 조용히 좋아집니다. 그래서 "값이 바뀌었나" 가 아니라 "진전 방향으로 바뀌었나" 로 봅니다. undefined → true 는 진전, undefined → false 는 아무것도 아닙니다.

전체 오탐률이 60.9% 에서 38.4% 로 내려갔습니다.

함정 ④ 안 울린 알림까지 분모에 넣고 있었습니다

여기까지도 분모가 틀려 있었습니다. 구간의 모든 판을 알림으로 셌거든요.

실제 훅에는 조건이 하나 더 붙습니다. "카드가 없거나, 카드가 커밋보다 낡았을 때" 만 울립니다. 카드가 최신이면 작업이 미완이어도 조용합니다. 그런데 제가 코드 주석에 이렇게 적어 두고 포기해 놨더군요.

그 조건은 과거 카드 상태를 복원해야 재는데 스냅샷에 없다. 그래서 실제 오탐 건수는 이보다 적다.

카드도 git 에 있습니다. 알림을 내는 함수는 (스냅샷, 카드) 의 순수 함수이고 둘 다 매번 커밋되니, 그날의 알림이 그대로 재계산됩니다. 못 잰다고 적어 둔 게 틀렸던 겁니다.

무거울 줄 알았는데 아니었습니다. 258판 × 카드 72장이면 1만 8천 번 파싱 같지만, 고유 blob 은 406개뿐입니다. 카드는 점증적으로 바뀌니까요. blob 단위로 캐시하면 전체 7초입니다.

검증은 그 사건 자체로 했습니다. 2026-08-24 트리로 재계산하니:

onreorderitem-migration · pr-8 · pre-1.6.0-revert-20260824
ssh-timeout-test-unhandled-rejection · storybook-10-vite-8

주석이 계기로 적어 둔 "카드가 밀린 작업 5건" 과 정확히 일치했습니다.

한 가지 한계는 남습니다. 실제 훅은 세션 소속(회사 세션이냐 개인 세션이냐)으로 한 번 더 거르는데, 그 판정에 쓰이는 값이 이력에 안 남습니다. 그래서 302 는 두 소속을 합친 수이고, 한 세션이 실제로 본 알림보다는 많습니다.

결과는 반대 방향이었습니다.

항목 교정 전 교정 후
울린 판(분모) 682 302
오탐률 38.4% 63.9%
판정 불가 1907 179

올라갔습니다. 안 울린 판을 분모에서 빼니, 실제로 울린 알림들이 유독 헛것이었다는 게 드러난 겁니다. 그리고 판정 불가가 1907 에서 179 로 줄었습니다 — 몇 달씩 열려 있는 장수 브랜치들은 카드가 최신이라 애초에 안 울리고 있었습니다. 제가 "이것들 때문에 못 잰다" 고 적어 뒀던 그 1907판의 대부분이 처음부터 측정 대상이 아니었습니다.

고치니까 새 결함이 생겼습니다

분모를 실제 발화로 교정하고 나니 한 번도 안 울린 구간이 생겼습니다. 카드가 내내 최신이라 조용했던 작업들이죠. 그걸 그대로 세면 울린 적 없는 알림의 오탐률을 재게 됩니다.

발견은 우연이 아니었습니다. 출력에 두 단위를 같이 내고 있었거든요.

교체 후   판 0/40  0.0%     구간 1/3  33.3%

판으로는 0인데 구간으로는 1입니다. 한 구간이 "거짓 알림 구간" 으로 세어지면서 알림은 0건인 모순이죠. 두 단위를 같이 내지 않았으면 못 봤습니다.

그래서 한 번도 안 울린 구간은 비율에서 뺐습니다. 최종 표의 교체 후가 0/1 인 것이 그 결과입니다 — 위 1/3 에서 안 울린 두 구간이 빠졌습니다.

두 단위를 같이 내게 된 것도 실수 덕분이었습니다. 처음엔 95% 신뢰구간을 에 걸었는데, 한 구간 안의 판은 전부 오탐이거나 전부 아니라서 독립 관측이 아닙니다. 78판이면 [0~7%] 이 나오는데 실제 독립 표본은 구간 3개라 [6~79%] 입니다. 판에 걸면 확신이 과장됩니다.

결과

탐지기 교체(2026-08-25) 전후
코호트      판 기준(소음)     구간 기준(적중)   95% 구간
교체 전       37/83   44.6%    4/13   30.8%  [13~58%]
경계 걸침    156/179   87.2%    4/5    80.0%  [38~96%]
교체 후        0/40    0.0%     0/1     0.0%  [0~79%]

작업 소속별
회사      울린 판 123   오탐  37   30.1%
개인      울린 판 179   오탐 156   87.2%

진짜 답은 "알림이 시끄럽다" 가 아니었습니다. 소음은 개인 레포에 몰려 있었습니다 (개인 87.2% vs 회사 30.1%). 그리고 경계 걸침 다섯 건이 전부 개인 작업입니다. 고친 것이 스쿼시 머지 탐지였으니, 개인 쪽이 스쿼시를 더 쓴다고 보는 게 자연스럽습니다.

다만 거기까지가 측정된 것이고, 그 뒤는 아닙니다. 저는 레포별 머지 설정을 세어 보지 않았습니다. 제 함정 기록에 개인 작업 하나가 스쿼시, 회사 작업 하나가 리베이스로 남아 있을 뿐이고, 그건 각각 한 건씩입니다. "회사는 스쿼시를 안 쓴다" 는 제가 확인한 사실이 아니라 그럴듯한 이야기입니다. 이 글의 요지를 생각하면 그 구분을 흐리면 안 되겠죠.

그리고 지표는 이제 "개선됐다" 고 말하지 못합니다. 교체 전 [13~58%] 와 교체 후 [0~79%] 가 겹치거든요. 교체 후 닫힌 구간이 1개라 그렇습니다. 개연성은 높지만 입증은 안 됩니다. 그렇게 말하게 만든 것이 이번 작업의 핵심입니다.

남는 것

못 고친 게 하나 있습니다.

알림을 보고 "아 저거 죽은 브랜치지" 하고 아무것도 안 움직인 채로 작업을 닫으면, 알림이 제 역할을 완벽히 했는데도 판정식은 "아무것도 안 움직이고 닫힘 = 오탐" 으로 셉니다. 반대로 알림을 계속 무시하다가 두 주 뒤 전혀 다른 이유로 커밋 하나 찍고 닫으면 "정상 알림" 이 됩니다.

진전을 사후 라벨의 대리 지표로 쓰는 한 구조적으로 남습니다. 푸시·머지로 반응한 경우는 함정 ③ 에서 닫았지만, 정리만 한 경우는 사람이 라벨을 달아야 갈립니다. 그건 이 도구가 처음에 기각한 바로 그 방향이고요.

제일 오래 남은 건 다른 겁니다. 네 번 다 "이제 맞다" 고 생각한 뒤에 틀린 게 드러났습니다. 그리고 네 번 다 힌트가 출력 안에 이미 있었습니다 — 판정 불가가 늘어나는 것, 경계에 걸친 다섯 건이 같은 날 닫힌 것, 주석의 "4건" 과 지표의 "5건" 이 어긋난 것, 판 단위와 구간 단위가 다른 답을 낸 것.

숫자 하나만 보면 안 보입니다. 같이 낸 다른 숫자가 그걸 잡아 줍니다. 그래서 비율을 낼 때 분모를 같이 내고, 판정 불가를 숨기지 않고, 단위가 둘이면 둘 다 냅니다. 귀찮아서 하나로 줄이고 싶을 때마다, 오늘 네 번을 떠올릴 겁니다.