본문 바로가기

Capacitor 파일 핸들링 가이드: 읽기, 쓰기, 선택, 공유

Capacitor 파일 핸들링 가이드: iOS와 Android에서 파일 읽기/쓰기, 디렉토리 선택, 파일 선택, 표시, 열기, 공유, 다운로드, 업로드하는 방법입니다.

Capacitor 모바일 앱에서 파일 읽기, 쓰기, 공유하기

Capacitor 파일 핸들링은 몇 가지 기본 요소로 정리됩니다: @capacitor/filesystem 앱 디렉토리에서 파일 읽고 쓰기, Capacitor.convertFileSrc 파일을 표시하는 방법, 사용자 파일을 가져오는 피커, 다운로드·업로드·열기·공유를 위한 네이티브 플러그인을 다룹니다. 이 가이드는 Capacitor 8 code로 각 기능을 보여주고, iOS와 Android에서 어떤 디렉토리를 사용해야 하는지 설명하며, 대부분의 파일 버그를 일으키는 메모리와 권한 관련 함정도 짚습니다.

파일 핸들링 툴박스

작업 플러그인
앱 스토리지에서 읽기, 쓰기, 목록 조회, 복사, 삭제 @capacitor/filesystem
로컬 파일 표시 <img>, <video>, <iframe> Capacitor.convertFileSrc (핵심)
사용자가 파일 또는 폴더를 선택하도록 허용 @capgo/capacitor-file-picker
진행률 표시 다운로드 @capacitor/file-transfer 또는 @capgo/capacitor-downloader 백그라운드, 일시 중지 가능한 다운로드용
진행률 표시 업로드 @capacitor/file-transfer 또는 @capgo/capacitor-uploader 백그라운드 업로드용
파일 공유 또는 다운로드 폴더에 저장 @capgo/capacitor-file-sharer 또는 @capacitor/share
다른 앱에서 파일 열기 @capacitor-community/file-opener
이미지 압축 @capgo/capacitor-file-compressor
Zip 압축 및 해제 @capgo/capacitor-zip

먼저 코어 플러그인을 설치하세요:

bun add @capacitor/filesystem
bunx cap sync

올바른 디렉토리 선택

모든 Filesystem 호출에는 디렉토리가 필요합니다 directory. 잘못된 디렉토리를 선택하는 것이 파일 유실과 앱 거절의 가장 흔한 원인입니다.

Directory iOS 위치 Android 위치 백업됨 용도
Data Documents 앱 파일 디렉터리 (/data/data/<pkg>/files) iOS iCloud 및 Android 자동 백업 앱이 소유한 사용자 데이터
Documents Documents (...와 동일 Data) 공용 Documents 폴더 지원 Android 11+에서는 지양 (아래 참조)
Library Library 앱 파일 디렉터리 iOS 지원 앱 지원 파일, 데이터베이스
LibraryNoCloud Library/NoCloud 앱 파일 디렉터리 아니요 대용량, 재다운로드 가능한 콘텐츠
Cache Library/Caches 앱 캐시 디렉터리 아니요 OS가 지울 수 있는 임시 파일
External Documents 앱 외부 파일 디렉터리 아니요 Android 대용량 앱 파일, 앱 제거 시 삭제됨
ExternalStorage Documents 공유 저장소 루트 해당 없음 레거시 전용, Android 11 이상에서는 접근 불가

대략적인 원칙:

  • 재다운로드 가능한 대용량 미디어 (오프라인 비디오, 지도 타일)는 LibraryNoCloud에 저장합니다. Apple은 iOS 데이터 저장 가이드라인에 설명된 대로, 재다운로드 가능한 대용량 콘텐츠를 iCloud에 백업되는 폴더에 저장하는 앱을 거부합니다.
  • 임시 파일 (한 번만 공유하는 내보내기 파일, 썸네일)은 Cache에 저장합니다. 사용자 데이터의 유일한 사본은 절대 거기에 저장하지 마세요.
  • 사용자가 나중에 앱 외부에서 찾아야 하는 파일은 앱 디렉토리에 아예 저장해서는 안 됩니다. 공유 시트, MediaStore Downloads 또는 시스템 파일 피커를 통해 저장하세요. 자세한 내용은 Capacitor의 Android scoped storage 가이드를 참조하세요..

파일 쓰기

import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';

// Text file
await Filesystem.writeFile({
  path: 'notes/today.txt',
  data: 'Hello from Capacitor',
  directory: Directory.Data,
  encoding: Encoding.UTF8,
  recursive: true, // create notes/ if missing
});

