본문 바로가기

Capacitor 앱에서 사진 찍고 편집하기

Capacitor 앱에서 Camera 8 API로 사진 촬영 및 편집: 캡처, 갤러리 선택, 앱 내 크롭, 라이브 미리보기, 압축, EXIF, 업로드.

Capacitor 앱에서 카메라로 사진 촬영 및 편집하기

Capacitor 앱에서 사진을 촬영하고 편집하려면 @capacitor/camera 8.1 이상: Camera.takePhoto() 카메라를 열고, editable: 'in-app' 자르기 및 회전 단계를 추가하고, chooseFromGallery() 시스템 피커로 기존 사진을 선택합니다. 직접 제어하는 커스텀 카메라 화면을 만들려면 사용하세요 @capgo/camera-preview. 이 가이드에서는 캡처, 편집, 권한, 압축, 메타데이터, 저장 및 업로드와 함께 Capacitor 8용 code도 다룹니다.

적합한 도구 선택하기

필요 기능 플러그인 이유
시스템 카메라 UI로 사진 찍기 @capacitor/camera takePhoto 커스텀 UI 유지보수 불필요, 어디서나 작동
사용자가 자르거나 회전할 수 있음 @capacitor/camera editable, editPhoto, editURIPhoto iOS 및 Android에 내장된 인앱 에디터
사진 또는 동영상 하나 이상 선택 @capacitor/camera chooseFromGallery Android Photo Picker 및 PHPicker 사용, 광범위한 권한 불필요
커스텀 카메라 화면 (오버레이, 신분증 프레임, 얼굴 가이드) @capgo/camera-preview HTML 및 JS로 제어하는 네이티브 프리뷰
자체 그리드에서 전체 라이브러리 탐색 @capgo/capacitor-photo-library 썸네일과 함께 페이지 단위로 라이브러리 액세스
축소, 크기 조정 또는 HEIC 변환 @capgo/capacitor-file-compressor 네이티브 인코더, EXIF 제거
대용량 사진을 안정적으로 업로드 @capgo/capacitor-uploader 진행률이 표시되는 네이티브 백그라운드 업로드

Camera plugin 설치 및 설정

bun add @capacitor/camera
bunx cap sync

8.1 이상 버전인지 확인하세요. 이 가이드의 메서드는 (takePhoto, chooseFromGallery, editPhoto, editURIPhoto)는 8.1에서 추가되었고, CameraErrorCode 8.2에서 추가되었습니다. 이전의 getPhoto 및 pickImages 는 비추천(deprecated) 상태입니다.

iOS

에 다음 키를 추가하세요. ios/App/App/Info.plist. Apple은 모호한 문자열이 포함된 빌드를 거부하므로, 실제 이유를 명확히 설명해야 합니다:

<key>NSCameraUsageDescription</key>
<string>Take a photo for your profile or attach it to a report.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose existing photos to attach.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save photos you take to your library.</string>

Android

촬영 및 갤러리 선택에는 권한이 필요하지 않습니다. 이 플러그인은 시스템 카메라 액티비티를 시작하고, Android 11+ 기기에서 Android Photo Picker를 사용하며, 지원되지 않는 경우 ACTION_OPEN_DOCUMENT 로 대체합니다. Google Play 서비스가 있는 구형 기기에서 백포트된 피커를 사용하려면 <application> 에서 AndroidManifest.xml안에 다음을 추가하세요. 이 스니펫은 tools 네임스페이스를 사용하므로, 아직 없다면 xmlns:tools="http://schemas.android.com/tools" 를 루트 <manifest> 요소에 추가하세요 (새로운 Capacitor 프로젝트는 xmlns:android):

<service android:name="com.google.android.gms.metadata.ModuleDependencies"
    android:enabled="false"
    android:exported="false"
    tools:ignore="MissingClass">
    <intent-filter>
        <action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
    </intent-filter>
    <meta-data android:name="photopicker_activity:0:required" android:value="" />
</service>

만 saveToGallery: true 만 선언합니다). 저장 권한은 구형 Android 버전에서만 필요합니다:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

추가하는 것을 피하세요 READ_MEDIA_IMAGES 앱의 핵심 목적이 갤러리나 에디터가 아닌 한. Google Play의 사진 및 동영상 권한 정책은 가끔 사진이 필요한 앱이 피커를 사용하도록 요구하며, 정당한 이유 없이 권한을 선언한 빌드는 거부됩니다.

