Flutter에서 파일 시스템에 어떻게 접근하나요?
질문
Flutter 앱에서 파일 시스템에 접근하고 파일을 읽고 쓰는 방법을 설명해주세요.
답변
Flutter 앱에서 파일 시스템에 접근하는 것은 로컬 데이터 저장, 사용자 생성 콘텐츠 관리, 미디어 파일 처리 등 다양한 기능 구현에 필수적입니다. Flutter는 여러 플랫폼(Android, iOS, 웹, 데스크톱)에서 일관된 파일 시스템 접근 방법을 제공합니다.
1. 기본 파일 시스템 접근 방법
1.1 path_provider 패키지 사용하기
Flutter에서 파일 시스템에 접근하기 위한 가장 기본적인 방법은 path_provider
패키지를 사용하는 것입니다:
// pubspec.yaml
dependencies:
path_provider: ^2.1.1
이 패키지는 앱별 디렉토리 경로를 제공하며, 플랫폼에 따라 적절한 위치를 자동으로 선택합니다:
import 'package:path_provider/path_provider.dart';
import 'dart:io';
Future<void> accessFileSystem() async {
// 앱 전용 임시 디렉토리 - 앱 종료 시 시스템에 의해 정리될 수 있음
final tempDir = await getTemporaryDirectory();
print('임시 디렉토리: ${tempDir.path}');
// 앱 전용 문서 디렉토리 - 사용자 데이터 저장에 적합
final appDocDir = await getApplicationDocumentsDirectory();
print('문서 디렉토리: ${appDocDir.path}');
// 앱 지원 디렉토리 - 사용자가 직접 액세스할 필요가 없는 앱 데이터용
final appSupportDir = await getApplicationSupportDirectory();
print('지원 디렉토리: ${appSupportDir.path}');
// 외부 저장소 디렉토리 (Android만 해당)
if (Platform.isAndroid) {
final externalDir = await getExternalStorageDirectory();
print('외부 저장소: ${externalDir?.path}');
}
// iOS 또는 Android에서 공유 디렉토리
final downloadsDir = await getDownloadsDirectory();
print('다운로드 디렉토리: ${downloadsDir?.path}');
}
1.2 기본 파일 작업
dart:io
패키지를 사용하여 기본적인 파일 작업을 수행할 수 있습니다:
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
// 파일 쓰기
Future<File> writeFile(String filename, String content) async {
final directory = await getApplicationDocumentsDirectory();
final file = File(path.join(directory.path, filename));
// 파일에 텍스트 쓰기
return file.writeAsString(content);
}
// 파일 읽기
Future<String> readFile(String filename) async {
try {
final directory = await getApplicationDocumentsDirectory();
final file = File(path.join(directory.path, filename));
// 파일 내용 읽기
String contents = await file.readAsString();
return contents;
} catch (e) {
return "파일을 읽지 못했습니다: $e";
}
}
// 파일 삭제
Future<void> deleteFile(String filename) async {
try {
final directory = await getApplicationDocumentsDirectory();
final file = File(path.join(directory.path, filename));
if (await file.exists()) {
await file.delete();
print('파일이 삭제되었습니다');
} else {
print('파일이 존재하지 않습니다');
}
} catch (e) {
print('파일 삭제 오류: $e');
}
}
// 파일 존재 여부 확인
Future<bool> fileExists(String filename) async {
final directory = await getApplicationDocumentsDirectory();
final file = File(path.join(directory.path, filename));
return file.exists();
}
1.3 디렉토리 작업
// 디렉토리 생성
Future<Directory> createDirectory(String dirName) async {
final baseDir = await getApplicationDocumentsDirectory();
final newDirPath = path.join(baseDir.path, dirName);
final newDir = Directory(newDirPath);
if (await newDir.exists()) {
return newDir;
} else {
return newDir.create(recursive: true);
}
}
// 디렉토리 내 파일 목록 가져오기
Future<List<FileSystemEntity>> listDirectory(String dirName) async {
try {
final baseDir = await getApplicationDocumentsDirectory();
final dirPath = path.join(baseDir.path, dirName);
final dir = Directory(dirPath);
if (await dir.exists()) {
return dir.list().toList();
} else {
return [];
}
} catch (e) {
print('디렉토리 목록 읽기 오류: $e');
return [];
}
}
2. 바이너리 파일 다루기
텍스트 파일 외에도 바이너리 데이터(이미지, 오디오 파일 등)를 처리해야 할 경우가 많습니다:
// 바이너리 파일 쓰기 (예: 이미지 저장)
Future<File> writeBytes(String filename, List<int> bytes) async {
final directory = await getApplicationDocumentsDirectory();
final file = File(path.join(directory.path, filename));
return file.writeAsBytes(bytes);
}
// 바이너리 파일 읽기
Future<List<int>> readBytes(String filename) async {
try {
final directory = await getApplicationDocumentsDirectory();
final file = File(path.join(directory.path, filename));
// 파일의 바이너리 데이터 읽기
List<int> bytes = await file.readAsBytes();
return bytes;
} catch (e) {
print('파일 읽기 오류: $e');
return [];
}
}
// 예제: 네트워크 이미지 저장
Future<File> downloadAndSaveImage(String url, String filename) async {
final http = await HttpClient().getUrl(Uri.parse(url));
final response = await http.close();
final bytes = await consolidateHttpClientResponseBytes(response);
return writeBytes(filename, bytes);
}
3. 스트림을 사용한 파일 접근
대용량 파일이나 점진적 처리가 필요한 경우 스트림을 사용할 수 있습니다:
// 스트림으로 파일 읽기
Future<void> readFileAsStream(String filename) async {
final directory = await getApplicationDocumentsDirectory();
final file = File(path.join(directory.path, filename));
Stream<List<int>> inputStream = file.openRead();
// UTF-8 텍스트 파일 처리 예제
await inputStream
.transform(utf8.decoder) // 바이트를 문자열로 디코딩
.transform(LineSplitter()) // 줄 단위로 분할
.forEach((line) {
print('읽은 줄: $line');
});
}
// 스트림으로 파일 쓰기
Future<void> writeFileAsStream(String filename, Stream<List<int>> data) async {
final directory = await getApplicationDocumentsDirectory();
final file = File(path.join(directory.path, filename));
try {
final sink = file.openWrite();
await sink.addStream(data);
await sink.flush();
await sink.close();
print('파일 쓰기 완료');
} catch (e) {
print('파일 쓰기 오류: $e');
}
}
4. 외부 저장소 접근 (Android)
Android에서는 외부 저장소에 접근할 때 권한이 필요합니다:
4.1 권한 설정
android/app/src/main/AndroidManifest.xml
파일에 다음 권한을 추가합니다:
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
4.2 런타임 권한 요청
Android 6.0(API 23) 이상에서는 런타임에 권한을 요청해야 합니다. permission_handler
패키지를 사용할 수 있습니다:
// pubspec.yaml
dependencies:
permission_handler: ^10.3.0
// 권한 요청 코드
import 'package:permission_handler/permission_handler.dart';
Future<bool> requestStoragePermission() async {
var status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
}
return status.isGranted;
}
// 파일 접근 전 권한 확인
Future<void> accessExternalFile() async {
if (await requestStoragePermission()) {
// 파일 작업 수행
final externalDir = await getExternalStorageDirectory();
print('외부 저장소 경로: ${externalDir?.path}');
} else {
print('저장소 권한이 거부되었습니다');
}
}
5. 파일 선택 및 공유
실제 앱에서는 사용자가 기기의 파일을 선택하거나 다른 앱으로 파일을 공유해야 하는 경우가 많습니다:
5.1 파일 선택기 사용 (file_picker 패키지)
// pubspec.yaml
dependencies:
file_picker: ^5.3.1
// 파일 선택 코드
import 'package:file_picker/file_picker.dart';
Future<void> pickFile() async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
File file = File(result.files.single.path!);
print('선택된 파일: ${file.path}');
// 선택된 파일 읽기
final content = await file.readAsString();
print('파일 내용: $content');
} else {
// 사용자가 파일 선택을 취소함
print('파일 선택이 취소되었습니다');
}
} catch (e) {
print('파일 선택 오류: $e');
}
}
// 여러 파일 선택
Future<void> pickMultipleFiles() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: ['jpg', 'pdf', 'doc'],
);
if (result != null) {
List<File> files = result.paths.map((path) => File(path!)).toList();
for (var file in files) {
print('선택된 파일: ${file.path}');
}
}
}
5.2 파일 공유하기 (share_plus 패키지)
// pubspec.yaml
dependencies:
share_plus: ^7.0.2
// 파일 공유 코드
import 'package:share_plus/share_plus.dart';
Future<void> shareFile(String filePath) async {
try {
await Share.shareXFiles([XFile(filePath)], text: '파일 공유');
} catch (e) {
print('파일 공유 오류: $e');
}
}
// 텍스트 공유
Future<void> shareText(String text) async {
await Share.share(text, subject: '공유 제목');
}
6. 데이터베이스 파일 다루기
SQLite와 같은 데이터베이스를 사용하는 경우, 파일 시스템에 DB 파일을 저장하고 다루어야 합니다:
// pubspec.yaml
dependencies:
sqflite: ^2.3.0
path: ^1.8.3
// 데이터베이스 파일 경로 가져오기
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as path;
Future<String> getDatabasePath() async {
// 데이터베이스 파일을 저장할 경로 가져오기
final databasesPath = await getDatabasesPath();
final dbPath = path.join(databasesPath, 'my_database.db');
return dbPath;
}
// 데이터베이스 백업
Future<File> backupDatabase() async {
final dbPath = await getDatabasePath();
final dbFile = File(dbPath);
// 백업 파일 경로
final docDir = await getApplicationDocumentsDirectory();
final backupPath = path.join(docDir.path, 'backup_${DateTime.now().millisecondsSinceEpoch}.db');
// 데이터베이스 파일 복사
return dbFile.copy(backupPath);
}
// 데이터베이스 복원
Future<void> restoreDatabase(String backupPath) async {
final dbPath = await getDatabasePath();
final backupFile = File(backupPath);
// 데이터베이스가 열려 있다면 닫기
await deleteDatabase(dbPath);
// 백업 파일 복원
await backupFile.copy(dbPath);
print('데이터베이스가 복원되었습니다');
}
7. 앱 번들 애셋 파일 접근하기
앱 번들에 포함된 파일(assets)에 접근하는 방법은 파일 시스템 접근과는 다릅니다:
# pubspec.yaml
flutter:
assets:
- assets/data/
- assets/images/
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
// 텍스트 파일 읽기
Future<String> loadAssetText(String assetPath) async {
return await rootBundle.loadString(assetPath);
}
// JSON 파일 읽기
Future<Map<String, dynamic>> loadAssetJson(String assetPath) async {
String jsonString = await rootBundle.loadString(assetPath);
return json.decode(jsonString);
}
// 바이너리 파일 읽기
Future<ByteData> loadAssetBinary(String assetPath) async {
return await rootBundle.load(assetPath);
}
// 애셋 파일을 앱 문서 디렉토리로 복사
Future<File> copyAssetToDocuments(String assetPath, String filename) async {
final byteData = await rootBundle.load(assetPath);
final buffer = byteData.buffer;
final docDir = await getApplicationDocumentsDirectory();
final file = File(path.join(docDir.path, filename));
return file.writeAsBytes(
buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)
);
}
8. 파일 암호화
민감한 데이터를 저장할 때는, 파일 내용을 암호화하는 것이 좋습니다:
// pubspec.yaml
dependencies:
encrypt: ^5.0.1
// 파일 암호화 코드
import 'package:encrypt/encrypt.dart';
import 'dart:convert';
class FileEncryption {
final Key key;
final IV iv;
final Encrypter encrypter;
FileEncryption(String keyString) :
key = Key.fromUtf8(keyString.padRight(32, '0')), // 32자리 키
iv = IV.fromLength(16),
encrypter = Encrypter(AES(Key.fromUtf8(keyString.padRight(32, '0'))));
// 파일 암호화
Future<File> encryptFile(File sourceFile, String outputPath) async {
final content = await sourceFile.readAsBytes();
final encrypted = encrypter.encryptBytes(content, iv: iv);
final encryptedFile = File(outputPath);
return encryptedFile.writeAsBytes(encrypted.bytes);
}
// 파일 복호화
Future<File> decryptFile(File encryptedFile, String outputPath) async {
final encryptedContent = await encryptedFile.readAsBytes();
final encrypted = Encrypted(encryptedContent);
final decrypted = encrypter.decryptBytes(encrypted, iv: iv);
final decryptedFile = File(outputPath);
return decryptedFile.writeAsBytes(decrypted);
}
// 문자열 암호화
Future<File> encryptString(String content, String filePath) async {
final encrypted = encrypter.encrypt(content, iv: iv);
final encryptedFile = File(filePath);
return encryptedFile.writeAsString(encrypted.base64);
}
// 암호화된 파일에서 문자열 복호화
Future<String> decryptString(File encryptedFile) async {
final encryptedContent = await encryptedFile.readAsString();
final encrypted = Encrypted.fromBase64(encryptedContent);
return encrypter.decrypt(encrypted, iv: iv);
}
}
9. 캐시 관리
앱에서 생성된 임시 파일을 관리하는 방법:
// 캐시 디렉토리 크기 계산
Future<int> getCacheSize() async {
final cacheDir = await getTemporaryDirectory();
int totalSize = 0;
if (await cacheDir.exists()) {
await for (final FileSystemEntity entity in cacheDir.list(recursive: true)) {
if (entity is File) {
totalSize += await entity.length();
}
}
}
return totalSize;
}
// 캐시 디렉토리 비우기
Future<void> clearCache() async {
final cacheDir = await getTemporaryDirectory();
if (await cacheDir.exists()) {
await for (final FileSystemEntity entity in cacheDir.list()) {
try {
await entity.delete(recursive: true);
} catch (e) {
print('캐시 삭제 오류: $e');
}
}
}
print('캐시가 비워졌습니다');
}
10. 플랫폼별 고려사항
Android 특정 코드
Android 11(API 30) 이상에서는 scoped storage
가 도입되어 파일 시스템 접근이 제한되었습니다:
// Android 11 이상에서 미디어 파일 접근
import 'package:flutter/services.dart';
Future<void> saveImageToGallery(Uint8List imageBytes, String name) async {
if (Platform.isAndroid) {
final result = await const MethodChannel('app/media_store')
.invokeMethod<String>('saveImage', {
'data': imageBytes,
'name': name,
});
print('이미지가 저장되었습니다: $result');
}
}
위 코드를 사용하려면 Android 측에 해당 메서드 채널을 구현해야 합니다.
iOS 특정 코드
iOS에서는 앱 샌드박스 외부 파일에 접근하기 위한 특별한 권한이 필요할 수 있습니다:
<!-- ios/Runner/Info.plist -->
<key>NSPhotoLibraryUsageDescription</key>
<string>사진 라이브러리에 접근하여 이미지를 저장합니다.</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>문서 폴더에 접근하여 파일을 저장합니다.</string>
요약
Flutter에서 파일 시스템에 접근하는 방법은 다음과 같이 요약할 수 있습니다:
기본 경로 접근:
path_provider
패키지를 사용하여 플랫폼별 표준 디렉토리 경로를 가져옵니다.파일 작업:
dart:io
패키지를 사용하여 파일 생성, 읽기, 쓰기, 삭제 등의 기본 작업을 수행합니다.외부 저장소: Android에서는 권한 설정 및 요청이 필요하며,
permission_handler
패키지를 사용할 수 있습니다.파일 선택 및 공유:
file_picker
와share_plus
패키지를 사용하여 사용자 상호작용을 처리합니다.데이터베이스 파일:
sqflite
패키지와 함께 데이터베이스 파일을 관리할 수 있습니다.애셋 파일:
rootBundle
을 사용하여 앱 번들에 포함된 파일에 접근합니다.보안: 필요한 경우
encrypt
와 같은 패키지를 사용하여 파일 내용을 암호화합니다.캐시 관리: 임시 파일의 크기를 모니터링하고 정리하는 방법을 구현합니다.
효율적인 파일 시스템 접근은 앱의 성능, 보안 및 사용자 경험에 직접적인 영향을 미치므로, 적절한 디렉토리 선택 및 에러 처리를 항상 고려해야 합니다.