PHPicker로 URL을 가져오게 된 계기
PHPicker로 이미지를 가져올 때, 처음에는 Data 타입으로 메모리에 저장하려고 했어요. 그런데 문제가 생겼어요 🤔
이미지 데이터를 Data 타입으로 저장하면 용량이 큰 이미지들 때문에 메모리 사용량이 급격히 늘어나거든요.
사진 한 장이 몇 MB씩 하니까, 여러 장 저장하면 금세 메모리 부족 경고가 뜰 수 있다고 판단을 내렸어요
그래서 생각해낸 방법이 URL만 저장해두고 필요할 때마다 파일을 읽어오는 방식이었어요 💪🏻
result.itemProvider.loadFileRepresentation(forTypeIdentifier: "public.image") { url, error in
guard let url = url else { return }
print("받은 URL: \(url)")
// file:///private/var/mobile/Containers/Data/PluginKitPlugin/...
self.savedImageURLs.append(url)
}
하지만 여기서 첫 번째 문제를 만납니다.....ㅜㅜ
아!!! 그전에 PHPicker에 대해서 자세하게 정리한 아주 훌륭한 블로그를 공유해볼게요https://codeisfuture.tistory.com/75
오늘은 동작보다는 URL이 어떻게 저장되는지에 대해 집중해보려고해요!

이건☝🏼첫번째 이슈: 임시 파일 경로로 하지 않기~
PHPicker 내부 동작 과정
- 사용자 선택 단계: 사용자가 Photos 앱에서 이미지를 선택
- 권한 확인: Photos 프레임워크가 해당 이미지 접근 권한 확인
- 임시 복사본 생성: 원본을 보호하기 위해 임시 디렉토리에 복사본 생성
- URL 반환: 임시 파일의 URL을 콜백으로 전달
🕳️ 임시 파일 경로의 특징
실제로 받아오는 URL을 출력해보면 이런 형태예요:
file:///private/var/mobile/Containers/Data/PluginKitPlugin/com.apple.PhotosUIPrivate.PhotosUIEdit/tmp/2F4A5B6C-7D8E-9F1A-2B3C-4D5E6F7A8B9C.jpeg
모야 이건....😅
하나씩 뜯어보죠...
- PluginKitPlugin: PhotosUI는 우리 앱 안에서 돌아가는 게 아니라 시스템의 플러그인으로 동작하는 로직이죠.
그러다보니까 기존 내 앱의 샌드박스랑 다른거고 플러그인의 샌드박스라고 생각하면 될거같아요 - tmp: 임시 파일을 저장하기 위한 용도이며, 앱이 실행되고 있을 때만 유지됩니다.
PhotosUI가 종료될때, 메모리 부족할때, 사용자가 앱을 종료할때, 시스템에 의해 숙청당할때 제거 됩니다 - UUID 형태의 파일명: 충돌 방지를 위한 고유 식별자
그러다보니까 앱에서는 해당 URL에 직접 접근이 불가능했던 거죠‼️
해결책 Documents 디렉토리로 복사
func loadImageSafely(from result: PHPickerResult) {
result.itemProvider.loadFileRepresentation(forTypeIdentifier: "public.image") { url, error in
guard let sourceURL = url else { return }
//1.Documents folder경로 가져오기
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
//2. 파일이름 생성하기
let fileName = UUID().uuidString + "." + sourceURL.pathExtension
let destinationURL = documentsPath.appendingPathComponent(fileName)
// 임시 파일을 Documents로 복사
try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
// UserDefaults에 저장
saveImageURL(destinationURL)
}
}
코드 설명:
- FileManager.default.urls(for:in:): 검색 경로를 특정 도메인 안에서 찾을 때 사용해요
- for: .documentDirectory: 앱 샌드박스의 Documents 폴더 경로
- in: .userDomainMask: 현재 앱의 사용자 영역
- 배열을 반환하기 때문에 [0]을 취해서 첫 번째 경로를 사용
- UUID().uuidString + "." + sourceURL.pathExtension: 1A2B3C4D-1234-5678-9ABC-123456789DEF.png 같은 유니크한 파일명 생성해요
그리고 최종 URL을 이전에 1+2를 합하는 거죠
이렇게 하니 앱 실행 중에는 문제없이 잘 작동했어요.

자 제가 이전 코드를 통해 Print로 찍어봤어요. 이미지가 잘 들어갔거든요?
그래서 터미널에서 해당 URL을 Open한 결과! 잘나와요

음 아주 잘 열리네요??
하지만 두번째 문제는 시뮬레이터로 코드를 수정하고 다시 앱을 빌드할 때 나타났습니다 🚨 🚨 🚨 🚨
해당 URL을 Userdefaults에 저장한 후에 다시 앱을 빌드해볼게요

