Android CameraX 완벽 가이드 — Kotlin으로 카메라 미리보기, 사진 촬영, EXIF GPS와 갤러리 저장 구현

Android 앱에서 카메라 기능을 구현하는 방법은 여러 가지입니다.

가장 간단한 방법은 시스템 카메라 앱을 실행하고 촬영 결과를 전달받는 것입니다.

하지만 다음과 같은 기능이 필요하다면 앱 내부에 직접 카메라 화면을 구현해야 할 수 있습니다.

  • 카메라 미리보기 화면 표시

  • 사진 촬영 버튼 직접 구성

  • 전면·후면 카메라 전환

  • 촬영한 사진을 갤러리에 저장

  • 촬영 위치를 GPS 메타데이터에 기록

  • 촬영 후 이미지 미리보기

  • 조류 관찰 기록과 사진 연결

  • 촬영 결과를 SQLite 데이터베이스에 저장

이러한 기능을 구현할 때 활용할 수 있는 라이브러리가 CameraX입니다.

CameraX는 Android Jetpack에서 제공하는 카메라 라이브러리로, Camera2 API를 직접 사용하는 것보다 카메라 생명주기와 기기 호환성을 관리하기 쉽게 만들어 줍니다.

이번 글에서는 Kotlin + XML 기반 Android 앱에서 CameraX를 이용해 사진 촬영 기능을 구현하고, MediaStore와 EXIF를 활용해 촬영 결과를 관리하는 방법을 살펴보겠습니다.

개발 환경: Kotlin, XML ViewBinding, MVVM, Coroutine, CameraX, MediaStore, ExifInterface, ActivityResultContracts. minSdk 24, targetSdk 35~36 기준으로 설명하며 라이브러리 버전은 공식 AndroidX 릴리스를 확인해 적용합니다.


1. CameraX란?

CameraX는 Android Jetpack의 카메라 라이브러리입니다.

카메라 미리보기, 사진 촬영, 영상 녹화, 이미지 분석 등 일반적인 카메라 기능을 Use Case 단위로 제공합니다.

대표적인 Use Case는 다음과 같습니다.

Use Case역할
Preview카메라 미리보기 표시
ImageCapture사진 촬영
ImageAnalysis프레임 분석, ML Kit 연동
VideoCapture동영상 녹화

이번 글에서는 Preview와 ImageCapture를 중심으로 설명합니다.


2. CameraX와 Camera2의 차이

Android에서 카메라 기능을 구현할 때 Camera2 API를 직접 사용할 수도 있습니다.

구분CameraXCamera2
구현 난이도비교적 낮음상대적으로 높음
생명주기 연동지원직접 관리 필요
카메라 미리보기PreviewView 활용Surface 등 직접 구성
사진 촬영ImageCaptureCaptureRequest 등 직접 처리
기기 호환성 처리라이브러리 지원개발자 책임이 큼
세부 카메라 제어일부 제한상대적으로 자유로움

일반적인 사진 촬영 기능이라면 CameraX부터 검토하는 것이 좋습니다.

반면 전문적인 카메라 제어가 필요한 앱에서는 Camera2 API가 적합할 수 있습니다.


프로젝트 설정

3. CameraX 의존성 추가

앱 모듈의 build.gradle.kts에 CameraX 라이브러리를 추가합니다.

dependencies {

    val cameraxVersion = "<version>"

    implementation(
        "androidx.camera:camera-core:$cameraxVersion"
    )

    implementation(
        "androidx.camera:camera-camera2:$cameraxVersion"
    )

    implementation(
        "androidx.camera:camera-lifecycle:$cameraxVersion"
    )

    implementation(
        "androidx.camera:camera-view:$cameraxVersion"
    )

    implementation(
        "androidx.exifinterface:exifinterface:<version>"
    )
}

<version>은 공식 AndroidX 릴리스 페이지에서 확인한 버전으로 변경해야 합니다.

CameraX 관련 라이브러리는 가능한 한 동일한 버전으로 구성하는 것이 좋습니다.


4. AndroidManifest.xml 설정

카메라 권한을 선언합니다.

<uses-permission
    android:name="android.permission.CAMERA" />

<uses-feature
    android:name="android.hardware.camera"
    android:required="false" />

