문서 목차
이 페이지에서목차

dev-machine-disk-hygiene

개발 장비나 자체 운영 러너의 디스크 여유가 부족할 때 사용합니다. 사용 중인 파일과 오래된 임시 파일·캐시를 구분하고 실행 중인 작업을 확인한 뒤 정리합니다.

한마디로#

사용 중인 파일과 오래된 임시 파일·캐시를 구분하고 실행 중인 작업을 확인한 뒤 정리합니다.

무엇을·언제#

⚠️ 먼저 어느 파일시스템인지 가르세요#

"디스크가 없다" 는 증상은 같아도 대상이 둘입니다. 섞으면 엉뚱한 곳을 정리합니다.

대상증상가드
컨테이너 런타임 게스트 (Colima/Docker VM) 컨테이너가 No space left on device 로 죽는다 ensure_docker_ready.sh 류의 임계 기반 prune 사다리
호스트 파일시스템 df 여유가 없다, 빌드·테스트가 전반적으로 실패 이 스킬

게스트 가드가 있다고 호스트가 안전한 것이 아닙니다 — 이번 실사고가 정확히 그 경우였습니다.

상황이 스킬이 주는 것
디스크 여유가 급감했다어디부터 재야 하는가 (홈이 아니다)
캐시를 지워도 되나실행 중인 CI 를 죽이지 않는 2중 판정 기준
SDK 버전을 bump 했다직전 버전 사본을 회수하는 절차
정리를 자동화하고 싶다launchd 스크립트와 안전 기준

실사고 — 994GB 볼륨에서 여유 6.1GB (2026-08-24)#

정리로 회수한 415GB 의 내역이 원인 구조를 그대로 보여줍니다. 러너 8개 환경입니다.

축회수만든 주체
$TMPDIR 고아 임시 디렉터리 137.4 GB 비정상 종료한 dart test
러너별 구버전 Flutter SDK 40개 157.9 GB 워밍 규칙 의 "한 번 받으면 영구"
러너별 Gradle·pub-cache 사본 86.2 GB 격리 규칙의 인스턴스별 분리
Xcode DerivedData·기기 심볼·구버전 SDK29.0 GB로컬 개발
머지 완료 워크트리4.4 GB방치

⚠️ 어느 항목도 결함이 아닙니다. 격리는 캐시 덮어쓰기 사고를, 워밍의 영구 캐시는 cold 다운로드 타임아웃을 막으려고 도입됐습니다. 각각 옳았고, 그 대가인 사본의 수명을 정한 규칙만 없었습니다. 이 스킬이 그 빈칸입니다.

① $TMPDIR 고아 — 하루 ~33GB#

dart test 가 비정상 종료하면 $TMPDIR/dart_test.kernel.* 가 남습니다. 개당 최대 23GB, 실측 25개 = 165GB(5일치). flutter_tools.* 도 39개 8.9GB.

du -x -k -d 1 "$(getconf DARWIN_USER_TEMP_DIR)" | sort -rn | head

러너 여러 개와 병렬 세션이 상시 테스트를 돌리면 재발은 예외가 아니라 기본값입니다. 정리 자체를 자동화해야 합니다(아래 §자동 정리).

② 러너 사본 — 격리·워밍의 대가#

실행 슬롯이 N개면 사본도 N개입니다. 버전 하나가 남을 때마다 N배로 잠깁니다.

자원실측(러너 8개)삭제하면
_work/_tool/flutter/<버전> 6버전 × 8 = 195GB 재다운로드 — 실패가 아니라 5~25분 지연
$HOME/.gradle-<러너>102GB의존성 재다운로드
$HOME/.pub-cache-<러너>15GBpub get 재수행

🔴 보존 목록은 러너를 공유하는 모든 리포의 합집합이다#

⚠️ 이 스킬의 이전 판은 여기서 틀렸습니다 — "SDK 버전 SSOT(.fvmrc + 워크플로의 FLUTTER_VERSION)를 확인하세요" 라고만 적어 어느 리포의 것인지를 빠뜨렸습니다. 그대로 따르면 사고가 재현됩니다.

러너 fleet 는 대개 여러 리포가 공유합니다.

