작업 머신을 윈도우 랩탑의 WSL2에서 맥으로 옮겼다. 이사 자체는 tar 몇 개면 끝나는 일이라 대수롭지 않게 봤다.
문제는 점검 순서였다. 처음엔 du -sh ~/*를 찍어놓고 큰 것부터 챙기고 있었다. 그 목록의 1등은 5.7GB짜리 생성물 폴더였고, 정작 잃으면 복구가 불가능한 것들은 전부 수백 MB 밑에 깔려 있었다.
이사 점검의 질문은 “무엇이 있나”가 아니라 “무엇이 여기에만 있나”다. 그걸 늦게 깨달아서 하루를 더 썼다.
1. 리포 25개 중 21개는 애초에 위험하지 않았다
원격이 있는 리포는 tar가 통째로 깨져도 맥에서 clone하면 그만이다. 그래서 먼저 “원격이 없거나, 로컬에만 커밋이 남은” 것을 찾았다.
cd ~/projects && for d in */; do d="${d%/}"
git -C "$d" rev-parse --git-dir >/dev/null 2>&1 || { echo "[git아님] $d"; continue; }
dirty=$(git -C "$d" status --porcelain | wc -l)
up=$(git -C "$d" rev-parse --abbrev-ref '@{u}' 2>/dev/null)
ahead=$([ -n "$up" ] && git -C "$d" rev-list --count '@{u}'..HEAD || echo NOUP)
echo "$d dirty=$dirty ahead=$ahead $(git -C "$d" remote get-url origin 2>/dev/null || echo '원격없음')"
done
25개 중 22개를 커밋·푸시했고, git으로는 복구가 안 되는 항목이 4종 나왔다.
| 종류 | 실제로 걸린 것 | 처리 |
|---|---|---|
| 원격 없음 | 리포 2개 (각 27커밋, 74커밋) | private 리포 신설 후 push |
| 앱 전체 미커밋 | 리포 2개 (각 89개 변경, 모델·컨트롤러·뷰 통째) | wip/pre-mac-migration 브랜치로 스냅샷 |
| git 아님 | 산출물 폴더 1.1GB, 빈 폴더 1개 | tar로만 보존 |
| 남의 리포 | 3개 (로컬 수정만 존재) | push하지 않음 |
미커밋 89개를 main에 바로 넣지 않은 건 취향 문제가 아니다. 검토 없이 밀어 넣으면 “이사 때문에 커밋한 것”과 “완성된 것”이 히스토리에서 구분되지 않는다. WIP 브랜치가 그 구분을 남긴다.
2. 복사는 성공하는데 안 열리는 것들
여기서부터가 진짜였다. 아래 셋은 tar도 성공하고 에러도 안 나는데 맥에서 못 쓰게 된다.
git 워크트리 — .git이 디렉터리가 아니다
워크트리로 만든 폴더는 .git이 gitdir: /home/<user>/projects/... 한 줄짜리 파일이다. 본체 쪽 .git/worktrees/<이름>/gitdir도 절대경로다.
경로 치환 sed는 보통 확장자로 대상을 고르고 --exclude-dir=.git을 붙인다. 둘 다 안 걸린다. 맥에서 그 폴더는 통째로 git 리포가 아니게 되고, 에러 대신 “not a git repository”가 나온다.
# 찾는 법
find ~/projects -maxdepth 2 -name .git -type f
# import에 추가한 것
sed -i '' "s|/home/<user>|$HOME|g" "$d/.git"
git -C "$d" worktree repair
심볼릭 링크 — tar는 링크를 링크로 담는다
~/.claude/skills 19개 중 10개가 심볼릭 링크였다. 절반이 다른 경로의 실체를 가리키고 있었고, 그중 6개는 export 대상 밖이었다.
tar czf는 링크를 링크로 담으므로 맥에서 전부 0바이트 껍데기가 된다. 그리고 절대경로 링크는 경로 치환 sed로 못 고친다 — 링크 대상은 파일 내용이 아니라 메타데이터다.
# export: -h 로 실체를 떠서 따로 담는다
tar czhf migrate-claude-skills.tar.gz -C "$HOME" .claude/skills
# import: 링크를 먼저 지워야 덮인다
find ~/.claude/skills -maxdepth 1 -type l -delete
tar xzf migrate-claude-skills.tar.gz -C "$HOME"
gitignore된 시크릿 — 빠져도 아무 신호가 없다
.env, master.key, *.pem은 정의상 git으로 안 넘어온다. 그래서 하나 빠져도 리포는 깨끗하고 테스트도 통과한다. 실행할 때가 되어서야 안다.
방법은 아카이브 목록과 맥 실측을 1:1로 대조하는 것뿐이었다.
tar tzf mac-transfer.tar.gz | grep 'projects-secrets/' | grep -v '/$' \
| sed 's|mac-transfer/projects-secrets/||' \
| while read f; do [ -f "$HOME/projects/$f" ] && echo "OK $f" || echo "MISS $f"; done
32개 중 1건이 MISS로 나왔다. 외부 API 토큰과 리소스 ID가 든 파일이었다.
그리고 파일이 있어도 내용이 맞는지는 별개다. 같은 날 한 배치 스크립트의 .env는 멀쩡히 존재했는데 값이 샌드박스 토큰이라 호출이 전부 401이었다.
3. 맥에 도착한 뒤 — 리눅스에서만 참이던 가정들
grep -oP는 cron에서만 죽는다
주간 리포트 배치가 .env에서 토큰을 이렇게 뽑고 있었다.
TOKEN="$(grep -oP '(?<=^ACCESS_TOKEN=).*' "$ENVFILE" | tr -d '\r')"
-P(PCRE)는 GNU grep 전용이다. macOS의 /usr/bin/grep은 BSD라 invalid option -- P로 죽고 $(...)가 빈 문자열이 된다. 아래 [ -n ... ] 가드가 잡아 중단시키긴 하는데, cron에서는 그 메시지를 아무도 안 본다. 주간 리포트가 조용히 안 나갈 뻔했다.
TOKEN="$(sed -n 's/^ACCESS_TOKEN=//p' "$ENVFILE" | tr -d '\r')" # BSD/GNU 동일
크롬이 깔려 있는데 “없다”고 한다
PDF 생성 경로가 google-chrome·chromium·chromium-browser를 PATH에서 찾고 있었다. macOS는 브라우저가 앱 번들이라 그 이름이 PATH에 없다. 크롬이 멀쩡히 설치돼 있는데도 “PDF 생성기 없음”으로 떨어져 HTML만 나갔다.
CHROME_BINS = %w[google-chrome-stable google-chrome chromium chromium-browser].freeze
# macOS 는 앱 번들 안에 있어 PATH 에 안 잡힌다 — 절대경로로 한 번 더 본다.
CHROME_APP_PATHS = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium"
].freeze
def self.chrome_bin
CHROME_BINS.find { |b| system("command -v #{b} > /dev/null 2>&1") } ||
CHROME_APP_PATHS.find { |p| File.executable?(p) }
end
시스템 파이썬이 3.9였다
매일 04:00에 도는 launchd 잡이 첫 실행부터 SyntaxError로 죽었다(launchctl list 종료코드 1). 원인은 f-string 표현식 안의 백슬래시다.
# 3.12+ (PEP 701) 에선 되지만 3.9 에선 파싱 자체가 안 된다
f"{f'<a href=\"{edit}\">' if edit else ''}"
# 밖으로 뺀다
edit_html = f'<p>편집/검수: <a href="{edit}">{edit}</a></p>' if edit else ""
venv를 시스템 파이썬(3.9.6) 위에 만들어서 생긴 일이다. WSL에서는 잘 돌던 코드다.
한 Rails 앱은 테스트 34개가 깨졌는데, 원인은 네이티브 도구 두 개(pdftotext, chrome)였다. 재설치 후 1278 runs, 0 failures로 복구. 덤으로 환경과 무관한 버그 하나도 같이 나왔다 — 3.days.ago로 데이터를 잡아놓고 시계를 고정 날짜로 travel_to하는 테스트라, 실제 날짜가 그 지점을 지나면 어차피 깨질 것이었다. 이사가 하루 일찍 꺼내줬을 뿐이다.
4. 제일 아프게 배운 것 두 가지
하나. “내가 돌려보니 되던데”가 거짓일 수 있다.
grep -oP가 맥에서 되는지 확인하려고 셸에서 돌려봤더니 잘 됐다. 알고 보니 그 셸은 grep이 PATH 바이너리가 아니라 다른 구현으로 풀리고 있었다. 로그인 셸과 cron은 그렇지 않다.
/bin/zsh -l -c 'type grep' # 사용자 로그인 셸에서 실제로 뭐가 잡히나
env -i PATH=/usr/bin:/bin /bin/bash -c "grep -oP 'x' /dev/null"
# → grep: invalid option -- P
명령 가용성은 대화형 셸이 아니라 실제 실행 환경(스케줄러 PATH)에서 확인해야 한다. WSL에서 쓰던 스크립트는 grep -P, sed -i 인자, date -d, readlink -f, xargs -r, stat -c가 전부 다르다.
둘. 명령이 되는지 시크릿 파일로 시험하지 마라.
바로 그 확인을 하면서 대상 파일로 실제 .env를 넣었다. 프로덕션 토큰이 세션에 평문으로 그대로 찍혔다. 그 전까지는 값을 가리며 작업하다 마지막 확인에서 놓쳤고, 결국 토큰 로테이션까지 갔다.
동작 확인은 /dev/null이나 더미 파일로 한다. 꼭 실제 파일을 써야 하면 | sed -E 's/=.*/=<가려짐>/'를 항상 파이프로 붙인다.
한계와 다음 계획
- cron은 아직 안 옮겼다. 랩탑 cron 5개가 리눅스 절대경로로 박혀 있다. import가
/tmp/crontab.new를 만들어주지만 자동 설치는 안 한다 — 맥이 슬립 중이면 cron이 안 도는 문제가 따로 있어서 launchd로 갈지 판단이 남았다. - 홈 폴더 일부는 의도적으로 안 옮겼다. 생성물 5.7GB·2.0GB, 툴체인 503MB, 기록 91MB. 툴체인은 재설치하면 되고 생성물은 용량 대비 가치를 사람이 판단할 일이지만, 목록에 없으면 영원히 사라지는 건 사실이라
MIGRATE-NOTES.md에 “이 스크립트가 옮기지 않는 것” 절을 따로 뒀다. - 검증이 아직 사후적이다. 지금은 깨진 걸 발견하면 고치는 식이다. 스케줄러 환경(
env -i PATH=...)으로 배치 스크립트를 한 번씩 돌려보는 스모크 체크를 붙이는 게 다음 순서다.
이사에서 배운 한 줄만 남기면 이렇다. 위험은 용량이 아니라 “여기에만 있음”에 비례한다. 그리고 그 위험은 에러 메시지로 오지 않는다.
자주 묻는 질문
Q. 리포는 전부 원격에 있는데 왜 따로 점검하나요?
원격이 있는 리포는 실제로 안전하다. 문제는 “전부 있다”는 게 확인 없이는 믿을 수 없는 가정이라는 점이다. 25개를 훑어 원격 없는 리포 2개, 앱 전체가 미커밋인 리포 2개가 나왔다. 점검 비용은 위 루프 한 번이고, 안 하면 27커밋과 74커밋이 통째로 사라진다.
Q. 워크트리와 심볼릭 링크는 왜 경로 치환으로 안 고쳐지나요?
둘 다 “파일 내용”이 아니기 때문이다. 워크트리의 .git은 확장자가 없는 파일이라 확장자 필터에 안 걸리고, --exclude-dir=.git을 붙이면 본체 쪽 gitdir도 제외된다. 심볼릭 링크는 아예 대상 경로가 메타데이터라 sed가 볼 내용 자체가 없다. 링크는 tar czhf로 실체를 뜨고, 워크트리는 git worktree repair를 쓴다.
Q. WSL 스크립트를 맥으로 옮길 때 가장 먼저 볼 것은?
GNU 전용 옵션이다. grep -P, sed -i(맥은 백업 접미사 인자가 필수), date -d, readlink -f, xargs -r, stat -c가 BSD 유저랜드에서 다르거나 없다. 그리고 확인은 대화형 셸이 아니라 env -i PATH=/usr/bin:/bin으로 스케줄러 환경을 흉내내서 한다.
Q. 시크릿이 빠진 걸 어떻게 알아채나요?
gitignore된 파일은 누락 신호가 없다. 아카이브 목록과 대상 머신의 파일 존재를 1:1로 대조하는 게 실질적으로 유일한 방법이다. 32개 중 1건을 그렇게 찾았다. 다만 존재 확인은 절반이고, 값이 맞는지는 실제 호출로 한 번 확인해야 한다 — 파일은 있는데 샌드박스 토큰이라 전부 401인 경우가 실제로 있었다.