프로젝트 리포지토리 크기가 40GB에 가깝고, Pods와 node_modules, DerivedData를 합치면 소스 코드 본체보다도 큽니다. 로컬에서 Xcode를 열고 프로젝트를 클라우드 Mac의 SMB 공유로 마운트해봤더니, 인덱싱에 1분 넘게 걸리고 브랜치만 전환해도 네트워크가 끊긴 게 아닌가 싶을 정도로 멈춰버립니다. 이는 개발 환경을 클라우드 Mac으로 옮긴 많은 사람들이 처음 맞닥뜨리는 벽입니다. 공유 마운트는 간편해 보이지만, 실제로 써보면 대규모 iOS 프로젝트의 파일 접근 패턴과 네트워크 파일 시스템이 근본적으로 맞지 않는다는 걸 알게 됩니다. 이 글에서는 HireVPS 노드에서 Mutagen을 사용해 로컬과 원격을 양방향으로 동기화하며, 이 경험을 로컬에 가깝게 되돌리는 과정을 정리합니다.
대규모 iOS 프로젝트에 공유 마운트가 적합하지 않은 이유
NFS, SMB 같은 네트워크 파일 시스템은 "가끔 접근하는 큰 파일"을 전제로 설계되어 있습니다. 그런데 Xcode 인덱싱, SourceKit, CocoaPods 캐시 스캔이 하는 일은 정확히 그 반대로, 짧은 시간 안에 수만 개의 작은 파일에 stat, open, close를 반복하는 것입니다. 매 작업마다 네트워크 왕복이 발생하고, 지연 시간이 20ms에 불과하더라도 이것이 수만 번 누적되면 분 단위의 멈춤으로 이어집니다. 더 골치 아픈 건 연결이 끊겼을 때의 처리입니다. 마운트 포인트는 연결이 불안정해지면 즉시 먹통이 되어 터미널과 에디터가 동시에 멈추고, 결국 강제로 언마운트한 뒤 다시 마운트하는 수밖에 없습니다.
흔히 쓰이는 세 가지 방식을 비교해보면 차이가 뚜렷해집니다.
| 방식 | 실시간성 | 대량의 작은 파일 처리 | 끊김 복구 | 적합한 시나리오 |
|---|---|---|---|---|
| SMB/NFS 마운트 | 강함(보이는 즉시 최신) | 나쁨, 파일 단위로 네트워크 왕복 | 나쁨, 먹통이 되기 쉬움 | 큰 파일을 가끔 읽고 쓰는 경우 |
| rsync 정기 작업 | 약함, 동기화 간격 존재 | 보통, 일괄 전송이라 효율적 | 좋음, 작업 단위로 재실행하면 됨 | 정기 백업, 단방향 푸시 |
| Mutagen 양방향 동기화 | 실시간에 가까움 | 좋음, 로컬 캐시+증분 전송 | 좋음, 끊기면 자동 재연결·재전송 | 지속적인 개발에서의 양방향 협업 |
저희가 겪었던 교훈 하나는, 처음에는 편하다는 이유로 그냥 마운트를 걸었다는 것입니다. 그 결과 Xcode의 "Building workspace" 단계에서만 평균 15~25초씩 더 기다려야 했고, 팀 전체가 인덱싱 대기로 매일 상당한 시간을 낭비했습니다. Mutagen으로 바꾼 뒤로는 이 대기 시간이 거의 사라졌습니다.
Mutagen으로 양방향 동기화 세션 구성하기
설치와 최초 연결
Mutagen 자체는 단일 바이너리로, 로컬과 클라우드 Mac 어느 쪽에도 별도의 상주 서비스가 필요 없습니다. 연결이 맺어질 때 원격 쪽에 agent가 자동으로 배포됩니다. 먼저 로컬에 설치합니다.
brew install mutagen-io/mutagen/mutagen
mutagen version
클라우드 Mac 쪽에 SSH가 이미 열려 있다고 가정하면(자격 증명은 개통 안내 메일에 있습니다), 동기화 세션을 만듭니다.
mutagen sync create \
--name=ios-app \
--ignore-vcs \
--symlink-mode=posix-raw \
/Users/me/Projects/ios-app \
ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app
--ignore-vcs를 붙이면 .git 내부 오브젝트를 자동으로 건너뜁니다(git 자체는 고유 프로토콜로 동기화하는 게 더 적합하며, Mutagen이 중복으로 옮길 필요는 없습니다). --symlink-mode=posix-raw는 CocoaPods에서 흔히 쓰이는 심볼릭 링크를 그대로 유지하고 변환하지 않도록 해줍니다.
제외 규칙: 절대 동기화하면 안 되는 디렉터리
프로젝트 루트에는 반드시 .mutagenignore를 만들어야 합니다. 그렇지 않으면 첫 동기화만으로도 몇 GB짜리 캐시 디렉터리까지 함께 전송됩니다.
DerivedData/
Pods/
.build/
node_modules/
*.xcuserstate
xcuserdata/
.DS_Store
이 디렉터리들의 공통점은 "어느 쪽에서든 로컬에서 다시 생성할 수 있다"는 것입니다. 동기화하면 전송량만 낭비되는 게 아니라, 다음 절에서 설명할 문제까지 일으킬 수 있습니다.
Xcode 관련 주의사항
- DerivedData는 절대 동기화하지 않는다: 안에 있는 모듈 캐시(ModuleCache.noindex)와 빌드 인덱스는 해당 머신의 절대 경로와 아키텍처에 묶여 있습니다. 다른 머신으로 동기화하면 거의 확실하게 전체 재인덱싱이 강제로 트리거되고, 심하면 이해할 수 없는 컴파일 오류까지 발생합니다. 로컬과 클라우드가 각자 독립적으로 생성하게 두면 되고, 어차피 버려도 되는 산출물입니다.
- Pods와 node_modules는 각 쪽에서 따로 설치: 동기화해야 할 것은 소스 코드 안의
Podfile.lock,package-lock.json이며, 의존성 디렉터리 자체는 양쪽에서 각각pod install,npm install을 실행합니다. 이렇게 하면 전송량도 절약되고, 바이너리 산출물이 아키텍처 간에 호환되지 않는 문제도 피할 수 있습니다. .gitignore와.mutagenignore를 일치시키기: 두 제외 목록이 서로 어긋나면, git에는 "변경 없음"으로 보이는데 Mutagen은 뒤에서 의미 없는 파일들을 계속 전송하는 상황이 벌어져 원인을 찾는 데 시간을 낭비하게 됩니다. 변경이 있을 때마다 두 목록의 차이를 비교하는 작은 스크립트를 만들어두는 걸 추천합니다.- 권한 비트 차이: 클라우드 Mac과 로컬 계정의 UID가 다릅니다. 권한 오류가 나면 먼저 실행 비트가 동기화 과정에서 빠지지 않았는지 확인하세요.
--permissions-mode=portable을 쓰면 대부분의 경우 완화할 수 있습니다.
실측 성능과 충돌 처리
일상적인 사용에서는 저장 한 번으로 트리거되는 증분 동기화(Swift 파일 몇 개 수정)가 대체로 1~2초 안에 끝나서, 체감상 로컬 저장과 큰 차이가 없습니다. 반면 첫 전체 동기화(수만 개의 소스 파일)는 디렉터리 트리를 스캔해야 해서 눈에 띄게 느려지므로, 작업을 시작하기 전에 한 번 실행해서 안정화시켜두고, 코드를 쓰면서 첫 동기화를 기다리는 식으로 쓰지 않는 게 좋습니다.
충돌 처리에서, 기본 양방향 모드인 two-way-safe는 충돌이 발생하면 자동으로 덮어쓰지 않고 멈춰서 사용자의 처리를 기다립니다. 강제 덮어쓰기 방식보다는 안전하지만, 양쪽에서 동시에 같은 파일을 수정하면 자주 멈추게 됩니다. 팀 협업 상황에서는 다음 설정을 더 추천합니다.
mutagen sync create \
--name=ios-app \
--sync-mode=two-way-resolved \
--default-file-mode-alpha=0644 \
/Users/me/Projects/ios-app \
ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app
two-way-resolved는 충돌 시 기본적으로 로컬 쪽(alpha)이 우선하도록 처리합니다. "같은 시간에는 한 사람만 한쪽에서 코드를 수정한다"는 규칙과 함께 쓰면 변경 사항이 사라지는 일은 거의 없습니다. 실제로 이미지, 폰트 같은 바이너리 리소스에서 충돌이 발생하면 mutagen sync list로 충돌 경로 목록을 확인할 수 있으며, 타임스탬프와 파일 크기를 비교해서 어느 쪽을 남길지 수동으로 결정합니다.
그대로 쓸 수 있는 설정 예시
Mutagen은 프로젝트 단위 설정 파일을 지원합니다. 프로젝트 루트에 mutagen.yml을 두면, 팀원이 리포지토리를 클론한 뒤 명령어 한 줄로 동기화를 시작할 수 있어서 각자 파라미터를 일일이 입력할 필요가 없습니다.
sync:
ios-app:
alpha: "."
beta: "ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app"
mode: "two-way-resolved"
ignore:
vcs: true
paths:
- "DerivedData"
- "Pods"
- "node_modules"
- "xcuserdata"
- ".build"
symlink:
mode: "posix-raw"
자주 쓰는 명령어는 이 정도만 기억해두면 충분합니다.
mutagen sync list
mutagen sync monitor ios-app
mutagen sync pause ios-app
mutagen sync resume ios-app
mutagen sync terminate ios-app
monitor는 "왜 변경 사항이 동기화되지 않았는지"를 확인할 때 특히 유용합니다. 스캔, 스테이징, 전송 세 단계 중 어디서 막혀 있는지를 실시간으로 볼 수 있습니다.
도입 전 체크리스트
.mutagenignore와.gitignore가 DerivedData, Pods, node_modules, xcuserdata 등 재생성 가능한 디렉터리를 모두 포함하고 있나요?- 동기화 모드로
two-way-safe와two-way-resolved중 무엇을 선택했나요? 팀 협업 상황이라면 후자를 우선하고, 누구의 변경을 우선할지 미리 약속해두세요. - 첫 전체 동기화는 급한 작업이 없는 시간대에 진행했나요? 수만 개 파일의 초기 스캔은 코드를 쓰면서 기다리기에 적합하지 않습니다.
- 클라우드 Mac의 구체적인 모델과 노드는 콘솔에서 현재 선택 가능한 구성인지 확인했나요? 대역폭과 디스크 사양이 동기화 속도의 상한을 좌우합니다.
mutagen sync monitor를 확인하는 습관이 이미 자리 잡았나요, 아니면 파일이 사라진 뒤에야 로그를 들여다보게 되나요?
이 항목들을 한 번씩 점검하면 클라우드 Mac에서의 편집 경험이 로컬에 거의 가까워지고, 팀 협업 시에도 "누구의 변경이 동기화되지 않았는지"를 두고 서로 책임을 미루는 일이 줄어들 것입니다.
자주 묻는 질문
SMB나 NFS로 마운트하면 되지 않나요?
네트워크 마운트는 작은 파일 접근(Xcode 인덱싱, Pods 조회)마다 왕복 통신이 발생해 대형 프로젝트는 열기만 해도 수십 초가 걸릴 수 있습니다. Mutagen은 로컬 캐시를 유지하며 증분 동기화하기 때문에 편집 반응성이 로컬과 거의 같고, 연결이 끊겨도 파일시스템이 멈추지 않습니다.
DerivedData도 동기화해야 하나요?
아니요. DerivedData의 모듈 캐시와 인덱스는 머신 아키텍처와 절대 경로에 묶여 있어서 동기화하면 재인덱싱이나 빌드 오류가 자주 발생합니다. .mutagenignore에 추가하고 각 머신이 개별적으로 생성하게 두세요.
양방향 동기화 중 충돌이 나면?
mutagen sync list로 충돌 경로를 확인하세요. 소스 코드 충돌은 대부분 two-way-resolved 모드에서 로컬(alpha) 쪽을 우선시하면 해결됩니다. 바이너리 자산은 수동으로 diff해서 판단하고, 동기화 시작 전에 git status로 작업 트리를 정리하는 습관이 충돌을 크게 줄여줍니다.
HireVPS Mac
지금 전용 클라우드 Mac mini를 이용해 보세요
일 단위 대여로 시작하고, 2분 안에 SSH/VNC 접속 정보를 받아 언제든지 사양을 업그레이드할 수 있습니다.