ls ~/actions/<러너>/_work/
# climb  coui  good-teacher  kobic  open-board  unibook   ← 6개 리포

실측에서 리포마다 현행 버전이 달랐습니다: 두 리포는 3.47.0, 하나는 3.44.8, 둘은 3.41.6. 한 리포(3.47.0) 기준으로만 판정해 나머지 셋의 현행 SDK 를 지웠고, 삭제한 3.41.6 이 같은 날 저녁 러너 2곳에 재생성됐습니다 — 그 리포들의 CI 가 재다운로드한 흔적입니다.

⚠️ 실패는 아닙니다(타임아웃 헤드룸이 cold 다운로드를 흡수). 그러나 예방 가능한 5~25분 지연을 러너 수만큼 유발했고, 원인은 판정 범위를 좁게 잡은 것입니다.

⇒ 보존 목록을 손으로 적지 마세요. 매 실행마다 러너에서 수집해 합집합을 만듭니다.

for fvmrc in "$ACTIONS"/*/_work/*/*/.fvmrc; do
  sed -n 's/.*"flutter"[[:space:]]*:[[:space:]]*"\([0-9][0-9.]*\)".*/\1/p' "$fvmrc"
done >> "$KEEP"
for wf in "$ACTIONS"/*/_work/*/*/.github/workflows/*.y*ml; do
  grep -hoE "(FLUTTER_VERSION|flutter-version):[[:space:]]*[\"']?[0-9]+\.[0-9]+\.[0-9]+" "$wf" \
    | grep -oE "[0-9]+\.[0-9]+\.[0-9]+"
done >> "$KEEP"
sort -u "$KEEP" -o "$KEEP"

# ⭐ 수집 실패 시 폭주 방지 — 이 가드가 없으면 "보존할 게 없으니 전부 삭제" 가 된다
[ -s "$KEEP" ] || { echo "⛔ 보존 버전 0건 — 중단"; exit 1; }

⛔ 로컬 SDK 관리자의 기본 버전은 지우지 마세요. 실측 당시 ~/fvm/default 가 가리키던 버전을 MCP 서버·분석기 등 55개 프로세스가 실행 중이었습니다. 러너 툴캐시와 로컬 SDK 는 별개입니다.

D-1. 삭제 기준은 2중이다 — 하나로는 부족하다#

삭제 대상이 지금 이 순간에도 생기고 사라집니다. 실측 중 한 디렉터리가 조회 사이에 나타났다 없어졌습니다.

① 프로세스·lsof 가 참조하지 않을 것    (스냅샷 — 레이스가 있다)
② N시간 이상 내용이 변경되지 않았을 것  (레이스를 덮는다)

①만으로는 조회와 삭제 사이의 창을 못 막고, ②만으로는 오래 도는 잡을 죽입니다. 테스트 임시 디렉터리는 6시간, 러너 캐시는 72시간이 실측상 안전했습니다.

⚠️ 활성 항목을 지우면 남의 CI 가 죽습니다 — 실측 당시 23GB짜리 하나가 실행 중이었고, 그것이 가장 큰 항목이었습니다. "제일 큰 것부터" 는 위험한 순서입니다.

D-2. du -sh ~/* 로는 원인이 안 보인다#

이번 179GB 는 /private/var/folders/... 에 있었고, 홈의 숨김 디렉터리 (~/.gradle* 126GB, ~/.dartServer 26GB)도 그 글롭에서 빠집니다.

du -x -k -d 1 "$HOME"                          # 숨김 포함
du -x -k -d 1 "$(getconf DARWIN_USER_TEMP_DIR)"

첫 조사에서 ~/* 합계와 df 사용량이 487GB 어긋났는데, 그 차이가 곧 원인이었습니다. ⭐ 합이 안 맞으면 그 차이를 쫓으세요.

D-3. macOS 에 timeout 이 없다 — 전부 조용히 실패한다#

timeout 90 du -sh "$d" 를 17개 디렉터리에 돌렸더니 17개 전부 빈 결과였습니다. "측정했는데 안 나왔다" 로 읽힐 뻔했고, 원인은 timeout 명령 자체의 부재였습니다.

which timeout gtimeout || echo "없음 — 백그라운드 + 파일로 우회"

D-4. zsh 는 단어 분할을 하지 않는다#

TARGETS="758 2971 2979"
for p in $TARGETS; do kill "$p"; done   # ⛔ zsh: 통째로 한 인자 → 전부 실패

출력이 KILL 758 2971 2979 한 줄로 나오고, 이어진 ps -p "$p" 확인도 같은 이유로 오판해 "전부 잔존"이라고 잘못 보고했습니다. set -- 758 2971 2979 + "$@" 로 고칩니다.

D-5. 중단한 스크립트의 자식이 살아남아 다음 판정을 오염시킨다#

pkill -f <스크립트> 는 셸만 죽입니다. 그 자식 find <경로> -exec stat 가 계속 돌았고, 커맨드라인에 그 경로가 있어서 다음 실행의 "사용 중" 검사가 오탐했습니다.

⇒ 중단 후 ps 로 자식까지 확인하고, 판정 함수에서 자기 자신을 제외하세요.

inuse(){ ps -eo command | grep -F "$1" | grep -v grep | grep -qv "$(basename "$0")"; }

D-6. find -exec stat {} \; 는 큰 트리에서 끝나지 않는다#

파일마다 프로세스를 fork 하므로 Gradle 캐시(수십만 파일)에서 사실상 정지합니다.

목적쓸 것
최근 변경 여부만 /usr/bin/find "$d" -mmin -$((H * 60)) -print -quit (첫 발견 즉시 중단)

⛔ 날짜 문자열을 만들어 -newermt 에 넘기지 마세요. -newermt "-6H" 는 macOS find 가 거부하고(2>/dev/null 이면 조용히 '오래됨'이 됩니다), 2026-10-19T08:04:57 처럼 날짜와 시각 사이에 T 를 넣은 형식은 다른 시각으로 읽습니다 — 실측(2026-10-19): 70분 기준이 5분 전에 바뀐 폴더도 '오래됨'으로 판정했고, 6시간 기준은 약 4시간으로 줄었습니다. Claude Code 의 zsh 에서는 find 가 셸 함수(bfs)라 같은 인자를 또 다르게 받으니 스크립트는 /usr/bin/find 를 직접 부르세요.

⛔ 맨 위 폴더 시각(stat -f '%m' "$d")으로 판정하지 마세요. 바로 아래 항목이 생기거나 지워질 때만 바뀌어서, 매일 쓰는 pub 캐시도 '75시간 전'으로 보였습니다(안쪽은 72시간 안에 변경). 그 기준으로 지우면 쓰는 캐시를 지우고 다음 CI 가 다시 받는 왕복이 생깁니다.

D-7. 자식 프로세스는 커맨드라인에 경로가 없을 수 있다#

삭제된 워크트리의 좀비 7개를 정리할 때, DDS 프로세스만 커맨드라인에 경로가 없어 "무관" 으로 분류될 뻔했습니다. 포트와 부모 체인으로 소속을 확정합니다.

lsof -nP -iTCP:<vm-service-port>   # 어느 프로세스에 붙어 있나
ps -p <pid> -o ppid=               # 고아면 ppid=1

그 좀비들은 3일 23시간 살아 있었고 워크트리는 이미 삭제된 뒤였습니다. 디스크는 점유하지 않았지만 DB 포트를 잡고 있었습니다.

워크트리 정리는 디스크 대책이 아니다#

워크트리는 개당 0.5~3.8GB 입니다(실측 14개 합계 29.5GB). 디스크를 위해 지울 대상이 아니며, 정리정돈 목적임을 알고 판단하세요. 그리고:

  • dirty=0 은 유효기간이 있습니다. 조사 때 0이던 워크트리가 40분 뒤 미커밋 239개였습니다 — 그 사이 다른 세션이 새 브랜치로 작업을 시작했습니다. 삭제 직전에 다시 재세요 (parallel-session-collision).
  • orca worktree list 의 parentWorktreeId 를 보세요. 자식이 매달린 워크트리는 남깁니다(실측: 머지 완료된 워크트리에 활성 자식 2개).
  • orca worktree rm 은 미커밋 변경이 있으면 거부합니다 — 이 거부가 마지막 안전망이라 --force 는 위 재확인을 통과한 뒤에만 씁니다.

자동 정리 (launchd)#

수동 정리는 재발을 막지 못합니다(하루 33GB). 위 D-1 기준을 그대로 구현한 스크립트를 주기 실행합니다.

⭐ 팀 맥에는 직접 만들지 말고 coco-de/desktop 의 것을 쓰세요 — macos/disk-cleanup/ 에 TMPDIR · 러너 캐시 · Docker(Colima) 정리 3종과 회귀 테스트가 있고, ./mac-setup.sh --cleanup-only 가 launchd 에 등록합니다(같은 Label 이라 예전 판을 갈아끼웁니다). 아래는 그 핵심 모양입니다.

#!/bin/sh
# $TMPDIR 의 dart_test.kernel.* / flutter_tools.* 고아 회수
# 안전 기준 2중: ① 프로세스·lsof 미참조  ② MIN_AGE_H 시간 동안 안에서 바뀐 파일 없음
T=$(getconf DARWIN_USER_TEMP_DIR); [ -d "$T" ] || exit 0
MIN_AGE_H=${MIN_AGE_H:-6}
# 사용 중 목록은 파일(mktemp)이 아니라 변수에 — 디스크가 꽉 차면 mktemp 가 실패해 정리가 멈춘다
INUSE=$( { ps -Ao command; lsof -n; } 2>/dev/null \
  | grep -oE '(dart_test\.kernel|flutter_tools)\.[A-Za-z0-9_]+' | sort -u )
for d in "$T"/dart_test.kernel.* "$T"/flutter_tools.*; do
  [ -d "$d" ] || continue
  printf '%s\n' "$INUSE" | grep -qxF -- "${d##*/}" && continue
  [ -n "$(/usr/bin/find "$d" -mmin -$((MIN_AGE_H * 60)) -print -quit 2>/dev/null)" ] && continue
  rm -rf "$d"