android:required="false"는 카메라가 없는 기기에도 앱 설치를 허용하는 설정입니다.

카메라가 앱의 필수 기능이라면 실제 서비스 요구사항에 따라 required 값을 결정해야 합니다.

사진에 현재 위치를 기록하려면 위치 권한도 검토해야 합니다.

<uses-permission
    android:name="android.permission.ACCESS_COARSE_LOCATION" />

<uses-permission
    android:name="android.permission.ACCESS_FINE_LOCATION" />

단, 위치 정보는 선택적 기능으로 제공하는 것이 좋습니다.

사용자가 위치 권한을 거부해도 기본 사진 촬영은 가능하도록 설계할 수 있습니다.


카메라 화면 구성

5. PreviewView 사용

CameraX에서는 PreviewView를 이용해 카메라 화면을 표시할 수 있습니다.

fragment_camera.xml 예제입니다.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:scaleType="fillCenter" />

    <ImageButton
        android:id="@+id/buttonCapture"
        android:layout_width="72dp"
        android:layout_height="72dp"
        android:layout_gravity="bottom|center_horizontal"
        android:layout_marginBottom="32dp"
        android:contentDescription="사진 촬영"
        android:src="@drawable/ic_camera"
        android:background="@drawable/bg_capture_button" />

    <ImageButton
        android:id="@+id/buttonSwitchCamera"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_gravity="bottom|end"
        android:layout_marginEnd="24dp"
        android:layout_marginBottom="44dp"
        android:contentDescription="카메라 전환"
        android:src="@drawable/ic_switch_camera" />

</FrameLayout>

예제에 사용된 Drawable 리소스는 프로젝트에 맞게 준비해야 합니다.

PreviewView는 화면 비율에 따라 미리보기를 자르거나 확대할 수 있습니다.

따라서 촬영 결과와 화면에 표시되는 구도가 완전히 일치하지 않을 수 있습니다.


카메라 권한 처리

6. ActivityResultContracts 사용

카메라 권한은 런타임에 요청해야 합니다.

Fragment에서는 다음과 같이 구현할 수 있습니다.

private val cameraPermissionLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->

        if (granted) {
            startCamera()
        } else {
            showCameraPermissionMessage()
        }
    }

권한을 확인합니다.

private fun checkCameraPermission() {

    val granted =
        ContextCompat.checkSelfPermission(
            requireContext(),
            Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED

    if (granted) {
        startCamera()
    } else {
        cameraPermissionLauncher.launch(
            Manifest.permission.CAMERA
        )
    }
}

권한이 거부된 경우에는 카메라 기능을 사용할 수 없다는 사실을 안내해야 합니다.

반복적으로 권한 요청 화면을 띄우는 방식은 피하는 것이 좋습니다.


CameraX 초기화

7. ProcessCameraProvider

CameraX에서는 ProcessCameraProvider를 이용해 카메라 Use Case를 생명주기에 연결합니다.

Fragment에서 다음과 같이 구현할 수 있습니다.

private var imageCapture: ImageCapture? = null

private var cameraSelector =
    CameraSelector.DEFAULT_BACK_CAMERA

카메라 시작 함수는 다음과 같습니다.

private fun startCamera() {

    val cameraProviderFuture =
        ProcessCameraProvider.getInstance(
            requireContext()
        )

    cameraProviderFuture.addListener({

        val cameraProvider =
            cameraProviderFuture.get()

        val preview = Preview.Builder()
            .build()
            .also {
                it.surfaceProvider =
                    binding.previewView.surfaceProvider
            }

        imageCapture = ImageCapture.Builder()
            .setCaptureMode(
                ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
            )
            .build()

        try {

            cameraProvider.unbindAll()

            cameraProvider.bindToLifecycle(
                viewLifecycleOwner,
                cameraSelector,
                preview,
                imageCapture
            )

        } catch (e: Exception) {

            Log.e(
                "CameraX",
                "카메라 초기화 실패",
                e
            )
        }

    }, ContextCompat.getMainExecutor(requireContext()))
}

이 예제는 Fragment의 View 생명주기에 카메라를 연결합니다.

bindToLifecycle()을 사용하면 생명주기에 따라 카메라 리소스가 관리됩니다.

다만 화면을 다시 생성하거나 카메라를 전환할 때는 기존 Use Case와 새로운 Use Case의 연결 상태를 주의해야 합니다.


사진 촬영

8. ImageCapture 사용

사진을 촬영하려면 ImageCapture.takePicture()를 사용합니다.

먼저 저장할 파일을 준비해야 합니다.

CameraX는 파일 또는 MediaStore URI를 대상으로 촬영 결과를 저장할 수 있습니다.

이번에는 MediaStore를 이용해 갤러리에 저장하는 방법을 사용하겠습니다.


9. MediaStore에 촬영 사진 저장

private fun takePhoto() {

    val capture = imageCapture ?: return

    val fileName =
        "BIRD_${System.currentTimeMillis()}.jpg"

    val contentValues = ContentValues().apply {

        put(
            MediaStore.Images.Media.DISPLAY_NAME,
            fileName
        )

        put(
            MediaStore.Images.Media.MIME_TYPE,
            "image/jpeg"
        )

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

            put(
                MediaStore.Images.Media.RELATIVE_PATH,
                "${Environment.DIRECTORY_PICTURES}/BirdCamera"
            )
        }
    }

    val outputOptions =
        ImageCapture.OutputFileOptions.Builder(
            requireContext().contentResolver,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        ).build()

    capture.takePicture(
        outputOptions,
        ContextCompat.getMainExecutor(requireContext()),
        object : ImageCapture.OnImageSavedCallback {

            override fun onImageSaved(
                output: ImageCapture.OutputFileResults
            ) {

                val uri = output.savedUri

                Toast.makeText(
                    requireContext(),
                    "사진 저장 완료",
                    Toast.LENGTH_SHORT
                ).show()

                Log.d(
                    "CameraX",
                    "Saved URI: $uri"
                )
            }

            override fun onError(
                exception: ImageCaptureException
            ) {

                Log.e(
                    "CameraX",
                    "사진 촬영 실패",
                    exception
                )
            }
        }
    )
}

