SwiftUI PhotosPicker 완벽 가이드 — 사진 선택, 카메라 촬영, 이미지 편집과 파일 저장

iPhone 앱을 개발하다 보면 사용자의 사진을 가져와야 하는 경우가 많습니다.

사진 콜라주 앱에서는 여러 장의 사진을 선택해야 하고, 프로필 관리 앱에서는 사진을 촬영하거나 기존 사진을 선택할 수 있어야 합니다.

생물도감 앱에서는 사용자가 촬영한 사진을 관찰 기록에 첨부하는 기능도 필요합니다.

Android에서는 Photo Picker, ActivityResultContracts, CameraX 등을 이용해 이러한 기능을 구현합니다.

iOS에서는 PhotosPicker, Transferable, UIImagePickerController, Photos 프레임워크 등을 사용할 수 있습니다.

이번 글에서는 SwiftUI를 중심으로 사진 선택부터 카메라 촬영, 간단한 이미지 편집, 파일 저장까지 구현하겠습니다.

1. 이번 글에서 구현할 기능

  • iPhone 사진 라이브러리에서 사진 한 장 선택

  • 여러 장의 사진 선택

  • PhotosPickerItem 데이터 로딩

  • 이미지 선택 중 로딩 표시

  • 카메라 촬영 화면 표시

  • 카메라 권한 처리

  • 이미지 회전

  • 이미지 크기 조절

  • 이미지 자르기

  • 앱 내부 Documents 저장

  • 사진 앱에 편집 이미지 저장

  • EXIF 메타데이터 주의사항

  • 사진 선택 및 저장 오류 처리

개발 환경

  • Xcode

  • Swift

  • SwiftUI

  • iOS 17 이상

  • PhotosUI

  • UIKit

  • Photos

  • ImageIO


2. Android와 iOS 사진 처리 비교

AndroidiOS역할
Android Photo PickerPhotosPicker사진 선택
ActivityResultContractsPhotosPickerItem 등선택 결과 처리
CameraXAVFoundation / UIImagePickerController카메라 촬영
BitmapUIImage / CGImage이미지 처리
MediaStorePhotos사진 라이브러리 저장
앱 내부 파일 저장Documents / Application Support앱 전용 파일 저장
EXIF InterfaceImageIO메타데이터 처리

iOS의 PhotosPicker는 사용자가 선택한 사진에 접근하도록 설계되어 있습니다.

사진을 선택하기 위해 앱 전체 사진 라이브러리에 대한 읽기 권한을 요청할 필요는 없습니다.

이 점은 Android의 시스템 Photo Picker와 비슷합니다.


3. PhotosPicker란?

PhotosPicker는 Apple이 제공하는 시스템 사진 선택 인터페이스입니다.

SwiftUI에서 사진이나 동영상을 선택할 수 있도록 지원합니다.

다음과 같은 장점이 있습니다.

  • 시스템 사진 선택 UI 사용

  • 사진 한 장 또는 여러 장 선택

  • 사진 유형 필터링

  • 선택 결과를 비동기로 로딩

  • 전체 사진 라이브러리 접근 권한 없이 선택한 항목 처리

단, PhotosPicker로 사진을 선택하는 기능과 사진 앱에 이미지를 저장하는 기능은 서로 다른 권한 체계를 사용합니다.


4. PhotosPicker 기본 구현

먼저 PhotosUI를 가져옵니다.

import SwiftUI
import PhotosUI

사진 한 장을 선택하는 기본 코드는 다음과 같습니다.

struct SinglePhotoPickerView: View {

    @State private var selectedItem: PhotosPickerItem?

    var body: some View {

        PhotosPicker(
            selection: $selectedItem,
            matching: .images
        ) {
            Label(
                "사진 선택",
                systemImage: "photo"
            )
        }
    }
}

matching: .images는 이미지 유형의 항목을 선택하도록 제한합니다.

선택 결과는 PhotosPickerItem으로 전달됩니다.


5. PhotosPickerItem에서 이미지 가져오기

사진을 선택한 뒤에는 실제 이미지 데이터를 로딩해야 합니다.

이때 loadTransferable()을 사용할 수 있습니다.

import SwiftUI
import PhotosUI

struct SinglePhotoPickerView: View {

    @State private var selectedItem: PhotosPickerItem?
    @State private var selectedImage: UIImage?
    @State private var isLoading = false