// JSON
await Filesystem.writeFile({
  path: 'settings.json',
  data: JSON.stringify({ theme: 'dark' }),
  directory: Directory.Data,
  encoding: Encoding.UTF8,
});

// Binary file: pass base64 and no encoding
await Filesystem.writeFile({
  path: 'images/logo.png',
  data: base64Png,
  directory: Directory.Data,
  recursive: true,
});

이 encoding 옵션에 따라 data 해석 방식이 달라집니다. Encoding.UTF8를 사용하면 문자열이 텍스트로 기록됩니다. 사용하지 않으면 네이티브 플랫폼은 data 를 base64로 디코딩해 원시 바이트를 기록합니다. 이걸 놓치면 “PDF가 깨졌다”는 전형적인 버그가 생깁니다.

로그와 같은 기존 파일에 내용을 추가하려면 appendFile 에 동일한 옵션을 사용하세요.

파일 읽기

const { data } = await Filesystem.readFile({
  path: 'settings.json',
  directory: Directory.Data,
  encoding: Encoding.UTF8,
});
const settings = JSON.parse(data as string);

// Binary: returns base64 on native, a Blob on web
const image = await Filesystem.readFile({
  path: 'images/logo.png',
  directory: Directory.Data,
});

파일이 있는지 확인하려면 stat를 사용하세요. 파일이 없으면 reject됩니다:

async function exists(path: string) {
  try {
    await Filesystem.stat({ path, directory: Directory.Data });
    return true;
  } catch {
    return false;
  }
}

대용량 파일을 청크 단위로 읽기

readFile 전체 파일을 하나의 base64 문자열로 반환합니다. 200MB 비디오라면 JS 메모리에 수백 MB의 문자열이 올라갑니다. readFileInChunks 대신 스트리밍합니다:

await Filesystem.readFileInChunks(
  { path: 'videos/clip.mp4', directory: Directory.Data, chunkSize: 1024 * 1024 },
  (chunk, err) => {
    if (err) return console.error(err);
    if (chunk === null) return console.log('done');
    processChunk(chunk.data); // base64 chunk
  },
);

더 좋은 방법은 바이트를 JS로 전혀 넘기지 않는 것입니다. 대부분의 '큰 파일 읽기' 작업은 사실 '업로드'나 '표시'이고, 둘 다 아래에 네이티브 경로가 있습니다.

목록, 복사, 이동, 삭제

const { files } = await Filesystem.readdir({ path: 'notes', directory: Directory.Data });
for (const f of files) console.log(f.name, f.type, f.size, f.mtime);

await Filesystem.mkdir({ path: 'archive', directory: Directory.Data, recursive: true });

await Filesystem.copy({
  from: 'notes/today.txt',
  to: 'archive/today.txt',
  directory: Directory.Data,
});

await Filesystem.rename({
  from: 'archive/today.txt',
  to: 'archive/2026-10-01.txt',
  directory: Directory.Data,
});

await Filesystem.deleteFile({ path: 'notes/today.txt', directory: Directory.Data });
await Filesystem.rmdir({ path: 'notes', directory: Directory.Data, recursive: true });

copy 및 rename 허용 toDirectory 디렉터리 간 이동에 사용합니다. 예를 들어 Cache 대상 Data 다운로드가 완료된 후에 사용합니다. 또한 절대 file:// 경로도 허용합니다. 생략하면 directory, 이는 다른 플러그인에서 반환된 파일을 가져오는 방식입니다.

WebView에서 파일 표시

가능하다면 base64 데이터 URL로 이미지나 동영상을 로드하지 마세요. 네이티브 경로를 Capacitor의 로컬 서버가 제공하는 URL로 변환하세요:

import { Capacitor } from '@capacitor/core';

const { uri } = await Filesystem.getUri({ path: 'images/logo.png', directory: Directory.Data });
img.src = Capacitor.convertFileSrc(uri);

iOS에서는 결과가 다음과 같습니다 capacitor://localhost/_capacitor_file_/..., Android에서는 https://localhost/_capacitor_file_/.... 이는 <img>, <video>, <audio>, 그리고 fetch()에서도 작동하므로, 파일을 ArrayBuffer 로 base64 없이 읽을 수 있습니다:

const buffer = await fetch(Capacitor.convertFileSrc(uri)).then((r) => r.arrayBuffer());

경로(path)를 저장하세요. 변환된 URL은 현재 앱 세션에서만 유효합니다.

기기에서 파일 선택

시스템 파일 선택기는 사용자에게 제어권을 제공하며, Android 13+ 또는 iOS에서는 저장소 권한이 필요하지 않습니다:

bun add @capgo/capacitor-file-picker
bunx cap sync
import { CapgoFilePicker } from '@capgo/capacitor-file-picker';

const { files } = await CapgoFilePicker.pickFiles({
  types: ['application/pdf', 'image/*'],
  limit: 5,
  readData: false, // keep false for anything bigger than a few MB
});

for (const file of files) {
  console.log(file.name, file.mimeType, file.size, file.path);
}

또한 pickImages, pickVideos, pickMedia 및 pickDirectory, 그리고 convertHeicToJpeg iOS의 경우입니다. iOS에서는 types 확장자가 아닌 MIME 타입만 허용됩니다.

선택한 파일은 임시 위치에 있거나 content:// 잠시 동안만 읽을 수 있는 URI에 있을 수 있습니다. 나중에 파일이 필요하다면 즉시 다음 명령어로 본인의 디렉토리에 복사하세요: Filesystem.copy.

파일 다운로드

진행률 표시: @capacitor/file-transfer

Filesystem.downloadFile Filesystem 8에서 deprecated되었습니다. 대체 구현은 응답을 디스크로 바로 스트리밍합니다:

bun add @capacitor/file-transfer
bunx cap sync
import { FileTransfer } from '@capacitor/file-transfer';
import { Directory, Filesystem } from '@capacitor/filesystem';

const { uri } = await Filesystem.getUri({ path: 'downloads/report.pdf', directory: Directory.Data });

const listener = await FileTransfer.addListener('progress', (p) => {
  if (p.lengthComputable) setProgress(p.bytes / p.contentLength);
});

await FileTransfer.downloadFile({
  url: 'https://example.com/report.pdf',
  path: uri,
  progress: true,
  headers: { Authorization: `Bearer ${token}` },
});

await listener.remove();

백그라운드 실행: @capgo/capacitor-downloader

사용자가 앱을 떠나도 계속 다운로드되어야 하고 일시 중지 및 재개가 가능한 대용량 파일에는 Capgo 다운로더를 사용하세요. 이 다운로더는 Android에서는 DownloadManager , iOS에서는 백그라운드 URLSession 에 의존합니다:

import { CapacitorDownloader } from '@capgo/capacitor-downloader';

await CapacitorDownloader.addListener('downloadProgress', ({ id, progress }) => {
  console.log(id, progress);
});
await CapacitorDownloader.addListener('downloadCompleted', ({ id }) => {
  console.log('done', id);
});

await CapacitorDownloader.download({
  id: 'course-video-12',
  url: 'https://cdn.example.com/course/12.mp4',
  destination: 'videos/12.mp4',
  network: 'wifi-only',
  notification: 'progress', // Android
});

상대 경로 destination 는 Android에서는 앱의 외부 파일 디렉터리, iOS에서는 Documents 디렉터리로 해석되며, 이는 Directory.External.

파일 업로드

전송하기 위해 파일을 base64로 읽지 마세요. 두 업로드 옵션 모두 디스크에서 네이티브로 읽습니다:

import { FileTransfer } from '@capacitor/file-transfer';

const result = await FileTransfer.uploadFile({
  url: 'https://api.example.com/upload',
  path: uri,          // full file:// path
  fileKey: 'file',
  mimeType: 'application/pdf',
  headers: { Authorization: `Bearer ${token}` },
  progress: true,
});
console.log(result.responseCode, result.bytesSent);

앱이 백그라운드로 전환되어도 살아남아야 하는 업로드(동영상, 배치 사진 업로드)에는 @capgo/capacitor-uploader를 사용하세요. 백그라운드에서 실행됩니다 URLSession iOS에서는, 그리고 Android에서는 진행률 알림이 있는 업로드 서비스:

import { Uploader } from '@capgo/capacitor-uploader';

const { id } = await Uploader.startUpload({
  filePath: uri,
  serverUrl: 'https://api.example.com/upload',
  method: 'PUT', // binary body, ideal for presigned S3 or R2 URLs
  mimeType: 'video/mp4',
});

Presigned PUT 대용량 파일에는 객체 스토리지로의 Presigned URL이 가장 안정적인 패턴입니다: API가 URL에 서명하고, 디바이스는 스토리지로 바로 업로드합니다.

파일 공유 또는 내보내기

사용자가 파일을 다른 앱으로 보내거나 찾을 수 있는 곳에 저장하게 하려면:

import { FileSharer } from '@capgo/capacitor-file-sharer';

await FileSharer.share({
  filename: 'report.pdf',
  path: uri,
  contentType: 'application/pdf',
});

// Android: write to Downloads via MediaStore. iOS: opens the share sheet ("Save to Files")
await FileSharer.save({
  filename: 'report.pdf',
  path: uri,
  contentType: 'application/pdf',
});