done

러너 캐시(SDK·Gradle·pub-cache)는 별도 스크립트·별도 주기로 나눕니다 — 임시 디렉터리는 하루 단위로 쌓이지만(실측 33GB/일) 캐시는 주 단위라, 한 스크립트에 묶으면 무거운 쪽이 가벼운 쪽의 주기를 지배합니다. 실측 구성은 임시 6시간 · 러너 캐시 24시간입니다.

~/Library/LaunchAgents/<label>.plist 에 StartInterval 21600(6시간)·RunAtLoad false· LowPriorityIO true 로 등록하고 launchctl bootstrap gui/$(id -u) <plist>. 등록 전에 한 번 손으로 실행해 활성 항목이 보존되는지 확인하세요.

⚠️ 정리 자동화는 다음 조사의 용의자가 됩니다. $TMPDIR 에서 파일이 사라지는 다른 증상(레이스·EDR·러너 정리 등)을 누군가 조사할 때, 이 launchd 가 가장 먼저 의심받습니다. 무엇을·얼마나 오래된 것만 지우는지를 스크립트 헤더와 이슈에 명시해 두면 그 배제가 1분 만에 끝납니다.

체크리스트#

디스크가 급하다고 할 때

  • $TMPDIR 부터 쟀는가 — 홈이 아니다 (D-2)
  • du 합계와 df 사용량의 차이를 확인했는가
  • timeout 이 실제로 있는지 확인했는가 (D-3)

지우기 전

  • 프로세스·lsof 미참조 와 N시간 미변경을 둘 다 봤는가 (D-1)
  • 러너 캐시라면 공유 리포 전체의 현행 버전 합집합을 보존했는가 — 손으로 적지 말고 수집 스크립트의 출력을 볼 것
  • 로컬 SDK 관리자의 기본 버전을 제외했는가
  • 워크트리라면 삭제 직전에 dirty 를 다시 쟀는가

지운 뒤

  • 중단한 정리 스크립트의 자식 프로세스가 남지 않았는가 (D-5)
  • 자동 정리가 등록·동작하는가

관련#

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기