이 예제는 Android 10 이상에서 권장되는 MediaStore 저장 방식에 초점을 맞췄습니다.

Android 9 이하까지 지원한다면 공유 저장소 쓰기 권한과 구버전 저장 방식을 별도로 처리해야 합니다.

또한 실제 프로젝트에서는 Fragment가 종료된 뒤 콜백이 호출될 가능성을 고려해야 합니다.


사진 저장 결과 처리

10. 촬영 완료 후 미리보기

사진 저장이 완료되면 반환된 URI를 이용해 이미지를 표시할 수 있습니다.

예를 들어 다음과 같습니다.

Glide.with(binding.imagePreview)
    .load(savedUri)
    .into(binding.imagePreview)

이렇게 하면 저장된 사진을 다시 Bitmap으로 직접 디코딩하지 않아도 됩니다.

고해상도 이미지를 처리하는 앱에서는 Glide 같은 이미지 로딩 라이브러리를 사용하는 것이 편리합니다.


카메라 전환

11. 전면·후면 카메라 전환

전면과 후면 카메라를 전환하려면 CameraSelector를 변경하고 Use Case를 다시 연결할 수 있습니다.

private fun switchCamera() {

    cameraSelector =
        if (
            cameraSelector ==
            CameraSelector.DEFAULT_BACK_CAMERA
        ) {

            CameraSelector.DEFAULT_FRONT_CAMERA

        } else {

            CameraSelector.DEFAULT_BACK_CAMERA
        }

    startCamera()
}

실제 앱에서는 전면 카메라가 없는 기기도 고려해야 합니다.

카메라 전환 전에 hasCamera()로 사용 가능 여부를 확인하는 것이 좋습니다.


촬영 품질 설정

12. ImageCapture 촬영 모드

ImageCapture에서는 촬영 모드를 설정할 수 있습니다.

대표적으로 다음과 같습니다.

모드특징
CAPTURE_MODE_MINIMIZE_LATENCY촬영 지연 최소화 우선
CAPTURE_MODE_MAXIMIZE_QUALITY이미지 품질 우선

예를 들어 품질을 우선한다면 다음과 같이 설정할 수 있습니다.

imageCapture = ImageCapture.Builder()
    .setCaptureMode(
        ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
    )
    .build()

실제 촬영 품질은 카메라 하드웨어와 기기 제조사의 구현에도 영향을 받습니다.


