
현장 조사, 시설 점검, 부동산 실사, 환경 모니터링 같은 업무 앱을 Flutter로 개발할 때 빠지지 않는 요구사항이 있습니다. "현장에서 사진 찍을 때 GPS 좌표와 메모를 같이 저장해 주세요."
쉬워 보이지만 실제로 구현하면 여러 장벽을 만납니다. image_picker로 촬영한 사진에는 Android에서 GPS EXIF가 종종 비어 있고, geolocator로 좌표를 따로 얻어와도 사진 파일에 직접 쓰는 방법이 막막합니다. 게다가 현장 메모까지 동시에 연결해 저장하려면 어느 레이어에서 어떻게 조합할지 설계가 먼저 있어야 합니다.
실제 현장 데이터 수집 앱을 만들면서 이 문제를 직접 파고들었습니다. 이 글에서는 GPS 획득 → EXIF 쓰기 → 메모 DB 저장까지 이어지는 전체 아키텍처와 핵심 코드를 정리합니다.
🔹 1. 현장 앱에서 GPS + 메모 동시 저장이 어려운 이유
Flutter에서 이 기능을 구현할 때 맞닥뜨리는 문제는 크게 세 가지입니다.
image_picker로 카메라를 열어 촬영하면, Android에서는 GPS EXIF 데이터가 비어 있는 경우가 많습니다. 이는 image_picker가 Android의 MediaStore API를 통해 시스템 카메라를 호출하는데, 카메라 앱의 위치 권한과 앱 자체의 위치 권한이 별개로 관리되기 때문입니다. iOS에서는 문제가 없어 개발 중 발견하기 어렵습니다.exif 패키지는 읽기 전용입니다. GPS 좌표를 사진 파일에 직접 기록하려면 native_exif 같은 네이티브 연동 패키지가 필요하며, 접근법도 iOS와 Android가 다릅니다.🔹 2. 전체 아키텍처 설계 — 3레이어 구조
이 문제를 해결하는 핵심은 촬영 → GPS 획득 → EXIF 쓰기 → DB 저장을 하나의 단위 작업(atomic operation)처럼 묶는 것입니다.
[UI Layer] 카메라 버튼 탭
↓
[Service Layer] FieldPhotoService.capture()
├─ ① geolocator → 현재 GPS 좌표 획득
├─ ② image_picker → 카메라 촬영 (JPEG 파일 경로)
├─ ③ native_exif → 사진 파일에 GPS EXIF 쓰기
└─ ④ 파일 앱 전용 디렉토리로 복사 (갤러리 외부)
↓
[Data Layer] FieldPhotoRepository.save()
├─ SQLite: 파일경로 + GPS + 메모 + 타임스탬프 저장
└─ 저장 성공 시 UI에 FieldPhotoRecord 반환
🔹 3. GPS 좌표 획득 — geolocator 실시간 위치
geolocator 패키지를 사용해 촬영 시점의 위치를 획득합니다. 촬영 직전이 아닌 앱 진입 시부터 위치 스트림을 pre-warm해두는 것이 핵심입니다. 현장에서 버튼 누르는 순간 GPS Fix를 기다리면 수 초 딜레이가 생기기 때문입니다.
dependencies:
geolocator: ^13.0.0
image_picker: ^1.1.2
native_exif: ^0.1.2
sqflite: ^2.3.3
path_provider: ^2.1.3
permission_handler: ^11.3.1
import 'package:geolocator/geolocator.dart';
class LocationService {
Position? _lastPosition;
// 앱 진입 시 스트림으로 위치 캐싱
void startWatching() {
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 5, // 5m 이상 이동 시 업데이트
),
).listen((pos) => _lastPosition = pos);
}
// 촬영 시 즉시 반환 (pre-warmed)
Future<Position> getCurrent() async {
if (_lastPosition != null) return _lastPosition!;
return Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
}
}
🔹 4. EXIF에 GPS 쓰기 — native_exif 실전 코드
native_exif 패키지는 JPEG 파일에 GPS 위도·경도를 직접 쓰는 기능을 제공합니다. GPSLatitude, GPSLongitude는 String 타입으로 입력하며, 음수값도 지원하지만 반환 시에는 양수로 돌아오므로 GPSLatitudeRef와 GPSLongitudeRef를 함께 설정해야 합니다.
import 'package:native_exif/native_exif.dart';
Future<void> writeGpsExif(
String filePath, double lat, double lng) async {
final exif = await Exif.fromPath(filePath);
await exif.writeAttributes({
'GPSLatitude': lat.abs().toString(),
'GPSLatitudeRef': lat >= 0 ? 'N' : 'S',
'GPSLongitude': lng.abs().toString(),
'GPSLongitudeRef': lng >= 0 ? 'E' : 'W',
});
await exif.close();
}
ACCESS_MEDIA_LOCATION 권한이 별도로 필요합니다. AndroidManifest.xml에 추가하고 permission_handler로 런타임 요청해야 합니다. 카메라로 직접 촬영해 앱 전용 디렉토리에 저장하는 경우에는 이 권한이 불필요합니다.🔹 5. 메모 + 메타데이터 로컬 저장 — SQLite 연동
EXIF에는 위도·경도 좌표만 담고, 현장 메모, 점검 카테고리, 작성자 정보 등 구조화된 데이터는 sqflite로 SQLite DB에 저장합니다. 파일 경로를 기본키로 양쪽을 연결합니다.
// 테이블 생성
await db.execute('''
CREATE TABLE field_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filepath TEXT UNIQUE NOT NULL,
lat REAL NOT NULL,
lng REAL NOT NULL,
memo TEXT,
category TEXT,
taken_at INTEGER NOT NULL
)
''');
// GPS + 메모 통합 저장 (Service Layer)
Future<void> captureAndSave(String memo, String category) async {
// 1. GPS pre-warm 위치 획득
final pos = await locationService.getCurrent();
// 2. 카메라 촬영
final photo = await ImagePicker().pickImage(source: ImageSource.camera);
if (photo == null) return;
// 3. 앱 전용 경로로 복사
final dir = await getApplicationDocumentsDirectory();
final dest = '${dir.path}/field/${DateTime.now().millisecondsSinceEpoch}.jpg';
await File(photo.path).copy(dest);
// 4. EXIF 쓰기
await writeGpsExif(dest, pos.latitude, pos.longitude);
// 5. DB 저장 (EXIF 성공 후)
await db.insert('field_photos', {
'filepath': dest,
'lat': pos.latitude,
'lng': pos.longitude,
'memo': memo,
'category': category,
'taken_at': DateTime.now().millisecondsSinceEpoch,
});
}
getApplicationDocumentsDirectory() 하위에 저장하면 일반 갤러리 앱에 표시되지 않습니다. 업무용 앱에서 개인 갤러리 오염을 막는 데 중요한 포인트입니다.🔹 6. 실무 함정 — Android GPS 누락 & 권한 처리
현장에서 실제로 부딪히는 문제와 그 해결책입니다.
geolocator의 accuracy를 LocationAccuracy.medium으로 낮추거나, 마지막으로 성공한 위치(lastKnownPosition)를 폴백으로 사용하고 DB에 GPS 신뢰도(accuracy 값)를 함께 기록해두면 나중에 의심 데이터를 필터링할 수 있습니다.permission_handler로 일괄 요청하고, 거부 시 사용자에게 설정 화면으로 이동하는 다이얼로그를 제공해야 현장에서 당황하는 일이 없습니다."해당 배너는 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
❓ 7. 자주 묻는 질문
Q. camera 패키지를 쓰면 image_picker GPS 문제가 없나요?
Flutter 공식 camera 패키지도 마찬가지로 EXIF에 GPS를 자동으로 써주지 않습니다. 결국 geolocator + native_exif 조합으로 직접 써야 합니다. 다만 camera 패키지는 프리뷰와 UI 커스터마이징이 자유롭다는 장점이 있습니다. 더 나아가 camera_with_gps 패키지는 카메라 UI를 열면서 GPS 좌표를 EXIF에 자동 임베드하는 기능을 통합 제공합니다.
Q. 오프라인 현장에서 나중에 서버에 업로드하려면 어떻게 하나요?
SQLite에 sync_status(pending/synced) 컬럼을 추가하고, 네트워크 연결 복구 시 pending 레코드를 서버로 전송하는 백그라운드 큐 방식이 일반적입니다. connectivity_plus 패키지로 연결 상태를 감지하고 자동 업로드를 트리거하면 됩니다.
Q. EXIF에 메모를 넣으면 안 되나요?
EXIF의 ImageDescription이나 UserComment 필드에 짧은 메모를 넣는 것은 가능합니다. 그러나 길이 제한이 있고, 검색·필터링이 어려우며, 메모 수정 시 원본 파일을 재쓰기해야 합니다. 짧은 주석 외의 구조화된 데이터는 SQLite 관리가 훨씬 유연합니다.
📋 핵심 요약 — 현장 사진 저장 아키텍처
- geolocator로 앱 시작 시 위치 스트림 pre-warm
- image_picker로 촬영 → 앱 전용 디렉토리에 복사
- native_exif로 JPEG 파일에 GPS 좌표 직접 쓰기
- sqflite에 파일 경로·GPS·메모·카테고리 통합 저장
- Android GPS 누락은 시스템 카메라 의존 버리고 앱 레벨에서 직접 처리
현장 데이터 수집 앱의 핵심 가치는 신뢰성입니다. 사진은 찍혔는데 위치가 없거나, 메모는 있는데 사진과 연결이 끊어진 데이터는 현장에서는 발견하기도 어렵습니다. GPS 획득 → EXIF 쓰기 → DB 저장을 하나의 원자적 흐름으로 묶고, 실패 시 롤백하는 구조를 처음부터 갖추는 것이 장기적으로 훨씬 안전한 선택입니다.
'IT 관련' 카테고리의 다른 글
| 스마트폰 화면이 아니라 ‘바닥’에 대화창이? 2세대 AI 웨어러블 디바이스가 바꿀 우리의 일상 (0) | 2026.06.10 |
|---|---|
| Google I/O 2026 핵심 분석 | "질문하는 AI"에서 "실행하는 AI"로의 전환 (0) | 2026.06.10 |
| VS Code·Cursor "Extension Host Terminated" 완전 해결 | 원인 분석부터 단계별 조치까지 (0) | 2026.06.08 |
| Debian 12 한글 입력기 Fcitx5 완벽 설치 가이드 | 한영전환 오류 완전 해결 (0) | 2026.06.07 |
| 2026 부산 창업·중소기업 지원 정책: 자금부터 DX까지 한눈에 (0) | 2026.06.06 |