    var body: some View {

        VStack(spacing: 20) {

            if let selectedImage {

                Image(uiImage: selectedImage)
                    .resizable()
                    .scaledToFit()
                    .frame(maxHeight: 300)

            } else {

                ContentUnavailableView(
                    "선택된 사진이 없습니다",
                    systemImage: "photo"
                )
            }

            PhotosPicker(
                selection: $selectedItem,
                matching: .images
            ) {
                Label(
                    "사진 선택",
                    systemImage: "photo.on.rectangle"
                )
            }

            if isLoading {
                ProgressView("사진 불러오는 중...")
            }
        }
        .padding()
        .onChange(of: selectedItem) { _, newItem in

            Task {
                await loadImage(from: newItem)
            }
        }
    }

    @MainActor
    private func loadImage(
        from item: PhotosPickerItem?
    ) async {

        guard let item else {
            selectedImage = nil
            return
        }

        isLoading = true

        defer {
            isLoading = false
        }

        do {

            guard let data = try await item.loadTransferable(
                type: Data.self
            ) else {
                return
            }

            selectedImage = UIImage(data: data)

        } catch {

            print("사진 로딩 실패: \(error)")
        }
    }
}

이 예제에서는 선택한 사진을 Data로 가져온 뒤 UIImage로 변환합니다.

다만 모든 사진 형식과 표현 방식에서 Data 로딩 결과가 동일하게 보장되는 것은 아닙니다.

또한 고해상도 원본을 한꺼번에 메모리에 올리면 메모리 사용량이 증가할 수 있습니다.


6. 여러 장의 사진 선택

사진 콜라주 앱에서는 여러 장의 사진을 선택하는 기능이 중요합니다.

PhotosPicker에서는 선택 결과를 배열로 받을 수 있습니다.

@State private var selectedItems: [PhotosPickerItem] = []

선택 화면은 다음과 같이 구현합니다.

PhotosPicker(
    selection: $selectedItems,
    maxSelectionCount: 9,
    matching: .images
) {
    Label(
        "사진 여러 장 선택",
        systemImage: "square.stack"
    )
}

maxSelectionCount는 사용자가 선택할 수 있는 최대 항목 수를 지정합니다.

이번 예제에서는 최대 9장을 선택하도록 했습니다.


7. 여러 장의 이미지 비동기 로딩

@MainActor
func loadSelectedImages(
    items: [PhotosPickerItem]
) async -> [UIImage] {

    var images: [UIImage] = []

    for item in items {

        do {

            guard let data = try await item.loadTransferable(
                type: Data.self
            ) else {
                continue
            }

            guard let image = UIImage(data: data) else {
                continue
            }

            images.append(image)

        } catch {

            print("이미지 로딩 실패: \(error)")
        }
    }

    return images
}

이 예제는 선택 순서대로 이미지를 로딩합니다.

다만 사진이 많거나 해상도가 높은 경우 모든 UIImage를 배열에 보관하면 메모리 사용량이 커질 수 있습니다.

실제 앱에서는 원본 파일을 임시 저장소에 보관하고, 화면에는 축소된 미리보기 이미지만 유지하는 구조가 더 적합할 수 있습니다.


8. 사진 그리드 화면 만들기

여러 장의 사진을 선택한 뒤에는 그리드 형태로 표시할 수 있습니다.

SwiftUI에서는 LazyVGrid를 사용할 수 있습니다.

import SwiftUI

struct PhotoGridView: View {

    let images: [UIImage]

    private let columns = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    var body: some View {

        ScrollView {

            LazyVGrid(
                columns: columns,
                spacing: 8
            ) {

                ForEach(
                    images.indices,
                    id: \.self
                ) { index in

                    Image(
                        uiImage: images[index]
                    )
                    .resizable()
                    .scaledToFill()
                    .frame(height: 110)
                    .frame(maxWidth: .infinity)
                    .clipped()
                }
            }
            .padding()
        }
    }
}

Android의 RecyclerView GridLayoutManager와 비슷한 용도로 사용할 수 있습니다.

다만 LazyVGrid를 사용하더라도 이미지 원본을 모두 메모리에 보관하면 메모리 문제가 발생할 수 있습니다.


9. 카메라 촬영 구현 방식

iOS에서 카메라 기능을 구현하는 방법은 크게 두 가지입니다.

방식특징
UIImagePickerController시스템 카메라 UI를 간단하게 사용
AVFoundation카메라 미리보기와 촬영 기능을 직접 구성