화면 회전 처리

13. 이미지가 회전되어 저장되는 문제

CameraX를 사용하다 보면 화면 회전과 관련된 문제를 만날 수 있습니다.

예를 들어 다음과 같은 상황입니다.

  • 세로 화면에서 촬영

  • 가로 화면으로 회전

  • 촬영한 이미지를 다시 표시

  • 일부 이미지 뷰어에서 회전 상태가 다르게 보임

사진 파일에는 픽셀 데이터와 EXIF 방향 정보가 함께 사용될 수 있습니다.

따라서 이미지를 직접 처리할 때는 EXIF Orientation을 고려해야 합니다.


14. targetRotation 설정

화면 회전 상태에 맞춰 ImageCapture의 targetRotation을 변경할 수 있습니다.

imageCapture?.targetRotation =
    binding.previewView.display.rotation

다만 화면 회전 감지와 targetRotation 갱신 시점을 적절히 관리해야 합니다.

카메라 센서 방향과 화면 회전은 동일한 개념이 아니므로, 단순히 Bitmap을 90도 회전시키는 방식으로 처리하지 않는 것이 좋습니다.


EXIF 메타데이터

15. 촬영 사진의 EXIF 정보

CameraX로 촬영한 사진에는 기기와 촬영 환경에 따라 EXIF 메타데이터가 포함될 수 있습니다.

대표적인 정보는 다음과 같습니다.

  • 촬영 날짜

  • 카메라 모델

  • 이미지 방향

  • 노출 관련 정보

  • GPS 위치 정보

단, 모든 기기에서 동일한 EXIF 항목이 기록되는 것은 아닙니다.

특히 GPS 위치 정보는 별도로 설정해야 하는 경우가 있습니다.


GPS 위치 기록

16. 위치 정보가 필요한 이유

조류 관찰 기록 앱에서는 촬영한 사진의 위치가 중요할 수 있습니다.

예를 들어 사용자가 특정 장소에서 새를 촬영했다면 다음 정보를 함께 저장할 수 있습니다.

관찰 대상: 참새
촬영 날짜: 2026-10-31
위도: 37.123456
경도: 127.123456
사진 URI: content://...

위 좌표는 설명을 위한 가상 값입니다.

이 정보를 이용하면 관찰 위치를 지도에 표시하거나, 특정 지역에서 촬영한 사진을 검색하는 기능을 구현할 수 있습니다.


17. 위치 권한 요청

위치 정보를 얻으려면 사용자 권한이 필요합니다.

일반적인 위치 요청에서는 다음 권한을 고려합니다.

<uses-permission
    android:name="android.permission.ACCESS_COARSE_LOCATION" />

<uses-permission
    android:name="android.permission.ACCESS_FINE_LOCATION" />

Android 12 이상에서는 사용자가 정확한 위치 대신 대략적인 위치를 허용할 수 있습니다.

따라서 위치 정보를 기록하는 기능은 정확한 위치를 사용할 수 있는지 확인해야 합니다.

GPS 메타데이터가 필요한 사진 촬영에서는 사용자에게 위치 기록 목적을 명확히 안내하는 것이 좋습니다.


18. FusedLocationProviderClient

Google Play services Location을 이용하면 현재 위치를 가져올 수 있습니다.

의존성은 다음과 같습니다.

dependencies {

    implementation(
        "com.google.android.gms:play-services-location:<version>"
    )
}

현재 위치를 요청하는 예제입니다.

@SuppressLint("MissingPermission")
private fun requestCurrentLocation() {

    val client =
        LocationServices.getFusedLocationProviderClient(
            requireActivity()
        )

    client.getCurrentLocation(
        Priority.PRIORITY_HIGH_ACCURACY,
        null
    ).addOnSuccessListener { location ->

        if (location != null) {

            Log.d(
                "Location",
                "Location acquired"
            )
        }
    }
}

이 코드는 위치 권한을 이미 확인한 경우에만 호출해야 합니다.

getCurrentLocation()은 위치를 가져오지 못하면 null을 반환할 수 있습니다.

또한 높은 정확도를 요청하더라도 실제 GPS 정확도가 보장되는 것은 아닙니다.


EXIF GPS 기록

19. CameraX Metadata 사용

