맥에는 RaiDrive가 없어서 직접 만들었다 — DriveBridge 하루 개발기
SMB와 WebDAV를 Finder 드라이브로 붙이는 macOS 메뉴바 앱을 하루 만에 만들었다. FUSE 없이 macOS 기본 NetFS만 쓰고, 암호는 Keychain에만 두고, Apple 공증까지 마쳐 다른 맥에서도 경고 없이 열린다.
맥에는 RaiDrive가 없다.
윈도우에서는 NAS든 WebDAV 서버든 RaiDrive로 드라이브 문자 하나를 붙여두면 그걸로 끝이다. 탐색기를 열면 Z:가 늘 거기 있고, 재부팅해도 다시 붙는다. 맥으로 넘어오면 그 감각이 사라진다. Finder의 서버에 연결(⌘K)을 열어 주소를 다시 치고, 계정과 암호를 다시 넣고, 로그아웃하면 또 사라진다. 자주 쓰는 서버가 두세 개면 이 왕복이 하루에 몇 번씩 생긴다.
App Store에도 대안이 있긴 하다. 다만 대부분 유료이거나, FUSE 같은 시스템 확장을 깔아야 하거나, 필요 없는 클라우드 서비스까지 한 뭉치로 들어온다. 내가 필요한 건 "저장해둔 서버 목록을 메뉴바에서 껐다 켜는 것" 하나였다. 그래서 하루를 들여 직접 만들었다.
DriveBridge — SMB와 WebDAV를 Finder 네트워크 드라이브로 붙여두고 메뉴바에서 관리하는 무료 네이티브 macOS 앱이다.

첫 결정: FUSE도, 커널 확장도 쓰지 않는다
이 앱의 성격을 결정한 건 기능 목록이 아니라 이 한 줄이었다.
macOS에서 원격 저장소를 드라이브처럼 붙이는 앱들은 보통 macFUSE나 File Provider 확장을 얹는다. 그러면 임의의 프로토콜을 파일시스템으로 둔갑시킬 수 있어 자유도가 높다. 대신 대가가 있다. 사용자가 별도 설치를 해야 하고, 시스템 보안 설정에서 확장을 허용해야 하고, macOS가 큰 업데이트를 하면 깨진다. "무료로 만들어 남에게 주고 싶은 앱"에는 이 비용이 너무 크다.
그래서 macOS가 이미 갖고 있는 것을 쓰기로 했다. NetFS 프레임워크 — Finder의 '서버에 연결'이 내부에서 부르는 바로 그 API다.
let status = NetFSMountURLSync(
url as CFURL, nil, username, secret,
openOptions, mountOptions, &unmanagedPoints
)
한 번 호출하면 OS가 알아서 마운트하고, 성공 시 마운트 지점 경로 배열을 돌려준다. 결과적으로 DriveBridge는 파일시스템을 구현하지 않는다. macOS가 원래 할 줄 아는 일을 대신 기억해두고 대신 눌러주는 앱이다. 설치는 .app을 응용 프로그램 폴더에 끌어다 놓는 것으로 끝난다.
경계도 여기서 자동으로 정해졌다. NetFS가 말할 줄 아는 프로토콜만 지원한다 — SMB와 WebDAV. Google Drive·OneDrive·Dropbox처럼 전용 OAuth API를 통해야 하는 서비스와 SFTP는 이 구조로 붙일 수 없어 이번 버전에서 뺐다. 할 수 있는데 안 한 게 아니라, 설계상 못 하는 영역을 처음부터 잘라낸 것이다.
SMB와 WebDAV는 어떻게 다른가
둘 다 "원격 폴더를 로컬 폴더처럼 쓴다"는 목적은 같지만 성격이 다르다.
- SMB — 원래 랜 안에서 파일을 공유하려고 만든 프로토콜이다. 파일 잠금·부분 읽기 같은 파일시스템 동작을 세밀하게 지원해서, 큰 파일을 열고 편집하는 작업이 빠르고 자연스럽다. 대신 인터넷 너머로 그대로 노출하기엔 위험해서, 보통 같은 내부망이나 VPN 안에서 쓴다. 집·사무실 NAS라면 SMB가 맞다.
- WebDAV — HTTP 위에 파일 조작 기능을 얹은 규격이다. HTTPS 443 포트 하나로 끝나니 방화벽을 거의 건드리지 않고, 인증서만 제대로면 외부에서 바로 붙는다. 대신 파일 하나를 다루는 데도 HTTP 요청이 오가서 SMB만큼 즉각적이진 않다. 밖에서 접속해야 하는 Nextcloud·ownCloud류라면 WebDAV가 편하다.
DriveBridge는 이 둘을 같은 목록에 나란히 두고, 연결마다 방식만 골라두게 했다. 안에서는 NAS, 밖에서는 클라우드 — 실제 사용 패턴이 그렇다.
SwiftUI로 짠 두 개의 얼굴
UI는 두 겹으로 나눴다. 평소에 보는 얼굴과, 설정할 때만 여는 얼굴이다.
메뉴바(MenuBarExtra, window 스타일). 평소엔 여기만 쓴다. 저장된 연결이 줄지어 있고 각 줄에 연결 버튼이, 이미 붙어 있으면 열기와 연결 해제 버튼이 붙는다. 창을 띄우지 않고 두 번 클릭으로 끝난다. 메뉴바 아이콘 자체도 상태를 말한다 — 하나라도 붙어 있으면 externaldrive.fill.badge.checkmark, 아니면 그냥 externaldrive.
private var menuBarIcon: String {
model.states.values.contains { $0.mountedURL != nil }
? "externaldrive.fill.badge.checkmark"
: "externaldrive"
}
관리 창(WindowGroup + NavigationSplitView). 연결을 추가·편집·삭제할 때만 연다. 왼쪽은 목록, 오른쪽은 선택한 연결의 상세다.