URL이 잘 저장이 되었는데 다시 빌드하니까 이번엔 파일이 없다고 하네요?

이건 ✌🏻두번째 이슈 ContainerID가 바뀌는 이슈 ~
UserDefaults에 저장해둔 이미지 URL들을 불러와서 접근하려는데, 계속 "파일을 찾을 수 없습니다" 에러가 뜨는 거예요 😱
그래서 뭐가 문제인지 확인해봤어요:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 현재 Documents 경로 확인
if let docsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
print("📂 현재 Documents 경로: \(docsPath.path)")
}
}
앱이 launch가 되고 나서 Documents를 확인해보니까 !!
👀👀👀
첫번째 런: .../Application/1127AB61-1E15-4752-B10C-4CC4F558FE70/Documents
두번째 런: .../Application/91A4199E-82F9-4B81-91FE-4CD0E23780AF/Documents
세번째 런: .../Application/F5F8EA7C-7DCA-46C2-B04B-EF887CA84D56/Documents
틀린그림찾기입니다
3. ......
2....
1.,.
와 다 맞추셨네요??? 🔔 자 맞아요 Documents의 앞 주소값이 다 달라요
iOS 샌드박스 구조 이해하기
이 문제를 제대로 이해하려면 iOS의 샌드박스 시스템을 알아야 해요
아 이것도 아주 좋은 블로그가 있어서 자세히 적기보다는 한번 읽고 오는 것을 추천드려요
https://codeisfuture.tistory.com/113
App Container 구조
자 읽고 오셨죠?
모두가 이제 아시다시피 각 iOS 앱은 고유한 App Container 안에서 실행됩니다
/var/mobile/Containers/Data/Application/[APP_CONTAINER_ID]/
├── Documents/ # 사용자 문서, iCloud 백업 O
├── Library/
│ ├── Application Support/ # 앱 지원 파일들
│ ├── Caches/ # 캐시 파일들, 백업 X
│ └── Preferences/ # 설정 파일들
└── tmp/ # 임시 파일들
App Container ID는 다음과 같은 요소들을 조합해서 생성됩니다:
- 앱의 Bundle ID
- 앱 서명 정보
- 설치 시점
- 시스템의 보안 정책
자 이제 왜 안 열리는지 눈치 채신 분 계신가요?
바뀌는 ContainerID 경로를 이미지의 URL로 포함해서 그런거에요.
그러다 보니 어? 이전 ContainerID의 값이 사라졌는데?
=> 경로가 없으니 찾을 수가 없는거죠
그러면 왜 왜! 컨테이너ID를 새로 만들어서 이런 일이 발생하게 할까요?
- 개발 과정의 격리: 이전 빌드의 데이터가 새 빌드에 영향을 주지 않도록
- 클린 테스트 환경: 매번 깨끗한 상태에서 테스트 가능
실제 기기 vs 시뮬레이터의 차이
실제 기기에서는?
실제 기기에서는 앱 서명과 Bundle ID가 동일하면 같은 컨테이너를 유지합니다
App Store에서 설치하거나 동일한 개발자 계정으로 서명된 앱이라면 컨테이너가 유지되죠.
시뮬레이터의 특수한 동작
하지만 시뮬레이터에서는 개발 편의를 위해 매번 새로운 컨테이너를 만듭니다.
이는 Apple이 의도한 설계로, 개발자가 클린한 환경에서 테스트할 수 있도록 돕는 기능이에요.
그러면 우리는 문제를 어떻게 해결할 수 있을까요?
최종 해결책: 상대 경로 사용
절대 경로 대신 파일명만 저장하고 런타임에 경로 계산하는 방식으로 개선을 해야해요
Apple 공식 문서에서 강조하는 점:
"Hard-coded pathnames are fragile and liable to break over time, so the system provides interfaces that search for files in well-known locations."
절대 경로를 하드코딩하지 말고, 항상 시스템 API를 통해 동적으로 경로를 계산하는 것이 안전한 방법이라고 합니다
그래서 이렇게 개선했습니다:
var imageUrl: URL {
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
return documentsPath.appendingPathComponent(fileName)
}
이제 파일명만 저장하고, 실제 경로는 런타임에 동적으로 계산하는 방식이에요! ✨
궁금증: 새 Container에도 파일이 있는 이유🧐 🧐 🧐
여기서 한 가지 의문이 들었어요:
"어? PHPicker로 선택한 파일을 UUID로 저장했는데, 새로 빌드하면 Container가 새로 생기잖아요? 그럼 파일들이 다 사라져야 하는 거 아닌가요?"
실제로 테스트해보니까 신기한 일이 벌어졌어요:
# 기존 경로
/Users/sunho/Library/Developer/CoreSimulator/Devices/AB2DD87B-F10B-44E5-9BBD-244643AB6999/data/Containers/Data/Application/6FF82983-E04B-4903-85D4-1C855BDED788/Documents/DB2DC2F0-0D56-44A2-8621-0B7D67DD7EF7.jpeg
# 새로운 Container에서도 같은 파일명으로 접근 가능!
# inode: 22848393 (똑같음!)
# 파일 크기: 1896240 (똑같음!)
왜 이런 일이 일어날까요? 🤯 🤯 🤯 🤯
하드링크 vs 소프트링크
답은 바로 하드링크(Hard Link) 방식 때문이에요!
하드링크는 같은 파일을 가리키는 서로 다른 이름이라고 생각하면 돼요:
하나의 인스턴스를 공유하는 참조타입같은 개념으로 이해하면 돼요!!!
핵심 특징:
- 같은 inode 번호를 가짐 (실제로는 같은 파일!)
- 실제 파일 데이터는 디스크상 한 곳에만 존재
- 용량을 중복으로 차지하지 않음
- 원본을 삭제해도 다른 링크로 접근 가능
이걸 통해 시뮬레이터의 동작을 예측해볼게요
1. 새 Container ID로 디렉토리 📁 구조 생성
2. 기존 Documents의 파일들을 복사하지 않고 하드링크로 연결 🔗
3. 결과적으로 성능도 좋고 용량도 절약하면서, 개발자는 이전 데이터를 그대로 사용 가능!
소프트링크 (Symbolic Link, Symlink)
위의 예시를 ReferenceType이라고 했으면 어?이건 Value인가? 추측할 수 있지만 다른개념이에요:<
포인터의 포인터라고 생각하면 될거같습니다
- 다른 inode를 가짐 (실제로는 경로 정보만 저장)
- 원본 파일의 경로를 가리키는 포인터
- 원본 파일 삭제하면 깨진 링크가 됨
- 다른 파일 시스템에도 생성 가능
- 디렉토리에도 생성 가능
다시한번 상황을 요약하자면