CameraX의 ImageCapture는 출력 옵션에 위치 메타데이터를 설정할 수 있습니다.

val metadata = ImageCapture.Metadata().apply {
    location = currentLocation
}

val outputOptions =
    ImageCapture.OutputFileOptions.Builder(
        requireContext().contentResolver,
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        contentValues
    )
    .setMetadata(metadata)
    .build()

여기서 currentLocation은 권한을 확인한 뒤 얻은 Android Location 객체입니다.

촬영 위치를 기록하지 않으려면 location을 설정하지 않으면 됩니다.

위치 메타데이터는 개인정보에 해당할 수 있으므로 사용자가 명확하게 선택할 수 있도록 제공하는 것이 좋습니다.


20. GPS 기록 시점

GPS 위치를 기록할 때는 위치를 언제 측정했는지도 중요합니다.

예를 들어 다음과 같은 상황을 고려해야 합니다.

10:00 현재 위치 획득
10:05 사진 촬영

5분 전에 획득한 위치를 촬영 위치로 기록하면 실제 촬영 장소와 차이가 발생할 수 있습니다.

따라서 다음 항목을 확인하는 것이 좋습니다.

  • 위치 측정 시각

  • 위치 정확도

  • 위치 정보의 최신성

  • 사용자 위치 권한

  • 촬영 시점과의 차이

사진 촬영 위치가 중요한 앱이라면 위치 정확도와 측정 시각을 함께 저장하는 것이 좋습니다.


EXIF GPS 읽기

21. 저장된 사진의 위치 확인

사진을 저장한 뒤 ExifInterface를 이용해 GPS 정보를 확인할 수 있습니다.

fun readPhotoLocation(
    context: Context,
    uri: Uri
): Pair<Double, Double>? {

    context.contentResolver
        .openInputStream(uri)
        ?.use { input ->

            val exif = ExifInterface(input)

            val coordinates = exif.latLong
                ?: return null

            return Pair(
                coordinates[0],
                coordinates[1]
            )
        }

    return null
}

촬영한 사진의 EXIF GPS가 정상적으로 기록되었는지 확인할 때 활용할 수 있습니다.


이미지 회전과 EXIF Orientation

22. Orientation 정보 확인

val orientation =
    exif.getAttributeInt(
        ExifInterface.TAG_ORIENTATION,
        ExifInterface.ORIENTATION_NORMAL
    )

대표적인 방향 정보는 다음과 같습니다.

값의미
ORIENTATION_NORMAL정상 방향
ORIENTATION_ROTATE_9090도 회전
ORIENTATION_ROTATE_180180도 회전
ORIENTATION_ROTATE_270270도 회전

일부 이미지 로딩 라이브러리는 EXIF Orientation을 자동으로 처리합니다.

따라서 Glide를 사용하는 경우에는 먼저 실제 표시 결과를 확인하는 것이 좋습니다.


MVVM 구조에 적용하기

23. CameraRepository

실제 프로젝트에서는 CameraX 초기화와 촬영 처리를 Fragment에서 담당하고, 저장 결과의 관리와 데이터베이스 연동을 Repository로 분리할 수 있습니다.

CameraFragment
    │
    ├── CameraX Preview
    ├── ImageCapture
    │
    ▼
CameraViewModel
    │
    ▼
ObservationRepository
    │
    ├── SQLite
    └── 사진 URI 관리

CameraX는 생명주기와 UI에 밀접하게 연결되므로 모든 카메라 로직을 무조건 Repository에 넣을 필요는 없습니다.

중요한 것은 UI 상태와 영구 저장 데이터를 분리하는 것입니다.


24. 촬영 결과 UI 상태

sealed interface CameraUiState {

    data object Idle : CameraUiState

    data object Capturing : CameraUiState

    data class Success(
        val uri: Uri
    ) : CameraUiState

    data class Error(
        val message: String
    ) : CameraUiState
}

ViewModel에서는 StateFlow를 사용할 수 있습니다.

class CameraViewModel : ViewModel() {

    private val _uiState =
        MutableStateFlow<CameraUiState>(
            CameraUiState.Idle
        )

    val uiState =
        _uiState.asStateFlow()

    fun onCaptureStarted() {
        _uiState.value = CameraUiState.Capturing
    }