관리 창 실제 화면(공개용 예시 값). 방식·주소·계정·쓰기 권한·자동 연결이 한 화면에 모여 있고, 연결되면 Finder 마운트 경로 줄이 하나 더 붙는다.
상세 화면은 일부러 표 형태로 만들었다. 연결이 안 될 때 사람이 확인하는 건 결국 "주소를 제대로 넣었나, 계정이 맞나, 읽기 전용으로 걸어둔 건 아닌가"이기 때문이다. 실패하면 그 아래에 NetFS가 준 오류 코드를 사람 말로 바꿔 붙인다. -6602는 "서버 연결에 실패했습니다. 주소와 계정을 확인해 주세요", -5998은 "서버에서 사용할 수 있는 공유 폴더를 찾지 못했습니다" 같은 식이다. 숫자만 던지면 사용자는 아무것도 할 수 없다.
암호는 설정 파일에 한 글자도 남기지 않는다
저장은 두 갈래로 완전히 갈라놨다.
- 연결 설정(이름·방식·주소·계정·옵션) →
UserDefaults에 JSON으로 - 암호 → 오직 macOS Keychain(
kSecClassGenericPassword)에
Keychain 항목의 계정 키는 연결의 UUID다. 연결을 지우면 Keychain 항목도 같이 지우고, 게스트 연결로 바꾸면 남아 있던 암호를 삭제한다.