사진 촬영

import { Camera, CameraDirection, CameraErrorCode, EncodingType } from '@capacitor/camera';

export async function takePicture() {
  try {
    const photo = await Camera.takePhoto({
      quality: 85,
      targetWidth: 2048,
      targetHeight: 2048,
      correctOrientation: true,
      encodingType: EncodingType.JPEG,
      cameraDirection: CameraDirection.Rear, // iOS and web
      includeMetadata: true,
    });

    // webPath works directly in <img src>
    document.querySelector<HTMLImageElement>('#preview')!.src = photo.webPath!;
    return photo;
  } catch (e: any) {
    if (e.code === CameraErrorCode.TakePhotoCancelled) return null;
    if (e.code === CameraErrorCode.CameraPermissionDenied) {
      // show a screen that explains how to enable the camera in Settings
      return null;
    }
    throw e;
  }
}

반환되는 값은 MediaResult:

  • uri: 전체 이미지의 네이티브 파일 URI (웹에서는 해당 없음). Filesystem, 업로더 또는 editURIPhoto.
  • webPath: WebView에서 로드할 수 있는 URL. base64 대신 미리보기에 사용하세요.
  • thumbnail: 네이티브에서는 base64 썸네일, 웹에서는 전체 이미지.
  • metadata: 크기, 형식, 해상도, 생성 날짜 및 includeMetadata 이 true일 때의 EXIF.
  • saved: saveToGallery 성공 여부.

targetWidth 및 targetHeight 함께 설정해야 합니다. 2048픽셀 이미지만 필요한데 12MP 사진을 업로드하는 것을 방지하는 가장 저렴한 방법입니다.

사진 편집: 자르기 및 회전

촬영 직후 편집

설정 editable 그리고 promise가 resolve되기 전에 사용자가 에디터로 이동합니다:

const photo = await Camera.takePhoto({
  quality: 85,
  editable: 'in-app', // 'in-app' | 'external' | 'no'
});

'external' Android에서는 이미지가 다른 설치된 에디터(예: Google Photos)로 전달되며, 실패 시 앱 내 에디터로 폴백됩니다. iOS는 외부 편집을 지원하지 않아 항상 'in-app'.

기존 사진 편집

선택한 사진이나 이전에 저장한 사진을 편집하려면 해당 URI를 전달하세요:

const edited = await Camera.editURIPhoto({
  uri: photo.uri!,
  saveToGallery: false,
  includeMetadata: false,
});

base64만 있는 경우(예: canvas 또는 서버 응답)에는 editPhoto를 사용하세요. 입력값은 data: prefix:

const { outputImage } = await Camera.editPhoto({ inputImage: base64 });
img.src = `data:image/jpeg;base64,${outputImage}`;

웹에서는 두 메서드 모두 사용할 수 없습니다. 필터, 스티커, 텍스트 또는 자유로운 드로잉이 필요하면 내장 에디터만으로는 부족합니다. 이미지를 <canvas> 에 렌더링하여 거기서 편집하거나, 아바타와 같은 고정 비율을 위해 Cropper.js 같은 JavaScript 크로퍼를 사용하세요. canvas export(canvas.toBlob(cb, 'image/jpeg', 0.85))는 직접 업로드할 수 있는 Blob를 제공합니다.

import { Camera, MediaTypeSelection } from '@capacitor/camera';

const { results } = await Camera.chooseFromGallery({
  mediaType: MediaTypeSelection.Photo,
  allowMultipleSelection: true,
  limit: 5,            // Android 13+ and iOS
  quality: 85,
  targetWidth: 2048,
  targetHeight: 2048,
});

const urls = results.map((r) => r.webPath);

단일 사진의 경우, allowMultipleSelection: false 및 editable: 'in-app' 하나의 호출로 선택 후 크롭을 제공합니다. mediaType: MediaTypeSelection.All 를 설정하여 비디오도 허용합니다.

iOS에서는 사용자가 제한된 접근 권한을 부여할 수 있습니다. pickLimitedLibraryPhotos() 를 통해 선택을 변경하고 getLimitedLibraryPhotos() 는 표시되는 내용을 반환합니다. 시스템 선택기를 사용하면 선택된 항목이 항상 읽을 수 있으므로 둘 다 거의 필요하지 않습니다.