Capgo 파일 공유기는 파일을 자체 캐시 폴더로 복사한 뒤 전용 FileProvider을(를) 통해 제공하므로 수정할 필요가 없습니다 file_paths.xml. 공식 @capacitor/share을(를) 사용하면 Android는 다른 폴더를 추가하지 않는 한 캐시 폴더에서만 공유할 수 있습니다 android/app/src/main/res/xml/file_paths.xml.

다른 앱에서 파일 열기

bun add @capacitor-community/file-opener
bunx cap sync
import { FileOpener } from '@capacitor-community/file-opener';

await FileOpener.open({ filePath: uri, contentType: 'application/pdf' });

iOS에서는 Quick Look 미리보기를 엽니다. Android에서는 MIME 타입의 기본 앱을 열거나 앱 선택기를 표시합니다. 해당 타입을 처리할 앱이 없으면 호출이 거부되므로, catch한 뒤 대신 공유를 제안하세요.

자주 쓰는 파일 레시피

PDF 생성: 로 HTML 렌더링 @capgo/capacitor-pdf-generator 또는 로 JS에서 빌드 pdf-lib, 그런 다음 base64 출력을 로 작성 writeFile. 저희 스캔을 PDF로 변환하는 튜토리얼 전체 흐름을 보여줍니다.

업로드 전 사진에서 GPS 정보 제거: @capgo/capacitor-file-compressor 리사이즈 시 모든 플랫폼에서 EXIF 메타데이터를 제거합니다.

이미지 리사이즈 또는 변환: 동일한 압축기로 너비, 높이, 품질 및 출력 형식(HEIC에서 JPEG 포함)을 처리합니다.

여러 파일 번들링: 폴더를 zip으로 압축하려면 @capgo/capacitor-zip 수백 개의 요청 대신 아카이브 하나를 업로드하세요.

웹 지원

@capacitor/filesystem IndexedDB를 사용해 웹에서도 동작하므로, 개발 중 브라우저에서 code가 실행됩니다. 유의할 차이점: 바이너리 읽기는 a를 반환합니다. Blob파일은 브라우저 프로필에만 존재하고 저장 공간 할당량은 훨씬 낮습니다. 웹 빌드에서는 URL.createObjectURL(blob) 표시용으로 사용하세요.

문제 해결

“파일이 존재하지 않습니다”: 디렉터리를 섞었거나, 절대 경로를 함께 전달했습니다 directory. 둘 중 하나만 사용하세요.

손상된 바이너리 파일: base64를 ...와 함께 작성했습니다 Encoding.UTF8. 인코딩을 제거하세요.

대용량 파일로 앱이 충돌합니다: 브리지를 통해 base64로 전송. 청크, file-transfer 또는 fetch(convertFileSrc(uri)).

Android에서 이미지가 표시되지 않습니다: 다음을 사용했습니다 file:// URL을 직접 사용하지 마세요. 다음을 사용해 변환하세요: Capacitor.convertFileSrc.

Failed to find configured root Android에서 공유할 때: 파일이 다음 경로 밖에 있습니다: file_paths.xml. Capgo 파일 공유 기능을 사용하거나 다음으로 복사하세요: Cache 먼저.

iOS에서 파일이 사라지는 경우: 파일이 다음 위치에 있었습니다: Cache, iOS는 저장 공간이 부족하면 이 경로를 비웁니다.

iCloud 저장소 사용으로 인한 App Store 거절: 다음 위치의 대용량 다운로드 콘텐츠: Data 또는 Documents. 이를 다음으로 이동하세요: LibraryNoCloud.

다음 단계

파일 저장 위치, 재시도 규칙, 네이밍 등 대부분의 파일 로직은 JavaScript에 있으며, 다음으로 조정할 수 있습니다: Capgo Live Update 사용하기 릴리스 후에도 가능합니다. Android의 공유 저장소 및 권한 관련 내용은 다음을 참고하세요: Scoped Storage 가이드, 그리고 파일 선택기 문서 모든 picker 옵션에 대해.

Capacitor 앱용 Live updates

웹 레이어 버그가 라이브 상태라면, 앱 스토어 승인을 며칠씩 기다리지 말고 Capgo를 통해 수정 사항을 배포하세요. 네이티브 변경 사항은 정상 리뷰 절차를 그대로 따르는 반면, 사용자는 백그라운드에서 업데이트를 받습니다.

블로그 최신 글

Capgo는 정말 프로페셔널한 모바일 앱을 만드는 데 필요한 최고의 인사이트를 제공합니다.