연결 추가 시트. 로그인 구역 아래에 "암호는 macOS Keychain에만 저장됩니다"를 명시했다. 주소 칸에는 방식에 맞는 예시가 placeholder로 뜬다.
여기서 제일 신경 쓴 건 화면에 안 보이는 쪽이다. 사용자가 주소 칸에 smb://user:secret@nas.local/shared처럼 인증 정보가 붙은 URL을 통째로 붙여넣는 일이 실제로 흔하다. 그대로 저장하면 암호가 평문으로 설정 파일에 박힌다. 그래서 주소를 정규화하는 지점에서 인증 부분을 무조건 떼어낸다.
// 주소 필드에 실수로 입력된 인증 정보가 설정 파일에 남지 않게 제거한다.
components.user = nil
components.password = nil
마운트를 실제로 호출하는 순간에도 한 번 더 떼어낸다. 두 군데 다 막아둔 이유는, 저장 경로가 늘어나도 새는 곳이 생기지 않게 하기 위해서다. 테스트로도 고정해뒀다 — smb://user:secret@nas.local/shared를 넣으면 저장되는 값은 smb://nas.local/shared여야 한다.
작은 편의도 같이 넣었다. WebDAV 주소를 http://로 시작하게 쓰면 그 자리에서 주황색 경고가 뜬다. 막지는 않되, 암호가 평문으로 나간다는 사실은 알려준다.
화면을 띄워보고 두 군데를 고쳤다
코드가 컴파일되는 것과 쓸 만한 것은 다른 문제다. 빌드한 앱을 실제로 실행해 클릭해보고 두 가지를 손봤다.
하나. 사이드바 폭. 기본값에 맡겼더니 연결 이름이 조금만 길어져도 사무실 NA…처럼 잘렸다. 그렇다고 넓게 고정하면 상세 화면이 좁아진다. 고정값 대신 범위를 줬다.
.navigationSplitViewColumnWidth(min: 220, ideal: 260, max: 340)
이름 줄과 상태 줄(연결됨/연결 안 됨) 두 줄이 안정적으로 들어가면서, 이름이 긴 연결이 많은 사람은 340까지 늘릴 수 있다.
둘. '연결 추가' 버튼 위치. 원래는 창 위쪽 툴바에 + 아이콘 하나로 뒀다. 그런데 연결이 하나도 없는 첫 실행 화면을 직접 보니 그 버튼이 전혀 눈에 들어오지 않았다. 빈 화면 안내문은 사이드바 한가운데 있는데 눌러야 할 버튼은 창 맨 위 구석에 있으니, 시선이 가는 곳과 손이 가야 할 곳이 따로 놀았다.
사이드바 아래쪽 고정 바로 내리고, 아이콘만 두지 않고 글자를 붙였다. Finder 사이드바나 시스템 설정이 쓰는, 이미 익숙한 자리다.
.safeAreaInset(edge: .bottom) {
...
Button { isAdding = true } label: {
Label("연결 추가", systemImage: "plus")
}
...
Button { Task { await model.refreshAll() } } label: {
Image(systemName: "arrow.clockwise")
}
}
오른쪽 끝에는 상태 새로 고침 버튼을 같이 뒀다. 목록을 보다가 "지금 진짜 붙어 있나?" 싶을 때 바로 누르는 자리다. 위 캡처에서 왼쪽 아래 + 연결 추가와 오른쪽 아래 새로 고침 아이콘이 그 결과다.
"껐다 켜도 붙어 있는가" — 이게 진짜 기능이다
RaiDrive를 쓰던 감각의 핵심은 연결 버튼이 아니라 다시 켰을 때 이미 붙어 있다는 것이다. 그래서 재연결 경로를 따로 짰다.
앱이 뜨면 먼저 저장된 연결 하나하나에 대해 지금 실제로 마운트돼 있는지를 조회한다. 마지막 마운트 경로를 statfs로 확인하고, 그게 아니면 현재 마운트된 볼륨들을 훑어 원격 주소(스킴·호스트·경로)가 같은 것을 찾는다. 이미 붙어 있으면 아무것도 하지 않는다. 그 다음, 자동 연결이 켜진 연결 중 아직 안 붙은 것만 조용히 마운트한다 — 이때는 Finder 창을 열지 않는다.
for profile in profiles {
await refresh(profile)
}
for profile in profiles where profile.connectOnLaunch {
guard state(for: profile).mountedURL == nil else { continue }
await connect(profile, revealInFinder: false)
}
이 순서가 중요하다. 상태 확인을 건너뛰고 바로 마운트하면 이미 붙어 있는 볼륨을 중복으로 붙이거나 오류를 뱉는다. 여기에 macOS 로그인 항목 등록(SMAppService.mainApp)을 켜두면, 맥에 로그인하는 것만으로 드라이브가 알아서 나타난다. 목표했던 감각은 이 조합에서 나온다.
남에게 줄 수 있는 상태로 만들기: 서명과 공증
여기까지가 코드고, 여기부터는 코드가 한 줄도 늘지 않는 작업이다. 그런데 이걸 안 하면 남에게 줄 수 없는 앱이 된다.
내 맥에서 빌드한 .app을 그대로 압축해 다른 맥에 보내면 Gatekeeper가 "확인되지 않은 개발자" 경고를 띄우고 실행을 막는다. 우클릭 → 열기 같은 우회를 안내하는 순간, 무료로 나눠주려던 앱은 "설명서를 읽어야 하는 앱"이 된다.
그래서 배포 파이프라인을 세 단계로 맞췄다.
- Developer ID 서명 — 빌드 스크립트가 이 맥에 설치된 Developer ID Application 인증서를 찾아
--options runtime(Hardened Runtime)과 타임스탬프를 붙여 서명한다. 인증서를 못 찾으면 ad-hoc 서명으로 떨어지되, 그렇게 됐다는 걸 콘솔에 경고로 남긴다. 조용히 실패하면 공증 단계에서야 알게 되기 때문이다. - Apple 공증(notarization) — 서명된 압축본을 Apple에 올려 악성코드 검사를 받는다.
- 티켓 스테이플링 — 통과 티켓을 앱 번들 안에 박아 넣는다. 이걸 해야 인터넷 연결이 없는 맥에서도 즉시 검증된다.
빌드 스크립트는 여기에 부수 작업까지 묶었다. SVG 아이콘 한 장에서 sips·iconutil로 .icns를 만들고, swift build -c release 결과를 .app 번들로 조립하고, 압축본과 Xcode Organizer용 .xcarchive까지 한 번에 뽑는다. 배포본을 만드는 일이 명령어 하나가 아니면 결국 안 하게 된다.
검증한 것
주장만 늘어놓지 않기 위해, 실제로 확인한 것만 적는다.
- 단위 테스트 8개 통과 — 주소 정규화·검증 6개, 저장소 2개. 저장소 테스트는
UserDefaults에 실제로 기록된 바이트를 문자열로 읽어 암호 문자열이 들어있지 않은지 직접 확인한다. "Keychain에 저장한다"는 말을 코드가 아니라 저장된 결과로 검사하는 쪽이 믿을 만하다. - 실제 WebDAV 서버 연결 성공 — 목이 아니라 진짜 서버에 HTTPS로 붙여, Finder에 볼륨이 뜨고 파일 목록이 보이는 것까지 확인했다.
- 앱 재실행 시 자동 연결 성공 — 앱을 완전히 종료했다 다시 실행하니, 저장된 연결의 마운트 상태를 조회한 뒤 끊겨 있던 연결을 Finder 창을 띄우지 않고 다시 붙였다.
- 공증 확인 — 배포본에 대한 Gatekeeper 평가가
accepted / source=Notarized Developer ID로 나오고, 스테이플된 티켓 검증도 통과한다.
지금의 한계
숨길 것 없이 적어둔다.
- Apple Silicon 전용. 배포본은 arm64 단일 아키텍처다. 인텔 맥에서 쓰려면 유니버설 빌드를 따로 뽑아야 한다.
- App Sandbox 미적용. 서명된 앱에 샌드박스 엔타이틀먼트를 넣지 않았다. Developer ID 직접 배포에서는 문제없지만, 다음 항목의 전제 조건이다.
- Mac App Store 버전 없음. 스토어에 올리려면 샌드박스가 필수고, 샌드박스 안에서 네트워크 볼륨을 마운트하려면 지금 구조를 손봐야 한다. 이번엔 직접 배포부터 완성하고 스토어는 뒤로 미뤘다.
- Google Drive·OneDrive·Dropbox·SFTP 미지원. 앞에서 적은 대로 NetFS 기반 설계의 의도된 경계다.
- 자동 재연결은 앱 실행 시점 한 번뿐. 네트워크가 끊겼다 돌아왔을 때 스스로 다시 붙지는 않는다. 지금은 새로 고침 버튼을 눌러야 한다.
다음 단계
한계 목록을 그대로 뒤집으면 할 일이 된다. 우선순위는 이 순서로 잡았다.
- 유니버설 빌드 — 인텔 맥까지 커버. 스크립트만 손보면 되는 가장 싼 개선이다.
- 네트워크 복귀 감지 후 재연결 — 슬립에서 깨거나 Wi-Fi가 바뀌었을 때 자동으로 다시 붙이기. 실사용에서 가장 아쉬운 지점이다.
- 연결 실패 원인 안내 강화 — 지금은 NetFS 오류 코드를 문장으로 바꾸는 수준이다. 주소·계정·방화벽 중 어디를 봐야 하는지까지 짚어주고 싶다.
- 샌드박스와 App Store — 앞의 셋을 마친 뒤에 검토한다.
하루의 결론
기능 목록만 보면 별것 아닌 앱이다. 원격 폴더를 붙였다 뗀다, 그게 전부다.
그런데 실제로 쓸 수 있는 물건이 되는 지점은 기능이 아니라 끝단에 있었다. 붙여넣은 주소에 섞인 암호를 지우느냐, 껐다 켠 다음에도 알아서 붙느냐, 남의 맥에서 경고 없이 열리느냐. 셋 다 기능 목록에는 한 줄도 안 올라가는 것들이다.
특히 마지막 하나 — Developer ID 서명, Apple 공증, 티켓 스테이플링 — 는 코드가 한 줄도 늘지 않는 작업인데, 이게 없으면 만든 사람 컴퓨터 밖으로 못 나간다. 만드는 일과 건네주는 일 사이의 거리를 오늘 다시 확인했다.
DriveBridge — SMB·WebDAV를 Finder 드라이브로 붙이는 macOS 메뉴바 앱 프로젝트로