단순히 사진 한 장을 촬영하는 기능이라면 UIImagePickerController를 활용할 수 있습니다.

반면 카메라 노출, 초점, 실시간 프레임 분석, 커스텀 UI 등이 필요하다면 AVFoundation을 검토하는 것이 좋습니다.

이번 글에서는 구현이 간단한 UIImagePickerController를 사용하겠습니다.


10. 카메라 권한 설정

카메라를 사용하려면 Info.plist에 사용 목적을 설명해야 합니다.

<key>NSCameraUsageDescription</key>
<string>사진을 촬영하여 앱에 첨부하기 위해 카메라를 사용합니다.</string>

이 설명이 누락되면 카메라 접근 시 앱이 종료될 수 있습니다.

사용 목적은 앱의 실제 기능에 맞게 작성해야 합니다.


11. 카메라 권한 요청

카메라 권한 상태는 AVFoundation에서 확인할 수 있습니다.

import AVFoundation

func requestCameraPermission() async -> Bool {

    let status = AVCaptureDevice.authorizationStatus(
        for: .video
    )

    switch status {

    case .authorized:
        return true

    case .notDetermined:

        return await AVCaptureDevice.requestAccess(
            for: .video
        )

    default:
        return false
    }
}

권한이 거부된 경우에는 사용자가 iOS 설정에서 권한을 변경할 수 있도록 안내해야 합니다.

앱이 사용자의 권한을 임의로 변경할 수는 없습니다.


12. UIImagePickerController를 SwiftUI에서 사용하기

UIImagePickerController는 UIKit 컴포넌트입니다.

SwiftUI에서 사용하려면 UIViewControllerRepresentable을 이용할 수 있습니다.

CameraPickerView.swift

import SwiftUI
import UIKit

struct CameraPickerView: UIViewControllerRepresentable {

    @Environment(\.dismiss)
    private var dismiss

    let onImageCaptured: (UIImage) -> Void

    func makeUIViewController(
        context: Context
    ) -> UIImagePickerController {

        let picker = UIImagePickerController()

        picker.sourceType = .camera
        picker.delegate = context.coordinator
        picker.allowsEditing = false

        return picker
    }

    func updateUIViewController(
        _ controller: UIImagePickerController,
        context: Context
    ) {
    }

    func makeCoordinator() -> Coordinator {

        Coordinator(
            parent: self
        )
    }

    final class Coordinator:
        NSObject,
        UIImagePickerControllerDelegate,
        UINavigationControllerDelegate {

        let parent: CameraPickerView

        init(parent: CameraPickerView) {
            self.parent = parent
        }

        func imagePickerController(
            _ picker: UIImagePickerController,
            didFinishPickingMediaWithInfo info:
                [UIImagePickerController.InfoKey: Any]
        ) {

            if let image = info[.originalImage] as? UIImage {
                parent.onImageCaptured(image)
            }

            parent.dismiss()
        }

        func imagePickerControllerDidCancel(
            _ picker: UIImagePickerController
        ) {
            parent.dismiss()
        }
    }
}

이 코드는 시스템 카메라 화면을 SwiftUI에서 표시하기 위한 기본 구현입니다.

카메라가 없는 환경에서는 실행할 수 없으므로 실제 기기 지원 여부를 확인해야 합니다.


13. 카메라 화면 표시

import SwiftUI
import UIKit

struct CameraExampleView: View {

    @State private var capturedImage: UIImage?
    @State private var showingCamera = false
    @State private var showingCameraAlert = false

    var body: some View {

        VStack(spacing: 20) {

            if let capturedImage {

                Image(uiImage: capturedImage)
                    .resizable()
                    .scaledToFit()
                    .frame(maxHeight: 300)
            }

            Button("사진 촬영") {

                Task {

                    guard UIImagePickerController.isSourceTypeAvailable(
                        .camera
                    ) else {

                        showingCameraAlert = true
                        return
                    }

                    let granted = await requestCameraPermission()

                    if granted {
                        showingCamera = true
                    } else {
                        showingCameraAlert = true
                    }
                }
            }
        }
        .sheet(
            isPresented: $showingCamera
        ) {

            CameraPickerView { image in
                capturedImage = image
            }
        }
        .alert(
            "카메라를 사용할 수 없습니다",
            isPresented: $showingCameraAlert
        ) {
            Button("확인", role: .cancel) {}
        } message: {
            Text(
                "카메라가 지원되는 기기인지 확인하고, 필요한 경우 설정에서 카메라 접근을 허용해 주세요."
            )
        }
    }
}

