- 2025년에도 iPhone에서 사용자가 보유한 음악 파일을 자유롭게 듣고 동기화하기 어렵다는 문제에서 출발해, 로컬 우선 음악 플레이어를 SwiftUI로 직접 구현함
- Apple Music 구독을 끊으면 iCloud Music Library 동기화가 멈추고, iTunes Match는 연 $24.99로 256kbps AAC 사본을 온라인에 저장하는 별도 유료 선택지임
- React Native/Expo는 깊게 중첩된 iCloud 폴더 순회와 파일 권한 처리에서 실패·크래시가 잦아, Apple 네이티브 API를 쓰는 SwiftUI 구현이 더 단순한 선택이 됨
- 앱은 SQLite FTS5로 파일명과 artist, album, title 같은 메타데이터를 검색하고, iCloud 가져오기·라이브러리 관리·재생 화면을 분리해 구성함
- iOS는 보안 범위 북마크와 개인 개발 빌드 제한 때문에, 개인이 만든 앱을 오래 안정적으로 쓰는 과정에서도 파일 접근과 배포 장벽이 남아 있음
왜 직접 음악 플레이어를 만들었나
- iPhone에서 사용자가 보유한 음악 파일을 재생하려면 Apple에 비용을 내거나 여러 제약을 우회해야 하는 상황이 여전히 남아 있음
- 로컬 음악 라이브러리를 이미 정리해 둔 사용자에게 스트리밍 서비스는 필수라기보다 편의 기능에 가까움
- Apple Music 구독을 해지하면 iCloud Music Library 기반 기기 간 동기화가 중단됨
- 이 기능은 구독 뒤에 있음
- iTunes Match를 쓰면 연 $24.99로 다시 사용할 수 있음
- Match는 256kbps AAC 사본을 온라인에 저장하며, 원본 파일은 사용자가 바꾸지 않는 한 그대로 남음
- 구독이 없으면 클라우드 동기화가 사라지고 케이블/Wi‑Fi 동기화로 돌아가야 함
- 목표는 오디오 파일을 가져오고, 정리하고, 재생하는 기본 음악 플레이어를 직접 만드는 것이었음
- 완성된 앱은 전체 텍스트 검색, iCloud 지원, 로컬 우선 경험을 제공하며 GitHub에 공개됨
기존 선택지가 맞지 않았던 이유
-
Apple 기본 앱
- Files 앱으로 iCloud의 음악 파일을 직접 재생할 수는 있지만, 음악 감상용 앱으로 설계되어 있지 않음
- 플레이리스트 관리, 메타데이터 정렬, 재생 큐 같은 핵심 기능이 부족함
- 음악 재생 자체는 가능해도 전체 사용자 경험은 제한적이며, 관련 Apple 커뮤니티 논의도 있음
-
서드파티 앱
- App Store에는 오프라인 음악 재생 앱이 많지만, 사용자가 이미 소유한 파일을 재생하는 앱에도 구독 기반 가격을 적용하는 경우가 많음
- Doppler는 단일 결제 모델이라는 장점이 있었음
- 다만 UX가 앨범 관리 중심이고, 검색 품질이 만족스럽지 않았으며, 중첩 폴더가 많은 iCloud 가져오기는 느리고 사용하기 어려웠음
React Native에서 SwiftUI로 바꾼 과정
-
처음 선택한 React Native/Expo
- 과거 Swift 경험 때문에 Swift를 피하고, 웹 개발 경험을 재사용할 수 있는 React Native/Expo를 먼저 선택함
- 재생 UI 구현은 쉬웠고, 필요한 기능을 갖춘 것으로 보이는 Gionatha Sturba의 music-player 템플릿을 사용함
- 문제는 파일 시스템 접근과 클라우드 파일 동기화에서 커졌음
expo-filesystem같은 라이브러리는 기본 파일 선택은 지원했지만, 깊게 중첩된 iCloud 디렉터리의 재귀 순회는 자주 실패하거나 앱 크래시를 일으킴- iOS 샌드박스는 명시적 사용자 권한 없이 앱이 파일을 읽지 못하게 함
- 결과적으로 JavaScript 기반 접근은 Apple 네이티브 API를 직접 쓰는 것보다 더 복잡해졌고, Swift로 전환하면 iCloud 파일 접근과 샌드박스 권한을 더 잘 제어할 수 있었음
-
SwiftUI를 선택한 이유
- UIKit이나 스토리보드 대신 SwiftUI를 선택해 선언적 UI 계층을 만들고, 도메인 로직과 데이터 동기화에 집중함
- Swift의
async/await와 Swift Actors 통합은 데이터 흐름과 동시성 관리를 쉽게 만들었음 - SwiftUI는 앱을 독립적인 ViewModel 컴포넌트로 구조화하는 데 도움이 됨
- OpenAI o1과 DeepSeek 같은 LLM은 순수 UI 코드나 데이터 바인딩 코드를 생성할 때 복잡한 상호 의존성을 덜 만들었음
앱 구조와 데이터 모델
-
세 가지 주요 화면
- 앱은 Library import, Library management, Player and playback 세 흐름으로 나뉨
- Library import는 iCloud 라이브러리 폴더를 추가하고, 모든 폴더에서 오디오 파일을 스캔해 SQLite 데이터베이스에 경로를 삽입함
- Library management는 추가한 곡을 관리하고 플레이리스트를 구성함
- Player and playback은 반복, 셔플, 큐 관리, 재생·정지·다음 곡 기능을 담당함
- Apple 기본 파일 선택기는 키워드로 검색한 여러 디렉터리와 여러 파일을 한 번에 선택하는 흐름에 맞지 않음
- 빈 라이브러리로 앱을 실행하면 Sync 탭에서 “Add iCloud Source” 버튼을 보여줌
- 폴더를 선택하면 트리 순회 중 진행 막대를 표시하고, 색인이 끝나면 Library 탭으로 이동함
- 첫 화면에는 Playlists / Artists / Albums / Songs가 표시됨
- 트랙을 누르면 하단에 Mini-Player가 나타나고, 이를 탭하면 전체 화면 Player로 이동함
- 전체 화면 Player는 셔플, 반복, 큐 재정렬, 볼륨을 제공함
- Sync에서 “+”를 눌러 다른 폴더를 선택하면 새 곡을 백그라운드에서 병합하며 재시작이 필요 없음
-
서버식 로직 계층
- 모바일 앱이지만 웹·클라우드 백엔드 경험을 바탕으로 백엔드식 아키텍처를 적용함
- 도메인·로직 계층을 View와 ViewModel 계층에서 분리해 클라우드 동기화, 메타데이터 파싱, SQLite 데이터 접근을 안정적으로 처리함
- 계층 구조는 다음과 같음
- SQLite가 원본 곡 행과 FTS 색인을 저장함
- Repository가 데이터베이스를 감싸고 비동기 API를 노출함
- 도메인 Actor가 가져오기, 검색, 큐 로직 같은 비즈니스 규칙을 소유함
- ViewModel이 Actor를 구독하고 UI용 구조체로 변환함
- SwiftUI View는 전달받은 데이터를 렌더링함
- 상태 변경은 스레드 안전하게 유지되고, iCloud 동기화·재생·UI는 직접 계층을 건너뛰지 않도록 분리됨
SQLite FTS5로 만든 로컬 검색
- 영구 저장소는 SQLite를 사용했고, CoreData는 쓰지 않음
- CoreData를 피한 이유는 스키마, 원시 쿼리, 특히 전체 텍스트 검색을 직접 제어해야 했기 때문임
- iOS 11 전후부터 FTS 기능이 포함된 SQLite를 추가 설정 없이 사용할 수 있음
- 일반 쿼리에는 SQLite.swift를 사용했지만, FTS 쿼리는 일반 SQL 문으로 처리함
- SQLite FTS5는 파일명과 artist, album, title 같은 메타데이터를 별도 검색 인프라 없이 질의할 수 있게 함
-
FTS 테이블 구성
- FTS5 테이블은 두 개를 사용함
songs_fts:SQLiteSongRepository가 사용하며artist,title,album,albumArtist를 색인함source_paths_fts:SQLiteSourcePathSearchRepository가 사용하며fullPath,fileName을 색인함- 두 FTS 테이블은 기본 B-tree 테이블인
songs,source_paths옆에 위치함 - FTS는 UI에서 읽기 전용이며, 모든 쓰기는 Repository 내부에서 처리됨
songs_fts생성 예시는songId를UNINDEXED로 두고unicode61토크나이저를 사용함- 다양한 문자를 처리하기 위해
unicode61을 선택함 - 검색하지 않는 키는
UNINDEXED로 지정해 용어 사전이 불필요하게 커지지 않게 함
-
부분 검색과 순위화
- 사용자가 “lumine”을 입력하면 내부적으로 “lumine*”처럼 와일드카드를 붙여 부분 검색 결과를 즉시 찾음
- 검색 결과 정렬에는 SQLite의
bm25를 사용함 - 이 방식은 예측 가능한 스키마, 로컬 우선 접근, 강력한 전체 텍스트 검색을 제공하며 네트워크 의존성이나 외부 서비스를 추가하지 않음
iOS 파일 접근과 북마크 제약
- iOS 앱은 파일 위치를 기억하기 위해 영속 북마크를 저장할 수 있음
- 하지만 앱 샌드박스 외부 파일에 확장 접근을 주는 security-scoped bookmarks는 macOS에서만 제공됨
- iOS 앱은 일반 북마크로 파일 경로를 기억하고 문서 선택기를 통해 다시 접근을 요청할 수 있지만, 해당 접근이 조용히 계속 유지된다는 보장은 없음
- 관련 내용은 Apple의 bookmark documentation에 있음
- 이를 완화하기 위해 파일을 앱 자체의 샌드박스 컨테이너로 복사하는 대체 메커니즘을 구현함
- 북마크가 유효한 동안 백그라운드에서 파일을 선제적으로 복사함
- iOS가 권한을 재설정해도 잘못된 오디오 파일 참조에 접근할 위험을 줄임
- 이 접근은 색인 속도도 개선함
- 접근이 활성화된 동안 폴더 구조를 한 번 스캔함
- 관련 오디오 파일만 가져옴
- 깊게 중첩된 디렉터리를 안전하게 순회함
- 외부 위치의 개별 오디오 파일을 기기 재시작 후에도 안정적으로 재생하는 문제는 해결되지 않았음
- 네이티브 앱에서도 이 사용 사례는 iOS에서 충분히 지원되지 않으며, 파일 접근을 안정적으로 처리하기가 여전히 복잡함
재생과 UI 구현
-
메타데이터 파싱
- 오디오 파일 메타데이터 파싱에는 Apple AVFoundation 프레임워크의
AVURLAsset을 사용함 AVURLAsset은 title, album artist 같은 미디어 파일 메타데이터를 검사할 수 있음- 트랙 번호 같은 일부 필드는 ID3 태그에서 직접 찾아야 했음
- 공식 문서가 엣지 케이스를 충분히 다루지 않아 GitHub 검색으로 구현 예시를 참고함
- 오디오 파일 메타데이터 파싱에는 Apple AVFoundation 프레임워크의
-
오디오 재생
- 라이브러리 색인 후에는
AVAudioPlayer인스턴스를 초기화해 재생하는 방식으로 오디오 플레이어를 구현함 - Control Center에서 음악을 제어하려면
AVAudioPlayerDelegate프로토콜을 구현하고 Apple의MPRemoteCommandCenter에 연결해야 함 MPRemoteCommandCenter는 시스템 수준 재생 제어에 앱이 응답할 수 있게 함
- 라이브러리 색인 후에는
Swift와 Apple 개발 경험
-
어려웠던 점
- Xcode의 실시간 SwiftUI 프리뷰는 진전이지만, 전체 개발 경험은 Flutter가 5년 전에 제공하던 VSCode 통합, 실시간 시뮬레이터 리로드, 익숙한 디버깅 도구 수준에 못 미쳤음
- Swift의 Neovim 또는 VSCode용 LSP 지원은
xcode-build-server같은 추가 도구가 필요하고, 웹 중심 생태계의 개발 경험과 같지는 않음 - Apple SDK 일부는 여전히 Objective-C 스타일에 남아 있음
- Spotlight 파일 검색은
NSMetadataQuery로만 노출되고,NSMetadataQuery는 KVO와 문자열 키를 사용함 - Swift 친화적 래퍼는 아직 없음
- SwiftUI의 선언적 UI는 유용하지만, iCloud 권한이 얽힌 전체 앱 동작은 프리뷰에서 에뮬레이션할 수 없어 클라우드 상호작용을 직접 모킹해야 함
-
좋았던 점
async/await덕분에 I/O 중심 동시성 코드를 콜백 없이 명령형 코드처럼 작성할 수 있었음- Actor 안에 동기 코드도 쉽게 넣고 JavaScript 생태계처럼 호출할 수 있었음
- React Native나 Flutter의 오픈소스 바인딩에 제한되지 않고 Apple 네이티브 라이브러리를 직접 사용할 수 있었음
- 많은 Apple API는 예제가 있어 시작하기 쉬웠음
- SwiftUI의 React식 UI 구성 방식은 생산성과 탐색 여지를 제공함
개인용 앱을 오래 쓰기 어려운 iOS
- 약 1.5주 동안의 구현 끝에 iCloud 저장소에서 오디오 파일을 가져올 수 있는 로컬/오프라인 음악 플레이어를 만들었음
- 하지만 개발자 인증서가 없으면 앱은 7일만 실행되고, 이후에는 다시 빌드해야 함
- Apple Developer Program에 등록하려면 $99를 내야 함
- EU의 DMA Act 이후에도 사이드로딩은 완전히 열려 있지 않음
- EU 사용자는 개발자 사이트에서 서드파티 마켓플레이스 앱을 설치할 수 있음
- 개발자는 여전히 Apple의 $99/년 프로그램에 등록해야 함
- Apple의 Alternative Terms에도 동의해야 함
- 개인·취미 용도에서는 7일 개발 빌드 제한이 사라지지 않음
- iOS의 PWA도 제한이 남아 있음
- iOS 16~18.x 업데이트 이후에도 PWA는 Safari 샌드박스 안에서 실행됨
- WebGL2와 web-push는 사용할 수 있음
- Web Bluetooth, USB, NFC, Background Sync, 약 50MB를 넘는 보장 저장소는 제공되지 않음
- WebGL은 Metal shim을 거치므로 실제 프레임레이트는 네이티브 Metal 앱보다 뒤처지는 경우가 많음
- AI는 익숙하지 않은 기술을 다루는 데 필요한 지식을 더 쉽게 제공해 소프트웨어 개발 복잡도를 낮췄지만, iOS 앱은 여전히 인위적 규칙을 따라야 함
- 개인이 직접 만든 앱이라도 일주일 넘게 실행하려면 Apple의 최종 허들이 남아 있으며, 이는 개인 앱 개발과 배포를 제약함