이번 글을 쓰면서 대략 8시간 정도 소요된 것 같아요. 시간을 너무 많이 투자했나 생각했는데, 할수록 무지를 깨닫게 되는 것 같아요.
절대 경로를 왜 피해야 하는지에서 시작해서, 결국 파일 시스템까지 이어지는 개념이었네요 💡
참고자료
PHPickerController의 UTI 활용법
왜 PHPickerController가 세상에? 나오게 되었을까?🤔 🤔 iOS에서 사진을 가져오는 전통적인 방식은 UIImagePickerController였습니다.하지만 몇 가지 심각한 단점이 있었습니다:1. 프라이버시 문제 👿UIImage
codeisfuture.tistory.com
Apple의 보안
안녕하세요! iOS 개발에서 정말 중요한 보안 메커니즘에 대해 정리해보겠습니다. 프로비저닝 프로파일과 샌드박스는 iOS 생태계의 핵심 보안 기둥이라고 할 수 있어요 🔐🔐🔐프로비저닝 프로
codeisfuture.tistory.com
PHPickerController의 UTI 활용법
왜 PHPickerController가 등장했을까? 🤔iOS에서 사진을 가져오는 전통적인 방식은 UIImagePickerController였습니다.하지만 몇 가지 심각한 단점이 있었습니다:1. 프라이버시 문제 👿UIImagePickerController를 사
codeisfuture.tistory.com
didFinishPickingMediaWithInfo returns different URL in iOS 13
- (void)videoPickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info returns different URLs in iOS 13 and the
stackoverflow.com
About Files and Directories
About Files and Directories The file system is an important part of any operating system. After all, it’s where users keep their stuff. The organization of the file system plays an important role in helping the user find files. The organization also make
developer.apple.com
Migrating your app’s files to its App Sandbox container | Apple Developer Documentation
Simplify your app’s access to documents and supporting files.
developer.apple.com
'SWIFT개발일지' 카테고리의 다른 글
Metal3편 - 메모리 사용량 급증 버그 수정 (0) | 2025.04.20 |
---|---|
이미지 최적화 3탄(kingFisher를 삭제하고 Custom) (0) | 2025.04.16 |
이미지 최적화 적용하기 (1) | 2025.03.06 |
ScrollView 꾸미기? Deep Dive (0) | 2025.02.22 |
MemoryLeak을 찾아보자(강한참조순환인가?) - 실전편2 (0) | 2025.01.16 |