본문 바로가기

[React-Native] 사진 업로드 시 EXIF 위치 정보 자동 추출

@eunyoung-study2026. 1. 29. 15:57

OpenTripPlanner에서 사용자가 업로드한 사진의 EXIF 메타데이터(GPS 위도/경도, 촬영시간)를 서버에서 추출해 DB에 저장하고, 클라이언트에 자동 인식 결과를 보여주도록 연동했다. 핵심 흐름은 "파일 저장 → EXIF 파싱 → DB 저장 → 클라이언트에 결과 반환" !


1. 백엔드: EXIF 추출 구현 핵심

  • exifread로 태그를 파싱하고, GPS DMS(도·분·초)를 십진법으로 변환.
  • 촬영시간은 DateTimeOriginal(우선) 또는 Image DateTime에서 가져온다.
  • 예외가 발생하면 안전하게 None을 반환.

좌표 변환 함수(convert_to_degrees)

  • GPS DMS → 십진변환 함수
def _convert_to_degrees(value) -> Optional[float]:
# EXIF GPS 좌표를 도(degree) 단위로 변환

if not value:
    return None

try:
    # exifread는 GPS 좌표를 \[degrees, minutes, seconds\] 형태의 리스트로 반환
    d = float(value.values\[0\].num) / float(value.values\[0\].den)
    m = float(value.values\[1\].num) / float(value.values\[1\].den)
    s = float(value.values\[2\].num) / float(value.values\[2\].den)
    return d + (m / 60.0) + (s / 3600.0)

except (AttributeError, IndexError, ZeroDivisionError, ValueError):
    return None

메인 추출 함수 (파일 열고 태그 파싱 → lat/lng/taken_at 반환)

  • 실제 EXIF 추출(파일 열고 exifread로 처리 후 lat/lng/taken_at 반환)
def extract_exif_lat_lng_taken_at(file_path: str) -> tuple[Optional[float], Optional[float], Optional[datetime]]:
...
# 이미지 파일에서 EXIF 위치 정보(GPS 위도/경도)와 촬영 시간을 추출

if not os.path.exists(file\_path):
    return None, None, None

try:
    with open(file\_path, 'rb') as f:
        tags = exifread.process\_file(f, details=False)

    # GPS 위도 추출
    lat = None
    lat\_ref = tags.get('GPS GPSLatitudeRef')
    lat\_val = tags.get('GPS GPSLatitude')

    if lat\_val and lat\_ref:
        lat = \_convert\_to\_degrees(lat\_val)
        if lat is not None and str(lat\_ref) == 'S':
            lat = -lat
         ...

2. 백엔드: 업로드 저장·응답 흐름

  • 엔드포인트 /uploads/photos에 multipart/form-data (files)로 업로드.
  • 서버는 LocalStorageService로 파일을 저장한 뒤, 저장 경로로 EXIF 추출 함수를 호출하고 결과를 Photo 모델에 저장.
  • 응답은 각 photo에 exif(있으면 {lat,lng,taken\_at})thumbnail\_url을 포함.

업로드 엔드포인트 호출부

@router.post("/uploads/photos")
async def upload_photos(
    files: list\[UploadFile\] = File(...),
    exif\_required: bool = Form(False),
    db: AsyncSession = Depends(get\_db),
    user=Depends(get\_current\_user),
)
svc = UploadService(db)
storage = LocalStorageService()

try:
    upload, photos = await svc.create\_upload\_with\_photos(user.user\_id, files, exif\_required=exif\_required)

3. 프론트엔드: 이미지 선택 및 업로드

  • expo-image-picker로 갤러리 다중 선택(권한 요청 포함).
  • 선택한 이미지를 FormDatafiles로 append(각 파일에 uri, name, type)하여 api.postFormData('/uploads/photos', formData, { requiresAuth: true }) 호출.
  • 서버 응답의 각 photo에서 exif와 place를 받아 화면에 표시.

FormData 생성 및 API 호출 부분

const formData = new FormData();

// FormData에 이미지 추가
assets.forEach((asset) => {
  const uri = asset.uri;
  const filename = uri.split('/').pop() || 'image.jpg';
  const match = /\\.(\\w+)$/.exec(filename);
  const type = match ? \`image/${match\[1\]}\` : 'image/jpeg';

  formData.append('files', {
    uri: Platform.OS === 'ios' ? uri.replace('file://', '') : uri,
    name: filename,
    type: type,
  } as any);
});

// 백엔드 API 호출
const response = await api.postFormData<{
  upload\_id: string;
  limits: { max\_photos: number };
  photos: Array<{
    photo\_id: string;
    file\_name: string;
    status: 'recognized' | 'needs\_manual';
    exif: ExifData | null;
    place: PlaceData | null;
    thumbnail\_url: string | null;
  }>;
}>('/uploads/photos', formData, {
  requiresAuth: true,
});

프론트엔드 UX 요약

  • EXIF가 있으면 "위치 자동 인식" 배지 표시, 위도/경도와 촬영시간 노출
  • EXIF가 없으면 "위치 정보 없음" 처리 후 사용자에게 수동 입력 모달 제공
  • 업로드 중 전역 로더(FullScreenLoader)로 상태 표시

4. 테스트 방법

  1. 백엔드 실행: storage 디렉터리 준비 및 FastAPI 서버 실행(예: uvicorn app.main:app --reload)
  2. 클라이언트 실행: Expo에서 앱 실행
  3. 사진 업로드:
  • GPS EXIF가 포함된 실제 사진(휴대폰 촬영) 선택 → 업로드 → 응답의 exif 확인
  • EXIF 없는 사진 업로드 → 수동 입력 흐름 확인
  1. DB 확인: Photo 레코드의 exif\_lat, exif\_lng, taken\_at 값 확인

[ 주의사항 및 향후 개선 ]

  • EXIF 시간대: EXIF의 시간은 타임존 정보가 명확하지 않을 수 있으므로 필요 시 보정 필요
  • 개인정보·프라이버시: 위치 정보는 민감 정보이므로 사용자 동의/노출 정책 설계 필요
  • 대량 업로드: 동시 업로드/대용량 이미지에 대한 큐잉·비동기 처리 고려
  • 리버스 지오코딩: 추출한 좌표로 장소 이름을 자동으로 매핑해 UX 개선 가능
  • 보안: 파일 형식 검증, 인증(업로드 권한), 악성 파일 검사 필요
더보기

[ 구현 코드·설치·권한 가이드 (실무 가이드) ]

  • 백엔드: exifread 설치
    • pip install exifread
  • 프론트엔드: expo-image-picker 설치
    • npm install expo-image-picker
  • FormData 업로드(요점)
    • formData.append('files', { uri, name, type }) → api.postFormData('/uploads/photos', formData, { requiresAuth: true })
  • iOS 권한(Info.plist)
    • NSPhotoLibraryUsageDescription / NSCameraUsageDescription / NSPhotoLibraryAddUsageDescription
  • 테스트 체크
    1. 서버 실행(uvicorn) 및 STORAGE_DIR 확인
    2. 실제 사진(위치 EXIF 포함)으로 업로드 → 응답 exif 확인
    3. EXIF 없는 사진은 수동 입력 후 PATCH로 저장 확인
eunyoung-study
@eunyoung-study :: 은영의 이해 노트

개념을 이해하고, 논문을 풀어보고, 코드로 확인하는 기록 ! 오늘도 파이팅 😉

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차