    fun onCaptureSuccess(uri: Uri) {
        _uiState.value = CameraUiState.Success(uri)
    }

    fun onCaptureError(message: String) {
        _uiState.value = CameraUiState.Error(message)
    }
}

Fragment에서는 이 상태를 관찰해 촬영 버튼을 비활성화하거나 로딩 표시를 할 수 있습니다.


SQLite 관찰 기록 저장

25. 사진 URI를 DB에 저장하기

조류 관찰 기록 테이블을 다음과 같이 구성할 수 있습니다.

CREATE TABLE bird_observations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    bird_ktsn INTEGER NOT NULL,
    photo_uri TEXT,
    latitude REAL,
    longitude REAL,
    observed_at TEXT NOT NULL
);

사진 자체를 SQLite BLOB으로 저장하는 대신 파일은 MediaStore에 보관하고 URI를 데이터베이스에 저장할 수 있습니다.

다만 저장한 URI가 나중에도 유효한지 확인해야 합니다.

사용자가 갤러리에서 사진을 삭제하면 해당 URI로 파일을 열 수 없게 될 수 있습니다.

따라서 앱에서 사진을 장기간 보관해야 한다면 별도의 파일 보존 정책이 필요합니다.


카메라 촬영과 앱 성능

26. 메인 스레드에서 피해야 할 작업

사진 촬영 후 다음 작업을 메인 스레드에서 처리하지 않는 것이 좋습니다.

  • 고해상도 Bitmap 디코딩

  • 이미지 압축

  • 파일 복사

  • SQLite 대량 데이터 저장

  • EXIF 메타데이터 일괄 처리

  • 서버 업로드

이러한 작업은 Coroutine의 Dispatchers.IO 등을 활용해 백그라운드에서 처리할 수 있습니다.

단, CameraX의 UI 및 생명주기 관련 작업은 적절한 스레드에서 실행해야 합니다.


27. 촬영 버튼 중복 클릭 방지

사용자가 촬영 버튼을 빠르게 여러 번 누르면 중복 촬영이 발생할 수 있습니다.

촬영 중에는 버튼을 비활성화하는 것이 좋습니다.

binding.buttonCapture.isEnabled = false

촬영 완료 또는 오류 발생 시 다시 활성화합니다.

binding.buttonCapture.isEnabled = true

실제 구현에서는 ViewModel의 Capturing 상태와 연동하면 UI 상태를 일관되게 관리하기 쉽습니다.


CameraX 오류 처리

28. 자주 발생하는 오류

CameraX를 사용하는 앱에서는 다음과 같은 오류가 발생할 수 있습니다.

오류 상황확인할 내용
카메라 미리보기 검은 화면권한, 생명주기, Preview 연결
카메라 초기화 실패다른 앱의 카메라 사용 여부
사진 저장 실패저장 공간, URI, MediaStore
전면 카메라 전환 실패카메라 지원 여부
사진 방향 이상targetRotation, EXIF
GPS 정보 없음위치 권한, 위치 획득 실패
앱 종료 후 오류Fragment 생명주기
촬영 지연기기 성능, 촬영 모드

카메라 기능은 기기 제조사와 하드웨어 차이의 영향을 받기 때문에 실제 기기 테스트가 중요합니다.


CameraX 테스트

29. 테스트해야 할 상황

다음 상황을 테스트하는 것이 좋습니다.

  1. 최초 카메라 권한 요청

  2. 카메라 권한 거부

  3. 권한 허용 후 카메라 시작

  4. 사진 촬영

  5. 갤러리 저장

  6. 전면·후면 전환

  7. 화면 회전

  8. 앱 백그라운드 이동

  9. 앱 복귀

  10. 촬영 중 화면 종료

  11. 저장 공간 부족

  12. GPS 권한 거부

  13. GPS 정보 획득 실패

  14. EXIF 정보 확인

  15. 여러 번 연속 촬영

특히 Android 13~16 기기에서 권한과 저장소 동작을 확인하는 것이 좋습니다.


조류 도감 앱 적용 사례

30. 관찰 기록과 카메라 연결

조류 도감 앱에서 다음과 같은 흐름을 구현할 수 있습니다.

조류 상세 화면
       │
       ▼
관찰 기록 추가
       │
       ▼
카메라 실행
       │
       ▼