이 예제에서는 카메라 사용 가능 여부와 권한을 확인한 뒤 촬영 화면을 표시합니다.

iPhone Simulator에서는 실제 카메라 촬영 기능을 테스트하기 어려우므로 실제 iPhone에서 확인하는 것이 좋습니다.


14. UIImage 이미지 회전

사진 편집 앱에서는 이미지를 회전하는 기능이 자주 필요합니다.

다음은 UIImage를 90도 회전한 새 이미지로 만드는 예제입니다.

import UIKit

func rotateImage90Degrees(
    _ image: UIImage
) -> UIImage {

    let newSize = CGSize(
        width: image.size.height,
        height: image.size.width
    )

    let renderer = UIGraphicsImageRenderer(
        size: newSize
    )

    return renderer.image { context in

        let cgContext = context.cgContext

        cgContext.translateBy(
            x: newSize.width,
            y: 0
        )

        cgContext.rotate(
            by: .pi / 2
        )

        image.draw(
            in: CGRect(
                origin: .zero,
                size: CGSize(
                    width: image.size.width,
                    height: image.size.height
                )
            )
        )
    }
}

이 코드는 새 비트맵을 생성하므로 원본 이미지가 클수록 메모리 사용량이 증가합니다.

실제 편집 앱에서는 원본 이미지에 편집 명령을 기록하고 최종 저장 시 렌더링하는 방식도 고려할 수 있습니다.


15. 이미지 크기 조절

이미지를 일정한 크기로 축소하려면 UIGraphicsImageRenderer를 사용할 수 있습니다.

import UIKit

func resizeImage(
    _ image: UIImage,
    targetSize: CGSize
) -> UIImage {

    let renderer = UIGraphicsImageRenderer(
        size: targetSize
    )

    return renderer.image { _ in

        image.draw(
            in: CGRect(
                origin: .zero,
                size: targetSize
            )
        )
    }
}

이 함수는 지정된 크기로 이미지를 그립니다.

다만 원본 비율과 targetSize의 비율이 다르면 이미지가 늘어나거나 찌그러질 수 있습니다.

사진의 비율을 유지하려면 별도의 비율 계산이 필요합니다.


16. 이미지 비율을 유지하면서 축소

import UIKit

func resizeImageMaintainingAspectRatio(
    _ image: UIImage,
    maxDimension: CGFloat
) -> UIImage {

    let originalSize = image.size

    let maxOriginalDimension = max(
        originalSize.width,
        originalSize.height
    )

    guard maxOriginalDimension > maxDimension else {
        return image
    }

    let ratio = maxDimension / maxOriginalDimension

    let newSize = CGSize(
        width: originalSize.width * ratio,
        height: originalSize.height * ratio
    )

    let renderer = UIGraphicsImageRenderer(
        size: newSize
    )

    return renderer.image { _ in

        image.draw(
            in: CGRect(
                origin: .zero,
                size: newSize
            )
        )
    }
}

이 함수는 긴 변을 기준으로 이미지를 축소합니다.

예를 들어 긴 변이 4000인 사진에 maxDimension을 1200으로 지정하면 긴 변을 1200으로 줄일 수 있습니다.

단, UIImage의 size는 포인트 단위이므로 정확한 픽셀 크기 관리가 필요한 경우 scale과 cgImage 크기를 함께 고려해야 합니다.


17. 이미지 자르기

이미지에서 특정 영역을 잘라내려면 CGImage를 사용할 수 있습니다.

import UIKit

func cropImage(
    _ image: UIImage,
    rect: CGRect
) -> UIImage? {

    guard let cgImage = image.cgImage else {
        return nil
    }

    guard let cropped = cgImage.cropping(
        to: rect
    ) else {
        return nil
    }

    return UIImage(
        cgImage: cropped,
        scale: image.scale,
        orientation: image.imageOrientation
    )
}

여기서 rect는 화면의 포인트 좌표가 아니라 CGImage의 픽셀 좌표를 기준으로 해석해야 합니다.

또한 원본 이미지의 orientation에 따라 사용자가 보는 방향과 실제 픽셀 배열의 방향이 다를 수 있습니다.

