Android scoped storage는 Capacitor 앱이 자체 폴더 밖에서 읽고 쓸 수 있는 범위를 제한합니다. Android 10부터 적용되었고 Android 11부터 완전히 적용된 이 방식에서는 앱이 전용 디렉터리를 자유롭게 사용할 수 있고, MediaStore를 통해 공유 컬렉션에 파일을 추가할 수 있으며, 그 외의 파일은 Storage Access Framework를 통해 사용자가 직접 선택해야 합니다. 이 가이드에서는 Android 버전별로 무엇이 바뀌었는지, 어떤 Capacitor Directory 값이 여전히 유효한지, 그리고 Capacitor 8에서 지원되는 방식으로 파일을 저장, 가져오기, 내보내는 방법을 설명합니다.
버전별 변경 사항
| Android | API | Capacitor 앱에 중요한 변경 사항 |
|---|---|---|
| 10 | 29 | Scoped storage 도입. 앱은 다음을 통해 옵트아웃 가능 requestLegacyExternalStorage="true" API 29 타겟팅 시 |
| 11 | 30 | 30 이상 타겟팅하는 앱에 Scoped storage 강제 적용. requestLegacyExternalStorage 무시됩니다. WRITE_EXTERNAL_STORAGE 더 이상 아무것도 부여하지 않습니다. MANAGE_EXTERNAL_STORAGE 도입됨. 트리 피커로 Download 루트 또는 볼륨 루트를 선택할 수 없음 |
| 12 | 31-32 | 주요 스토리지 변경 없음 |
| 13 | 33 | READ_EXTERNAL_STORAGE 로 대체됨 READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO. 다른 앱의 문서에 접근할 수 있는 권한 없음 |
| 14 | 34 | 로 사진 부분 접근 READ_MEDIA_VISUAL_USER_SELECTED |
| 15 및 16 | 35-36 | 새로운 스토리지 모델 없음, 사진 및 비디오 권한에 대한 Play 정책 강제 적용 |
Capacitor 8은 API 36을 타겟으로 하므로, 최신 기기에서는 항상 전체 scoped storage로 실행됩니다. 이전 동작으로 되돌리는 플래그는 없습니다.
이전 Capacitor code에서 무엇이 깨졌나
Capacitor 2 또는 3을 위해 작성되었거나 Cordova에서 이식된 Code은 새 기기에서 조용히 실패하는 경우가 많습니다:
Filesystem.writeFile({ directory: Directory.Documents })또는Directory.ExternalStorage“전화로 저장”을 위해. Android 11+에서는 이러한 경로가 공유 스토리지를 가리키며, 작성에 실패하거나 재설치 후 파일을 다시 읽을 수 없습니다.- 다음과 같은 임의 경로 읽기
/storage/emulated/0/Download/file.pdf설정에 입력하거나 서버에서 받은 경로에서 - 요청하기
READ_EXTERNAL_STORAGE사용자의 문서를 나열하려면. Android 13 이상에서는 미디어 파일이 아닌 파일에는 이 권한이 아무 효과가 없습니다. - 사용하기
Filesystem.requestPermissions()그리고 ~라고 가정하면granted앱이 어디에나 쓸 수 있다는 뜻입니다. - 파일 경로를 만들 때
content://피커에서 반환된 URI.
권한 없이도 여전히 작동하는 것
앱 자체 디렉터리는 scoped storage의 영향을 받지 않으며 권한도 필요 없습니다:
Capacitor Directory |
Android 경로 | 사용자에게 보임 | 앱 제거 시 삭제됨 |
|---|---|---|---|
Data, Library, LibraryNoCloud |
/data/data/<package>/files |
아니요 | 지원 |
Cache |
/data/data/<package>/cache |
아니요 | 지원 |
External |
/storage/emulated/0/Android/data/<package>/files |
USB 또는 일부 파일 관리자만 지원 | 지원 |
ExternalCache |
/storage/emulated/0/Android/data/<package>/cache |
동일 | 지원 |
Documents |
공유 Documents 폴더 |
지원 | 아니요 |
ExternalStorage |
공용 저장소 루트 | 지원 | 아니요 |
첫 4행은 안전합니다. Documents 및 ExternalStorage 레거시 행입니다. Android 11부터 앱은 해당 위치에 있는 파일 중 자신이 생성한 파일만 볼 수 있으며, 재설치 후에는 그 파일에 대한 접근 권한도 잃습니다. 새로운 code에는 지원되지 않는다고 간주하세요.
External 대용량 파일(오프라인 비디오, 지도)에 유용합니다. 더 큰 파티션에 위치할 수 있으며, 해당 위치의 파일은 Auto Backup에 포함되지 않습니다. Android 11부터 다른 앱은 읽을 수 없습니다 Android/data따라서 실제로는 여전히 비공개입니다.
사용자가 찾을 수 있는 파일 저장: MediaStore
사용자가 청구서에서 “다운로드”를 탭하면 Downloads 폴더에 저장되기를 기대합니다. Android 10 이상에서는 앱이 권한 없이 공유 Downloads, Pictures, Movies, Music 및 Documents 컬렉션에 MediaStore를 통해 파일을 삽입할 수 있습니다.
@capgo/capacitor-file-sharer 이것을 save():
bun add @capgo/capacitor-file-sharer
bunx cap sync
import { FileSharer } from '@capgo/capacitor-file-sharer';
import { Filesystem, Directory } from '@capacitor/filesystem';
const { uri } = await Filesystem.getUri({ path: 'exports/invoice-1042.pdf', directory: Directory.Data });
const result = await FileSharer.save({
filename: 'invoice-1042.pdf',
path: uri,
contentType: 'application/pdf',
android: {
saveDirectory: 'downloads', // 'downloads' | 'pictures' | 'movies' | 'music' | 'documents'
relativePath: 'MyApp/Invoices', // subfolder, Android 10+
},
});
console.log(result.uri); // content:// URI of the saved file
iOS에서는 동일한 호출이 공유 시트를 열고 사용자가 “Files에 저장”을 선택하므로, 하나의 code 경로가 두 플랫폼을 모두 커버합니다.
Android 9 이하에서는 공유 저장소에 쓰려면 여전히 WRITE_EXTERNAL_STORAGE이 필요합니다. 해당 버전에만 선언하세요:
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
Capacitor 8은 Android 7(API 24) 이상을 지원하므로, 여전히 Android 7~9 기기에 배포 중이라면 그 줄을 유지하세요.
사진과 동영상
미디어의 경우 파일을 직접 쓰는 것보다 플랫폼 플로우를 우선하세요. @capacitor/camera 은/는 saveToGallery: true으로/로 갤러리에 쓰며, 구형 Android 버전에서만 레거시 저장소 권한이 필요합니다. 가이드를 참고하세요: Capacitor에서 사진 촬영 및 편집.
사용자 파일 읽기: 권한 대신 피커
Scoped storage는 사용자가 파일을 선택하도록 기대합니다. Android 선택기는 content:// 저장소 권한이 없어도 앱이 읽을 수 있는 URI를 반환합니다.
import { CapgoFilePicker } from '@capgo/capacitor-file-picker';
import { Filesystem, Directory } from '@capacitor/filesystem';
const { files } = await CapgoFilePicker.pickFiles({ types: ['application/pdf'], limit: 1 });
const picked = files[0];
// Copy into app storage right away: picker grants can expire
await Filesystem.copy({
from: picked.path!,
to: `imports/${picked.name}`,
toDirectory: Directory.Data,
});
사진과 비디오의 경우, @capgo/capacitor-file-picker의 pickMedia 및 @capacitor/camera의 chooseFromGallery 권한이 전혀 필요 없는 Android 13+의 Android Photo Picker를 사용하세요.
READ_MEDIA_* 권한 피하기
Google Play의 사진 및 비디오 권한 정책은 다음만 허용합니다 READ_MEDIA_IMAGES 및 READ_MEDIA_VIDEO 갤러리 앱이나 사진 편집기처럼 핵심 기능이 광범위한 갤러리 접근을 요구하는 앱에 해당합니다. 그 외의 앱은 반드시 피커를 사용해야 하며, Play Console에 이를 명시해야 합니다. 사용하는 플러그인이 이러한 권한을 선언하는 경우, 병합된 매니페스트에서 제거하세요:
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"
tools:node="remove" />
추가 xmlns:tools="http://schemas.android.com/tools" 에 <manifest> 태그를 먼저 추가하세요.
Storage Access Framework로 폴더 접근
일부 앱은 폴더에 대한 지속적 접근이 필요합니다. 사용자 폴더로 동기화되는 메모 앱이나 현장 앱의 내보내기 폴더가 그 예입니다. Storage Access Framework(SAF)를 사용하면 사용자가 폴더를 ACTION_OPEN_DOCUMENT_TREE:
const { path } = await CapgoFilePicker.pickDirectory();
// On Android: a content://com.android.externalstorage.documents/tree/... URI
알아야 할 세 가지:
- 결과는 경로가 아닌 트리 URI입니다. 에 전달할 수 없습니다.
Filesystem.writeFile폴더 내부의 파일은DocumentsContract또는DocumentFile을 통해 네이티브 code에서 생성 및 읽습니다. - 재시작 후에도 접근 권한이 유지되지 않습니다 앱이 URI에 대해
takePersistableUriPermission를 호출하지 않는 한. 해당 기능이 필요하면 작은 네이티브 메서드를 추가하세요:
@PluginMethod
public void persist(PluginCall call) {
Uri uri = Uri.parse(call.getString("uri"));
getContext().getContentResolver().takePersistableUriPermission(
uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
call.resolve();
}
- 일부 폴더는 권한을 부여할 수 없습니다. Android 11 이상에서는 피커가 다음을 거부합니다: 내부 저장소 루트, 해당
Download폴더의 루트, 그리고Android/data및Android/obb. 사용자에게 하위 폴더를 만들거나 선택하도록 안내하세요.
앱당 영구화된 권한은 개수에 제한이 있으며, 사용자는 시스템 설정이나 앱 데이터 삭제 시 이를 취소할 수 있습니다. 접근 권한은 getPersistedUriPermissions() 권한이 없으면 다시 요청하세요.
모든 파일 액세스: MANAGE_EXTERNAL_STORAGE
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
이 권한은 예전의 “모든 파일 읽기 및 쓰기” 동작을 제공하지만, 런타임 프롬프트는 아닙니다. 사용자가 별도 설정 화면에서 직접 활성화해야 하며, Google Play에서 수동으로 검토합니다. 허용되는 사용 사례는 파일 관리자, 백업 및 복구, 백신, 문서 관리, 기기 내 파일 검색 등 광범위한 액세스가 핵심 기능인 몇 가지 경우입니다. 메모 앱, Downloads 폴더의 파일을 여는 PDF 뷰어, 또는 “내보내기 파일을 저장하기만 하면 되는” 앱은 거부됩니다.
Play 외부(기업용 MDM, 사이드로드된 키오스크 기기)로 배포한다면 선택지가 될 수 있습니다. 그렇지 않다면 MediaStore와 SAF를 기반으로 설계하세요.
다른 플러그인과의 연동
| 플러그인 | Scoped storage의 영향 |
|---|---|
@capacitor/filesystem |
앱 디렉터리에서는 문제없습니다. Documents 및 ExternalStorage Android 11+에서는 레거시입니다. requestPermissions() 오래된 Android에서는 해당 디렉터리에만 관련됩니다 |
@capacitor/camera |
시스템 카메라와 Photo Picker를 사용하며, 다음을 제외하고는 저장소 권한이 필요하지 않습니다 saveToGallery Android 9 이하 |
@capacitor/share |
다음으로 공유합니다 FileProvider. 다음에 나열된 경로만 res/xml/file_paths.xml 공유할 수 있습니다 |
@capgo/capacitor-file-sharer |
자체 FileProvider 및 MediaStore를 save(), 별도 설정 불필요 |
@capgo/capacitor-downloader |
앱 외부 저장소에 다운로드한 후 사용 save() 내보내려면 |
@capgo/capacitor-file-picker |
Picker 기반, Android 13+ 권한 불필요 |
@capgo/capacitor-photo-library |
매니페스트에 추가하는 항목 READ_MEDIA_* 전체 라이브러리 읽기용, 위 Play 정책 적용 대상. pickMedia() 시스템 picker를 사용하며 권한이 필요 없으므로, 이것만 사용한다면 제거하세요 READ_MEDIA_* 포함 tools:node="remove" |
어떤 디렉토리를 사용할까
- 앱 데이터, 설정, 데이터베이스:
Directory.Data - 임시 파일 및 공유 내보내기:
Directory.Cache - 재다운로드 가능한 대용량 미디어:
Directory.ExternalAndroid에서,Directory.LibraryNoCloudiOS에서 - 다운로드나 갤러리에 저장할 파일: MediaStore를 통해
FileSharer.save() - 앱으로 가져온 파일: 선택 후 복사
Directory.Data - 앱이 계속 작성할 폴더: SAF 트리 URI 및 영구 권한
완전한 읽기/쓰기 API에 대해, Capacitor 파일 처리 가이드.
기존 앱 마이그레이션
- code에서 검색
Directory.Documents,Directory.ExternalStorage,/storage/emulated, 그리고requestLegacyExternalStorage. - 앱 소유 파일을 다음 위치로 이동하세요:
Directory.Data또는Directory.External. 새 버전을 처음 실행할 때, 아직 읽을 수 있는 기존 파일을 복사한 뒤 삭제하세요. - “기기에 저장” 기능을 다음으로 교체하세요:
FileSharer.save(). - 경로 입력을 피커로 교체하세요.
- 추가
android:maxSdkVersion레거시 스토리지 권한으로 전환하고 제거하세요:READ_MEDIA_*필요하지 않다면. - Android 10, 11, 13, 15 또는 16 기기나 에뮬레이터에서 테스트하세요. 동작은 바로 그 경계에서 달라집니다.
preserveLegacyExternalStorage="true" 플러그인의 <application> 태그는 이를 사용하던 버전에서 업데이트된 앱에 레거시 접근을 유지합니다. requestLegacyExternalStorage, 앱이 재설치될 때까지 유지됩니다. 일회성 마이그레이션 단계에서만 도움이 되므로, 이 기능에 의존하지 마세요.
문제 해결
EACCES (Permission denied) 쓰기 /storage/emulated/0/...: 공유 저장소에 직접 쓰고 있습니다. MediaStore나 앱 디렉토리를 사용하세요.
다음으로 저장한 파일: Directory.Documents 재설치 후 사라짐: 앱의 소유권이 상실되었습니다. 사용자 파일에는 MediaStore를 사용하세요.
FileNotFoundException 파일 읽기 중: content:// 나중에 URI 접근 시: 임시 권한이 만료되었습니다. 파일을 선택할 때 복사하거나, tree URI의 경우 권한을 유지하세요.
사진 및 동영상 권한 관련 Play Console 경고: 플러그인이 추가한 READ_MEDIA_IMAGES 또는 READ_MEDIA_VIDEO. 제거하려면 다음을 사용하세요: tools:node="remove" 필요하지 않다면.
requestPermissions() granted를 반환하지만 쓰기 실패: Android 11+에서는 레거시 스토리지 권한이 granted 상태이지만 공유 스토리지에는 의미가 없습니다.
네이티브와 웹 파트 동기화 유지
스토리지 규칙은 네이티브 설정이므로 매니페스트나 플러그인 변경 시 스토어 릴리스가 필요하며, Capgo 빌드 클라우드에서 생성할 수 있습니다. 관련 파일 로직(경로, 폴더 이름, 내보내기 플로우)은 Live Update로 업데이트 가능한 JavaScript입니다. Capgo Live Update 사용하기.