사진 촬영
       │
       ▼
MediaStore 저장
       │
       ▼
EXIF GPS 확인
       │
       ▼
관찰 기록 SQLite 저장
       │
       ▼
관찰 기록 목록 표시

이 구조에서는 사용자가 특정 조류의 관찰 기록을 사진과 함께 남길 수 있습니다.

추후 지도 기능을 추가하면 관찰 위치를 지도에 표시할 수도 있습니다.


드론 사진 관리 앱 적용 사례

31. 외부 촬영 사진 활용

CameraX는 Android 기기의 카메라를 제어하는 라이브러리입니다.

따라서 DJI 드론 카메라를 CameraX로 직접 제어하는 용도로 사용할 수는 없습니다.

하지만 드론으로 촬영한 사진을 Android 앱에 불러와 다음과 같은 작업을 수행할 수 있습니다.

  • Photo Picker로 드론 사진 선택

  • EXIF GPS 정보 읽기

  • 촬영 위치 지도 표시

  • 사진 목록 관리

  • GPS 좌표 CSV 내보내기

  • 촬영 날짜별 사진 분류

이러한 기능은 CameraX보다 Photo Picker, ExifInterface, MediaStore와 지도 SDK를 조합하는 방식이 적합합니다.


CameraX 개발 체크리스트

32. 출시 전 점검

  • CameraX 라이브러리 추가

  • CAMERA 권한 선언

  • 런타임 권한 처리

  • 카메라 없는 기기 대응

  • PreviewView 설정

  • ProcessCameraProvider 연결

  • LifecycleOwner 연동

  • ImageCapture 구현

  • MediaStore 저장

  • 저장 실패 처리

  • 촬영 버튼 중복 클릭 방지

  • 전면·후면 카메라 전환

  • 화면 회전 처리

  • EXIF Orientation 확인

  • GPS 권한 처리

  • GPS 정보 최신성 확인

  • 위치 기록 선택 기능

  • 촬영 결과 미리보기

  • SQLite 관찰 기록 연동

  • URI 유효성 확인

  • 백그라운드 작업 분리

  • 앱 생명주기 테스트

  • 실제 기기 테스트

  • 개인정보처리방침 검토

  • Google Play Data Safety 확인


33. 마무리

CameraX는 Android 앱에서 카메라 기능을 구현할 때 유용한 Jetpack 라이브러리입니다.

Camera2 API를 직접 사용하는 것보다 일반적인 사진 촬영 기능을 비교적 간단하게 구현할 수 있습니다.

이번 글의 핵심을 정리하면 다음과 같습니다.

  1. CameraX는 Preview, ImageCapture 등 Use Case 기반으로 동작합니다.

  2. PreviewView를 이용해 카메라 미리보기를 표시할 수 있습니다.

  3. 카메라 권한은 ActivityResultContracts로 요청할 수 있습니다.

  4. ProcessCameraProvider를 이용해 생명주기에 카메라를 연결합니다.

  5. ImageCapture로 사진을 촬영할 수 있습니다.

  6. MediaStore를 이용해 촬영 사진을 갤러리에 저장할 수 있습니다.

  7. 전면·후면 카메라 전환은 CameraSelector를 이용합니다.

  8. 사진 방향은 targetRotation과 EXIF 정보를 함께 고려해야 합니다.

  9. GPS 위치 정보는 사용자 권한과 개인정보 보호를 고려해야 합니다.

  10. 촬영 결과는 SQLite 관찰 기록과 연결할 수 있습니다.

조류 도감처럼 사진과 관찰 기록이 중요한 앱에서는 CameraX와 MediaStore를 조합하면 기본적인 촬영 기능을 구현할 수 있습니다.

이후 GPS 위치 기록, 지도 표시, 관찰 기록 검색 기능으로 확장할 수 있습니다.

CameraX의 핵심은 카메라 화면을 구현하는 것뿐 아니라, 촬영 결과를 앱의 데이터와 안전하게 연결하는 것입니다.


참고 자료

태그: Android, Kotlin, CameraX, Android 카메라, PreviewView, ImageCapture, CameraSelector, MediaStore, EXIF, GPS, FusedLocationProviderClient, MVVM, SQLite, 사진 촬영, Android 16, Android 앱 개발