앱이 자체 UI 내에서 사용자 라이브러리를 표시하는 경우(사진 인쇄 앱, 백업 앱 등)에는 @capgo/capacitor-photo-library:

import { PhotoLibrary } from '@capgo/capacitor-photo-library';

const { state } = await PhotoLibrary.requestAuthorization();
if (state === 'authorized' || state === 'limited') {
  const { assets, hasMore } = await PhotoLibrary.getLibrary({
    limit: 60,
    offset: 0,
    thumbnailWidth: 256,
    thumbnailHeight: 256,
  });
  // assets[i].thumbnail?.webPath for the grid, getPhotoUrl({ id }) for full size
}

같은 플러그인의 pickMedia() 는 시스템 선택기를 열고 선택된 항목을 캐시로 복사합니다. 두 플로우 모두에 하나의 플러그인을 사용하려면 이 기능을 활용하세요. Android에서 전체 라이브러리 접근을 요청하기 전에 위의 Google Play 정책을 기억하세요.

커스텀 카메라 화면 만들기

시스템 카메라 UI로는 신분증 프레임, 문서 윤곽선, 또는 브랜드 로고를 표시할 수 없습니다. @capgo/camera-preview 네이티브 카메라 피드를 렌더링하고 HTML로 UI를 그릴 수 있습니다:

bun add @capgo/camera-preview
bunx cap sync
import { CameraPreview } from '@capgo/camera-preview';

await CameraPreview.start({
  position: 'rear',
  toBack: true,          // preview behind the WebView
  aspectRatio: '4:3',
  storeToFile: true,     // capture() returns a file path instead of base64
  disableAudio: true,
});

await CameraPreview.setFlashMode({ flashMode: 'auto' });
await CameraPreview.setZoom({ level: 2 });

const { value: filePath, exif } = await CameraPreview.capture({ quality: 85 });

await CameraPreview.stop();

사용하면 toBack: true, 미리보기는 WebView 뒤에 위치하므로 카메라가 활성화된 동안 페이지와 모든 부모 요소의 배경이 투명해야 합니다. 일반적인 패턴은 camera-active 에 body 클래스를 적용하여 background: transparent 을 설정하고 앱의 나머지 부분을 숨기는 것입니다. 그리고 NSCameraUsageDescription 을 iOS에 추가하고, Android에서는 플러그인이 CAMERA 을 통해 런타임 권한을 요청합니다. requestPermissions(). 항상 stop() 을 화면이 닫힐 때 호출하여 카메라를 해제하세요.

표시, 압축, 변환

사용 webPath (또는 Capacitor.convertFileSrc(path) 다른 플러그인 경로)에서 이미지를 표시할 때. base64를 피하세요. <img> 태그를 풀사이즈 사진에 사용하지 마세요: 4MB JPEG가 JS 메모리에서 5.3MB 문자열이 되어 브리지를 건넙니다.

업로드 전에 이미지를 축소하세요:

import { FileCompressor } from '@capgo/capacitor-file-compressor';

const { path } = await FileCompressor.compressImage({
  path: photo.uri!,
  width: 1600,          // keeps aspect ratio
  quality: 0.75,
  mimeType: 'image/jpeg',
});

압축기는 iPhone의 HEIC 입력을 받아들이므로, HEIC를 읽을 수 없는 백엔드를 위해 같은 호출로 HEIC를 JPEG로 변환합니다. 결과는 임시 디렉토리에 기록되므로 @capacitor/filesystem 보관해야 한다면 이동하세요.

EXIF 메타데이터와 위치 개인정보 보호

사용하면 includeMetadata: true, metadata.exif 사진의 EXIF 블록을 포함합니다. 카메라 롤의 사진에는 GPS 좌표가 포함될 수 있습니다. 두 가지 규칙으로 문제를 피하세요:

  1. 공개되는 사진(프로필 사진, 마켓플레이스 리스팅)을 업로드하기 전에 위치 정보를 제거하세요. @capgo/capacitor-file-compressor 압축 과정의 일부로 모든 플랫폼에서 EXIF를 제거합니다.
  2. 위치 정보(현장 보고서, 점검 등)가 필요하면 압축 전에 EXIF에서 위치를 읽고 별도 필드로 전송하세요. 이후 App Store 개인정보 보호 라벨과 Play 데이터 안전성 양식에 이를 명시해야 합니다. 관련 내용은 privacy manifest 가이드에서 확인하세요 iOS 관련 내용은 참고하세요.