정확한 크롭 기능을 구현하려면 먼저 이미지 방향을 정규화하거나 좌표 변환을 처리해야 합니다.


18. 이미지 편집 상태 관리

사진 편집 앱에서는 원본 이미지와 편집 결과를 구분하는 것이 좋습니다.

예를 들어 다음과 같이 구성할 수 있습니다.

import UIKit
import Observation

@MainActor
@Observable
final class PhotoEditorViewModel {

    var originalImage: UIImage?
    var editedImage: UIImage?

    func setImage(_ image: UIImage) {

        originalImage = image
        editedImage = image
    }

    func rotate() {

        guard let editedImage else {
            return
        }

        self.editedImage = rotateImage90Degrees(
            editedImage
        )
    }

    func reset() {

        editedImage = originalImage
    }
}

이렇게 하면 사용자가 편집을 취소하거나 원본으로 되돌리는 기능을 구현할 수 있습니다.

다만 여러 단계의 실행 취소와 다시 실행을 지원하려면 편집 명령을 별도로 관리하는 구조가 필요합니다.


19. SwiftUI 이미지 편집 화면

import SwiftUI

struct PhotoEditorView: View {

    @State private var viewModel = PhotoEditorViewModel()

    var body: some View {

        VStack(spacing: 20) {

            if let image = viewModel.editedImage {

                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(maxHeight: 400)

            } else {

                ContentUnavailableView(
                    "사진을 선택해 주세요",
                    systemImage: "photo"
                )
            }

            HStack(spacing: 20) {

                Button {
                    viewModel.rotate()
                } label: {

                    Label(
                        "회전",
                        systemImage: "rotate.right"
                    )
                }

                Button {
                    viewModel.reset()
                } label: {

                    Label(
                        "초기화",
                        systemImage: "arrow.counterclockwise"
                    )
                }
            }
        }
        .padding()
    }
}

실제 앱에서는 PhotosPicker에서 선택한 이미지를 viewModel.setImage()로 전달하면 됩니다.


20. 앱 내부에 사진 저장하기

iOS 앱에서 사용자 파일을 저장할 때는 Documents 또는 Application Support 디렉터리를 사용할 수 있습니다.

이번에는 Documents에 JPEG 파일을 저장하겠습니다.

import UIKit

func saveImageToDocuments(
    _ image: UIImage
) throws -> URL {

    guard let data = image.jpegData(
        compressionQuality: 0.9
    ) else {

        throw NSError(
            domain: "ImageSave",
            code: 1,
            userInfo: [
                NSLocalizedDescriptionKey:
                    "이미지를 JPEG로 변환하지 못했습니다."
            ]
        )
    }

    let documentsURL = FileManager.default.urls(
        for: .documentDirectory,
        in: .userDomainMask
    )[0]

    let fileURL = documentsURL.appendingPathComponent(
        UUID().uuidString + ".jpg"
    )

    try data.write(
        to: fileURL,
        options: .atomic
    )

    return fileURL
}

이 함수는 JPEG 데이터를 앱 내부 Documents 디렉터리에 저장합니다.

JPEG는 투명도를 지원하지 않으므로 투명 배경이 필요한 이미지라면 PNG 등 다른 형식을 고려해야 합니다.


21. 저장된 이미지 다시 불러오기

import UIKit

func loadImageFromFile(
    url: URL
) -> UIImage? {

    UIImage(
        contentsOfFile: url.path
    )
}

파일 URL을 SwiftData에 문자열 형태로 저장할 수도 있지만, 앱 컨테이너의 절대 경로를 그대로 장기간 저장하는 것은 피하는 것이 좋습니다.

일반적으로 상대 파일명이나 식별자를 저장한 뒤 현재 앱 디렉터리 경로와 조합하는 방식을 고려할 수 있습니다.


22. 사진 앱에 이미지 저장하기

편집한 이미지를 사용자의 사진 앱에 저장하려면 Photos 프레임워크를 사용할 수 있습니다.

먼저 Info.plist에 다음 항목을 추가합니다.

<key>NSPhotoLibraryAddUsageDescription</key>
<string>편집한 사진을 사진 앱에 저장하기 위해 사용합니다.</string>

저장 함수는 다음과 같습니다.

import Photos
import UIKit