saveToGallery: true 켜짐 takePhoto 또는 editURIPhoto 이미지를 카메라 롤에 저장하고 saved 결과에 설정합니다. 사진을 앱 전용으로 유지하려면 앱 저장소로 복사하세요:

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

await Filesystem.copy({
  from: photo.uri!,
  to: `photos/${Date.now()}.jpg`,
  toDirectory: Directory.Data,
});

캐시 디렉터리의 파일은 OS가 언제든 삭제할 수 있으므로, 이러한 파일에 대한 장기 참조를 저장하지 마세요.

사진 업로드하기

작은 사진의 경우, fetch 포함 FormData 이면 충분합니다:

const blob = await fetch(photo.webPath!).then((r) => r.blob());
const form = new FormData();
form.append('file', blob, 'photo.jpg');
await fetch('https://api.example.com/photos', { method: 'POST', body: form });

대용량 파일이나 불안정한 네트워크 환경에서는 앱이 백그라운드로 전환되어도 업로드를 계속하는 네이티브 업로더를 사용하세요:

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

const { id } = await Uploader.startUpload({
  filePath: path,
  serverUrl: 'https://api.example.com/photos',
  method: 'POST',
  mimeType: 'image/jpeg',
});

await Uploader.addListener('events', (event) => {
  console.log(event.name, event.payload);
});

예시: 프로필 사진 플로우

  1. “사진 찍기”와 “라이브러리에서 선택”이 포함된 액션 시트를 표시하세요.
  2. 호출하세요 takePhoto 또는 chooseFromGallery 포함 editable: 'in-app', targetWidth: 1024, targetHeight: 1024.
  3. 1:1 비율을 보장해야 한다면 canvas에서 정사각형으로 자르세요.
  4. 를 사용하여 압축하고 quality: 0.8 EXIF도 제거됩니다.
  5. 업로드한 뒤 로컬 파일이 아닌 서버 URL을 표시하세요. 그래야 재설치해도 아바타가 유지됩니다.

문제 해결

iOS에서 카메라를 열 때 앱이 충돌합니다: usage description 키가 누락되었습니다 Info.plist.

Android에서 사진이 회전되어 나옵니다: 설정 correctOrientation: true (기본값)이며, EXIF 방향을 적용하지 않고 원시 픽셀을 읽지 않도록 하세요.

저메모리 Android 기기에서 사진 촬영 후 앱이 재시작되는 문제: 카메라가 열려 있는 동안 Android가 액티비티를 종료할 수 있습니다. 다음을 청취하세요. appRestoredResult 과 @capacitor/app 재시작 후 결과를 복구하려면.

webPath 로드되지 않습니다: 저장한 webPath 그리고 재시작 후에 재사용했습니다. 영구 저장하려면 uri 또는 Filesystem 경로를 사용하고 다시 변환하려면 Capacitor.convertFileSrc.

카메라 미리보기가 검은 화면으로 나옵니다: WebView 위 요소에 여전히 배경색이 설정되어 있거나 start() 권한이 부여되기 전에 호출되었습니다.

Play Console이 사진 권한 문제로 빌드를 거부합니다: 제거하세요 READ_MEDIA_IMAGES 및 READ_MEDIA_VIDEO 앱에 광범위한 접근이 필요하다는 신고서를 제출한 경우가 아니라면

스토어 출시 없이 수정 사항 배포하기

이미지 크기, 압축 수준, 크롭 비율, 업로드 엔드포인트는 모두 웹 code에 있습니다. Capgo Live Update 사용하기 리뷰 대기 없이 모든 사용자에게 즉시 적용할 수 있으며, 새 플러그인 같은 네이티브 변경 사항은 일반 빌드를 통해 배포됩니다. Mac 없이 iOS 빌드가 필요하면 Capgo 빌드 2026년 4월부터 App Store Connect에서 요구하는 Xcode 26으로 서명 및 업로드를 처리합니다.

Capacitor 앱용 Live updates

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

블로그 최신 글

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