func saveEditedImageToPhotos(
    _ image: UIImage
) async throws {

    let status = await PHPhotoLibrary.requestAuthorization(
        for: .addOnly
    )

    guard status == .authorized else {

        throw NSError(
            domain: "PhotoPermission",
            code: 1,
            userInfo: [
                NSLocalizedDescriptionKey:
                    "사진 저장 권한이 없습니다."
            ]
        )
    }

    try await PHPhotoLibrary.shared().performChanges {

        PHAssetChangeRequest.creationRequestForAsset(
            from: image
        )
    }
}

사용자가 저장 버튼을 누른 시점에 권한을 확인하고 저장을 진행하는 것이 좋습니다.


23. 사진 선택 권한과 저장 권한의 차이

이 부분은 iOS 앱 개발에서 자주 혼동하는 내용입니다.

기능일반적인 권한 요구
PhotosPicker로 사용자가 선택한 사진 가져오기전체 사진 라이브러리 읽기 권한 불필요
사진 앱에 새 사진 추가Add Only 권한
사진 라이브러리 전체 조회사진 라이브러리 접근 권한 필요
카메라 촬영카메라 권한 필요

사진 선택 기능만 필요한데 전체 사진 라이브러리 접근 권한을 요청하면 불필요한 개인정보 접근으로 보일 수 있습니다.

따라서 앱에 필요한 최소한의 권한만 요청하는 것이 좋습니다.


24. EXIF 메타데이터 주의사항

사진에는 다음과 같은 정보가 포함될 수 있습니다.

  • 촬영 날짜

  • 카메라 모델

  • 렌즈 정보

  • ISO

  • 셔터 속도

  • GPS 좌표

  • 이미지 방향

  • 저작권 정보

PhotosPicker에서 Data로 이미지를 가져오더라도 선택된 표현 형식이나 시스템 변환에 따라 원본 파일과 동일한 메타데이터가 유지된다고 보장할 수 없습니다.

또한 UIImage로 디코딩한 뒤 JPEG로 다시 저장하면 원본 EXIF 정보가 사라질 수 있습니다.

따라서 GPS 좌표가 중요한 사진 관리 앱이나 탐조 기록 앱에서는 원본 파일과 편집 이미지를 분리해 관리하는 것이 좋습니다.


25. 원본 사진과 편집 이미지 분리

실제 사진 편집 앱에서는 다음과 같은 구조를 고려할 수 있습니다.

PhotoProject
    │
    ├── original/
    │     └── original_001.heic
    │
    ├── preview/
    │     └── preview_001.jpg
    │
    └── export/
          └── edited_001.jpg

원본 파일은 가능한 한 그대로 보관합니다.

화면에는 축소된 미리보기 이미지를 표시합니다.

사용자가 편집을 완료하면 최종 이미지를 렌더링해 내보냅니다.

이렇게 하면 원본 손상을 방지하고 메모리 사용량을 줄일 수 있습니다.


26. 사진 콜라주 앱으로 확장하기

여러 장의 사진을 하나의 이미지로 합치는 기능은 UIGraphicsImageRenderer로 구현할 수 있습니다.

다음은 두 장의 이미지를 가로로 배치하는 간단한 예제입니다.

import UIKit

func createHorizontalCollage(
    left: UIImage,
    right: UIImage
) -> UIImage {

    let canvasSize = CGSize(
        width: 1200,
        height: 600
    )

    let renderer = UIGraphicsImageRenderer(
        size: canvasSize
    )

    return renderer.image { _ in

        left.draw(
            in: CGRect(
                x: 0,
                y: 0,
                width: 600,
                height: 600
            )
        )

        right.draw(
            in: CGRect(
                x: 600,
                y: 0,
                width: 600,
                height: 600
            )
        )
    }
}

이 예제는 각 이미지를 600×600 영역에 맞춰 그립니다.

원본 사진의 비율이 다르면 이미지가 찌그러질 수 있으므로 실제 콜라주 앱에서는 Aspect Fill 크롭 계산을 추가해야 합니다.


27. 이미지 메모리 최적화

사진 앱에서 가장 주의해야 할 부분 중 하나는 메모리 사용량입니다.

예를 들어 4000×3000 해상도의 사진 한 장을 RGBA 4바이트 픽셀 버퍼로 디코딩하면 대략 다음과 같은 메모리가 필요할 수 있습니다.

4000 × 3000 × 4
= 48,000,000 bytes
≈ 45.8 MiB

사진 10장을 모두 디코딩하면 픽셀 버퍼만 약 458MiB가 될 수 있습니다.

실제 메모리 사용량은 이미지 표현 방식과 추가 버퍼에 따라 달라집니다.

따라서 여러 장의 사진을 다루는 앱에서는 다음 원칙이 중요합니다.

  1. 원본 파일은 디스크에 저장

  2. 화면에는 축소 이미지 사용

  3. 필요한 순간에만 원본 디코딩

  4. 편집 결과 렌더링 후 임시 버퍼 해제

  5. 이미지 캐시 크기 제한

  6. 실제 iPhone에서 메모리 사용량 측정


28. 사진 앱 개발 시 권한 관련 주의사항

App Store 심사에서는 앱이 요청하는 권한과 실제 기능의 관련성이 중요합니다.

예를 들어 사진 선택 기능만 제공하면서 전체 사진 라이브러리 접근 권한을 요청할 필요는 없습니다.

카메라를 사용한다면 카메라 사용 목적을 명확히 설명해야 합니다.

사진을 저장한다면 저장 목적을 설명하는 권한 문구를 설정해야 합니다.

또한 사용자가 선택한 사진을 서버에 업로드하는 경우에는 개인정보처리방침과 실제 데이터 처리 방식을 일치시켜야 합니다.


29. 실전 테스트 체크리스트

  • PhotosPicker 사진 한 장 선택

  • 여러 장 선택

  • 선택 수 제한

  • 사진 로딩 실패 처리

  • 로딩 상태 표시

  • 이미지 그리드 표시

  • 고해상도 사진 테스트

  • HEIC 이미지 테스트

  • 카메라 권한 요청

  • 카메라 권한 거부 처리

  • 실제 iPhone 카메라 촬영

  • 이미지 회전

  • 이미지 크기 조절

  • 이미지 자르기

  • 원본 이미지 유지

  • Documents 저장

  • 저장 파일 다시 불러오기

  • 사진 앱 저장

  • 사진 저장 권한 거부 처리

  • EXIF 메타데이터 확인

  • 메모리 사용량 확인

  • 앱 재실행 후 파일 유지 확인


30. 마무리

SwiftUI에서는 PhotosPicker를 이용해 사용자가 선택한 사진을 비교적 간단하게 가져올 수 있습니다.

카메라 촬영에는 UIImagePickerController나 AVFoundation을 사용할 수 있으며, 이미지 편집에는 UIKit과 Core Graphics 등의 기능을 활용할 수 있습니다.

이번 글에서 살펴본 핵심 내용을 정리하면 다음과 같습니다.

  1. PhotosPicker는 시스템 사진 선택 UI를 제공합니다.

  2. 사진 선택만으로 전체 사진 라이브러리 접근 권한이 필요하지는 않습니다.

  3. PhotosPickerItem에서 loadTransferable로 선택 데이터를 가져올 수 있습니다.

  4. 여러 장의 사진을 선택해 LazyVGrid로 표시할 수 있습니다.

  5. UIImagePickerController를 SwiftUI에 연결해 카메라 촬영 기능을 구현할 수 있습니다.

  6. UIGraphicsImageRenderer로 회전과 크기 조절 등의 이미지 편집을 구현할 수 있습니다.

  7. 앱 내부 Documents에 이미지 파일을 저장할 수 있습니다.

  8. Photos 프레임워크를 이용해 사진 앱에 이미지를 저장할 수 있습니다.

  9. 원본 사진의 EXIF와 GPS 메타데이터 보존 여부를 확인해야 합니다.

  10. 대용량 사진 앱에서는 메모리 관리와 원본·미리보기 분리가 중요합니다.

Android의 Photo Picker나 CameraX에 익숙한 개발자라면 iOS에서도 유사한 사진 처리 기능을 구현할 수 있습니다.

다만 iOS는 사진 선택과 사진 라이브러리 접근 권한이 분리되어 있으므로 필요한 기능에 맞춰 적절한 API를 선택해야 합니다.

사진 앱 개발의 핵심은 사진을 가져오는 것뿐 아니라 원본 데이터 보존, 편집 과정의 메모리 관리, 사용자 권한을 함께 설계하는 것입니다.


참고 자료

태그: iOS, Swift, SwiftUI, PhotosPicker, PhotosPickerItem, UIImagePickerController, AVFoundation, UIImage, 사진 선택, 카메라 촬영, 이미지 편집, 사진 저장, EXIF, 사진 콜라주, iPhone 앱 개발