콘텐츠 대표 이미지 - iOS 카메라와 AVFoundation을 완전 정복하는 사진·비디오 처리 가이드
📱 프로그램개발 · iOS

iOS 카메라와 AVFoundation을 완전 정복하는 사진·비디오 처리 가이드

🎬 카메라 앱 개발의 A to Z, 실전 코드와 함께 친절하게 파헤쳐보자!

안녕! 오늘은 iOS 개발자라면 한 번쯤은 꼭 마주치게 되는 카메라와 AVFoundation 세계로 같이 뛰어들어볼 거야. 📸
솔직히 말하면, 처음 AVFoundation을 접했을 때 "이게 뭐야... 클래스가 왜 이렇게 많아?" 싶었던 사람 손 들어봐. 🙋
근데 한번 제대로 이해하고 나면 진짜 강력하고 재미있는 프레임워크야. 사진 찍고, 영상 녹화하고, 필터 입히고, 실시간 처리까지 — 이 모든 게 다 AVFoundation 안에 있거든.

이 글에서는 AVFoundation의 핵심 구조부터 시작해서, 실제 카메라 앱을 만들 때 필요한 사진 촬영, 비디오 녹화, 실시간 프레임 처리까지 단계별로 쭉 훑어볼 거야. 코드도 실전 위주로 넣을 테니까 따라오면서 직접 해봐! 🚀


🎯 AVFoundation이 뭔데? 왜 써야 해?

iOS에서 카메라를 다루는 방법은 크게 두 가지야.
하나는 UIImagePickerController처럼 시스템이 제공하는 UI를 그냥 쓰는 방법이고,
다른 하나는 AVFoundation을 직접 써서 카메라를 완전히 내 손으로 제어하는 방법이야.

UIImagePickerController는 진짜 간단해. 3줄이면 카메라 열 수 있어. 근데 문제가 있어 — 커스터마이징이 거의 불가능해. 내가 원하는 UI를 만들 수도 없고, 실시간으로 프레임을 처리할 수도 없어.

그래서 진짜 카메라 앱을 만들려면 AVFoundation을 써야 해. 인스타그램, 스냅챗, 틱톡 같은 앱들이 다 이걸 기반으로 만들어진 거야. 🔥

AVFoundation 프레임워크 구조 AVFoundation Framework 📷 캡처 레이어 AVCaptureSession AVCaptureDevice AVCaptureInput AVCaptureOutput AVCapturePreviewLayer 사진·영상 캡처 실시간 미리보기 카메라 제어 ▶️ 재생 레이어 AVPlayer AVPlayerItem AVPlayerLayer AVQueuePlayer AVPlayerViewController 비디오 재생 스트리밍 지원 큐 관리 ✂️ 편집 레이어 AVAsset AVAssetTrack AVComposition AVVideoComposition AVAssetExportSession 트랙 편집 합성·트랜지션 내보내기 Apple 공식 멀티미디어 처리 프레임워크 — iOS, macOS, tvOS 지원

위 그림처럼 AVFoundation은 크게 세 가지 영역으로 나눌 수 있어.
캡처(Capture) — 카메라로 사진·영상 찍기
재생(Playback) — 비디오 파일 재생하기
편집(Edit) — 영상 자르고 합치고 내보내기

오늘 우리가 집중할 건 주로 캡처 레이어야. 여기서 카메라 제어의 핵심이 다 일어나거든!


🏗️ AVCaptureSession — 카메라의 심장

AVFoundation 카메라 구현의 핵심은 AVCaptureSession이야.
이걸 중심으로 입력(Input)과 출력(Output)이 연결되는 구조야. 마치 전기 회로처럼 생각하면 돼.

카메라 하드웨어 → AVCaptureDeviceInput → AVCaptureSession → AVCaptureOutput → 화면/파일

이 흐름을 이해하면 AVFoundation의 절반은 이해한 거야! 😄

기본 세션 설정하기

먼저 프로젝트에 카메라 권한 설정부터 해야 해. Info.plist에 이걸 추가해줘:


<key>NSCameraUsageDescription</key>

<string>카메라를 사용하여 사진과 영상을 촬영합니다.</string>

<key>NSMicrophoneUsageDescription</key>

<string>영상 녹화 시 마이크를 사용합니다.</string>

<key>NSPhotoLibraryUsageDescription</key>

<string>촬영한 사진을 앨범에 저장합니다.</string>

이제 Swift 코드로 기본 세션을 만들어보자:


import AVFoundation

import UIKit



class CameraViewController: UIViewController {



// 1. 핵심 객체들 선언

var captureSession: AVCaptureSession!

var previewLayer: AVCaptureVideoPreviewLayer!

var currentDevice: AVCaptureDevice?



override func viewDidLoad() {

super.viewDidLoad()

checkPermissionAndSetup()

}



func checkPermissionAndSetup() {

switch AVCaptureDevice.authorizationStatus(for: .video) {

case .authorized:

setupCaptureSession()

case .notDetermined:

AVCaptureDevice.requestAccess(for: .video) { granted in

if granted {

DispatchQueue.main.async {

self.setupCaptureSession()

}

}

}

default:

print("카메라 권한이 없습니다.")

}

}



func setupCaptureSession() {

captureSession = AVCaptureSession()

// 세션 품질 설정 (photo / high / medium / low)

captureSession.sessionPreset = .photo



// 입력 장치 설정

guard let device = AVCaptureDevice.default(

.builtInWideAngleCamera,

for: .video,

position: .back

) else { return }



currentDevice = device



do {

let input = try AVCaptureDeviceInput(device: device)

if captureSession.canAddInput(input) {

captureSession.addInput(input)

}

} catch {

print("입력 장치 설정 실패: \(error)")

return

}



// 미리보기 레이어 설정

previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)

previewLayer.videoGravity = .resizeAspectFill

previewLayer.frame = view.bounds

view.layer.insertSublayer(previewLayer, at: 0)



// 세션 시작 (백그라운드 스레드에서!)

DispatchQueue.global(qos: .userInitiated).async {

self.captureSession.startRunning()

}

}

}
💡 중요 포인트!
captureSession.startRunning()은 반드시 백그라운드 스레드에서 실행해야 해. 메인 스레드에서 실행하면 UI가 블로킹되어서 앱이 버벅거려. 이건 진짜 자주 하는 실수야!

세션 프리셋(Preset) 선택하기

sessionPreset은 카메라의 출력 품질을 결정해. 용도에 맞게 골라야 해:

프리셋 해상도 용도
.photo 최대 해상도 사진 촬영 (가장 고품질)
.hd4K3840x2160 4K (3840×2160) 4K 영상 녹화
.hd1920x1080 1080p Full HD 영상
.hd1280x720 720p HD 영상
.medium 480p 수준 실시간 처리, 스트리밍
.low 저해상도 QR코드 스캔, 바코드

📸 사진 촬영 구현하기

세션 설정이 끝났으면 이제 실제로 사진을 찍어보자!
사진 촬영에는 AVCapturePhotoOutput을 사용해. iOS 10 이후로 기존의 AVCaptureStillImageOutput은 deprecated 됐으니까 꼭 AVCapturePhotoOutput을 써야 해.

AVCapturePhotoOutput 설정


// CameraViewController에 추가

var photoOutput: AVCapturePhotoOutput!



// setupCaptureSession() 안에 추가

func setupPhotoOutput() {

photoOutput = AVCapturePhotoOutput()



// HEIF 포맷 지원 여부 확인 후 설정

if photoOutput.availablePhotoCodecTypes.contains(.hevc) {

// HEIF 포맷 지원 (iOS 11+, iPhone 7 이상)

print("HEIF 지원됨")

}



if captureSession.canAddOutput(photoOutput) {

captureSession.addOutput(photoOutput)

}

}

실제 사진 촬영 코드


// 셔터 버튼 탭 시 호출

@objc func capturePhoto() {

let settings = AVCapturePhotoSettings()



// 플래시 설정

if currentDevice?.hasFlash == true {

settings.flashMode = .auto  // .on / .off / .auto

}



// 고해상도 사진 활성화

settings.isHighResolutionPhotoEnabled = true



// HEIF 포맷으로 촬영 (용량 절약)

if let codecType = photoOutput.availablePhotoCodecTypes.first(where: { $0 == .hevc }) {

let heifSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: codecType])

photoOutput.capturePhoto(with: heifSettings, delegate: self)

} else {

// JPEG 폴백

photoOutput.capturePhoto(with: settings, delegate: self)

}

}



// AVCapturePhotoCaptureDelegate 구현

extension CameraViewController: AVCapturePhotoCaptureDelegate {



// 촬영 완료 시 호출

func photoOutput(_ output: AVCapturePhotoOutput,

didFinishProcessingPhoto photo: AVCapturePhoto,

error: Error?) {



if let error = error {

print("촬영 실패: \(error)")

return

}



// 이미지 데이터 추출

guard let imageData = photo.fileDataRepresentation() else { return }

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



// 사진 앨범에 저장

UIImageWriteToSavedPhotosAlbum(image, self, #selector(imageSaved(_:didFinishSavingWithError:contextInfo:)), nil)



// 또는 메모리에서 바로 사용

DispatchQueue.main.async {

self.showPreview(image: image)

}

}



@objc func imageSaved(_ image: UIImage,

didFinishSavingWithError error: Error?,

contextInfo: UnsafeRawPointer) {

if let error = error {

print("저장 실패: \(error)")

} else {

print("✅ 사진 저장 완료!")

}

}

}

촬영 타이밍 콜백 이해하기

AVCapturePhotoCaptureDelegate에는 여러 콜백이 있어. 촬영 과정의 각 단계를 알 수 있어:

1
willBeginCapture — 셔터가 눌리기 직전. 이때 셔터음 재생 가능
2
willCapturePhotoAnimation — 화면 플래시 효과 넣을 타이밍
3
didCapturePhoto — 실제 캡처 완료 (아직 처리 중)
4
didFinishProcessingPhoto — 이미지 데이터 사용 가능 ← 여기서 처리!
5
didFinishCapture — 모든 과정 완료

// 셔터 애니메이션 효과 추가

func photoOutput(_ output: AVCapturePhotoOutput,

willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {

// 화면 깜빡임 효과

DispatchQueue.main.async {

self.previewLayer.opacity = 0

UIView.animate(withDuration: 0.25) {

self.previewLayer.opacity = 1

}

}

}

🎥 비디오 녹화 구현하기

이번엔 영상 녹화야! 비디오 녹화에는 AVCaptureMovieFileOutput을 사용해.
사진보다 조금 더 복잡하지만, 원리는 같아. 파일로 직접 저장하는 방식이라 메모리 걱정이 없어서 좋아. 😊

AVCaptureMovieFileOutput 설정


var movieOutput: AVCaptureMovieFileOutput!

var isRecording = false



func setupMovieOutput() {

movieOutput = AVCaptureMovieFileOutput()



// 최대 녹화 시간 설정 (선택사항)

movieOutput.maxRecordedDuration = CMTime(seconds: 60, preferredTimescale: 600)



if captureSession.canAddOutput(movieOutput) {

captureSession.addOutput(movieOutput)

}



// 비디오 안정화 설정

if let connection = movieOutput.connection(with: .video) {

if connection.isVideoStabilizationSupported {

connection.preferredVideoStabilizationMode = .auto

}

}

}



// 마이크 입력 추가 (영상에 소리 포함)

func addAudioInput() {

guard let audioDevice = AVCaptureDevice.default(for: .audio) else { return }

do {

let audioInput = try AVCaptureDeviceInput(device: audioDevice)

if captureSession.canAddInput(audioInput) {

captureSession.addInput(audioInput)

}

} catch {

print("마이크 설정 실패: \(error)")

}

}

녹화 시작 / 중지


@objc func toggleRecording() {

if isRecording {

stopRecording()

} else {

startRecording()

}

}



func startRecording() {

guard !movieOutput.isRecording else { return }



// 임시 파일 경로 생성

let tempDir = FileManager.default.temporaryDirectory

let fileName = "video_\(Date().timeIntervalSince1970).mov"

let outputURL = tempDir.appendingPathComponent(fileName)



// 기존 파일 삭제

try? FileManager.default.removeItem(at: outputURL)



movieOutput.startRecording(to: outputURL, recordingDelegate: self)

isRecording = true

print("🔴 녹화 시작!")

}



func stopRecording() {

guard movieOutput.isRecording else { return }

movieOutput.stopRecording()

isRecording = false

print("⏹ 녹화 중지")

}



// AVCaptureFileOutputRecordingDelegate 구현

extension CameraViewController: AVCaptureFileOutputRecordingDelegate {



func fileOutput(_ output: AVCaptureFileOutput,

didFinishRecordingTo outputFileURL: URL,

from connections: [AVCaptureConnection],

error: Error?) {



if let error = error {

print("녹화 실패: \(error)")

return

}



// 사진 앨범에 저장

UISaveVideoAtPathToSavedPhotosAlbum(

outputFileURL.path,

self,

#selector(videoSaved(_:didFinishSavingWithError:contextInfo:)),

nil

)

}



@objc func videoSaved(_ videoPath: String,

didFinishSavingWithError error: Error?,

contextInfo: UnsafeRawPointer) {

if error == nil {

print("✅ 영상 저장 완료!")

}

}

}
⚠️ 주의!
AVCapturePhotoOutput과 AVCaptureMovieFileOutput을 동시에 세션에 추가하면 일부 기기에서 충돌이 발생할 수 있어. 사진 모드와 영상 모드를 전환할 때는 세션을 beginConfiguration() / commitConfiguration()으로 감싸서 안전하게 전환해야 해!

사진 ↔ 영상 모드 전환


enum CaptureMode {

case photo

case video

}



func switchCaptureMode(to mode: CaptureMode) {

captureSession.beginConfiguration()



switch mode {

case .photo:

// 영상 출력 제거, 사진 출력 추가

if captureSession.outputs.contains(movieOutput) {

captureSession.removeOutput(movieOutput)

}

if captureSession.canAddOutput(photoOutput) {

captureSession.addOutput(photoOutput)

}

captureSession.sessionPreset = .photo



case .video:

// 사진 출력 제거, 영상 출력 추가

if captureSession.outputs.contains(photoOutput) {

captureSession.removeOutput(photoOutput)

}

if captureSession.canAddOutput(movieOutput) {

captureSession.addOutput(movieOutput)

}

captureSession.sessionPreset = .hd1920x1080

}



captureSession.commitConfiguration()

}

🔄 카메라 전환 & 고급 제어

이제 좀 더 실용적인 기능들을 알아보자. 전면/후면 카메라 전환, 줌, 포커스, 노출 제어 같은 거야.
이런 기능들이 있어야 진짜 카메라 앱처럼 느껴지거든! 🎛️

전면 / 후면 카메라 전환


func switchCamera() {

guard let currentInput = captureSession.inputs.first as? AVCaptureDeviceInput else { return }



let currentPosition = currentInput.device.position

let newPosition: AVCaptureDevice.Position = currentPosition == .back ? .front : .back



guard let newDevice = AVCaptureDevice.default(

.builtInWideAngleCamera,

for: .video,

position: newPosition

) else { return }



do {

let newInput = try AVCaptureDeviceInput(device: newDevice)



captureSession.beginConfiguration()

captureSession.removeInput(currentInput)



if captureSession.canAddInput(newInput) {

captureSession.addInput(newInput)

currentDevice = newDevice

} else {

captureSession.addInput(currentInput) // 실패 시 원복

}



captureSession.commitConfiguration()



// 전환 애니메이션

animateCameraSwitch()



} catch {

print("카메라 전환 실패: \(error)")

}

}



func animateCameraSwitch() {

let transition = CATransition()

transition.duration = 0.3

transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

transition.type = .fade

previewLayer.add(transition, forKey: nil)

}

줌(Zoom) 제어


// 핀치 제스처로 줌 제어

var lastZoomFactor: CGFloat = 1.0



@objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {

guard let device = currentDevice else { return }



let maxZoom = min(device.activeFormat.videoMaxZoomFactor, 10.0)

let minZoom: CGFloat = 1.0



func update(scale factor: CGFloat) {

do {

try device.lockForConfiguration()

defer { device.unlockForConfiguration() }



device.videoZoomFactor = factor

} catch {

print("줌 설정 실패: \(error)")

}

}



let newScaleFactor = minMaxZoom(lastZoomFactor * gesture.scale,

min: minZoom, max: maxZoom)



switch gesture.state {

case .began: fallthrough

case .changed:

update(scale: newScaleFactor)

case .ended:

lastZoomFactor = newScaleFactor

update(scale: newScaleFactor)

default: break

}

}



func minMaxZoom(_ factor: CGFloat, min: CGFloat, max: CGFloat) -> CGFloat {

return Swift.min(Swift.max(factor, min), max)

}

탭으로 포커스 & 노출 설정


@objc func handleTap(_ gesture: UITapGestureRecognizer) {

let touchPoint = gesture.location(in: view)



// 화면 좌표 → 카메라 좌표 변환

let devicePoint = previewLayer.captureDevicePointConverted(fromLayerPoint: touchPoint)



focusAndExpose(at: devicePoint)

showFocusIndicator(at: touchPoint)

}



func focusAndExpose(at point: CGPoint) {

guard let device = currentDevice else { return }



do {

try device.lockForConfiguration()



// 포커스 설정

if device.isFocusPointOfInterestSupported &&

device.isFocusModeSupported(.autoFocus) {

device.focusPointOfInterest = point

device.focusMode = .autoFocus

}



// 노출 설정

if device.isExposurePointOfInterestSupported &&

device.isExposureModeSupported(.autoExpose) {

device.exposurePointOfInterest = point

device.exposureMode = .autoExpose

}



// 화이트밸런스 자동

if device.isWhiteBalanceModeSupported(.autoWhiteBalance) {

device.whiteBalanceMode = .autoWhiteBalance

}



device.unlockForConfiguration()

} catch {

print("포커스 설정 실패: \(error)")

}

}



// 포커스 인디케이터 UI

func showFocusIndicator(at point: CGPoint) {

let focusView = UIView(frame: CGRect(x: 0, y: 0, width: 70, height: 70))

focusView.center = point

focusView.layer.borderColor = UIColor.yellow.cgColor

focusView.layer.borderWidth = 2

focusView.backgroundColor = .clear

view.addSubview(focusView)



UIView.animate(withDuration: 0.3, animations: {

focusView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)

}) { _ in

UIView.animate(withDuration: 0.2, delay: 0.5) {

focusView.alpha = 0

} completion: { _ in

focusView.removeFromSuperview()

}

}

}
카메라 제어 기능 한눈에 보기 1.5x 🔄 ⚡ 핀치 줌 제어 탭 포커스 / 노출 전면/후면 전환 플래시 제어 셔터 버튼

🎨 실시간 비디오 프레임 처리 (AVCaptureVideoDataOutput)

이제 진짜 재미있는 부분이야! 🔥
AVCaptureVideoDataOutput을 사용하면 카메라에서 나오는 모든 프레임을 실시간으로 받아서 처리할 수 있어.
필터 적용, 얼굴 인식, AR 효과, QR 코드 스캔 같은 기능들이 다 이걸 기반으로 해.

단, 이 방식은 성능이 매우 중요해. 60fps로 들어오는 프레임을 16ms 안에 처리해야 하거든. 그래서 Metal이나 Core Image를 함께 쓰는 경우가 많아.

VideoDataOutput 설정


var videoDataOutput: AVCaptureVideoDataOutput!

let videoDataOutputQueue = DispatchQueue(

label: "com.myapp.videoDataOutput",

qos: .userInitiated,

attributes: [],

autoreleaseFrequency: .workItem

)



func setupVideoDataOutput() {

videoDataOutput = AVCaptureVideoDataOutput()



// 픽셀 포맷 설정

videoDataOutput.videoSettings = [

kCVPixelBufferPixelFormatTypeKey as String:

kCVPixelFormatType_32BGRA  // BGRA 형식 (Metal과 호환성 좋음)

]



// 처리가 늦으면 프레임 드롭 (메모리 보호)

videoDataOutput.alwaysDiscardsLateVideoFrames = true



// 델리게이트 설정 (전용 큐에서 처리)

videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)



if captureSession.canAddOutput(videoDataOutput) {

captureSession.addOutput(videoDataOutput)

}



// 방향 설정

if let connection = videoDataOutput.connection(with: .video) {

connection.videoOrientation = .portrait

// 전면 카메라 미러링

if currentDevice?.position == .front {

connection.isVideoMirrored = true

}

}

}

프레임 처리 델리게이트


extension CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {



func captureOutput(_ output: AVCaptureOutput,

didOutput sampleBuffer: CMSampleBuffer,

from connection: AVCaptureConnection) {



// CMSampleBuffer → CVPixelBuffer 변환

guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }



// Core Image로 필터 적용

let ciImage = CIImage(cvPixelBuffer: pixelBuffer)

let filteredImage = applyFilter(to: ciImage)



// 메인 스레드에서 UI 업데이트

DispatchQueue.main.async {

self.filteredImageView.image = UIImage(ciImage: filteredImage)

}

}



// 프레임 드롭 감지

func captureOutput(_ output: AVCaptureOutput,

didDrop sampleBuffer: CMSampleBuffer,

from connection: AVCaptureConnection) {

print("⚠️ 프레임 드롭 발생 — 처리 속도 최적화 필요")

}

}



// Core Image 필터 적용 예시

func applyFilter(to image: CIImage) -> CIImage {

let context = CIContext()



// 흑백 필터

guard let filter = CIFilter(name: "CIPhotoEffectMono") else { return image }

filter.setValue(image, forKey: kCIInputImageKey)

return filter.outputImage ?? image

}

다양한 Core Image 필터들

Core Image에는 200개 이상의 내장 필터가 있어. 자주 쓰는 것들만 정리해봤어:

필터 이름 효과 주요 파라미터
CIPhotoEffectMono 흑백 변환 없음
CIPhotoEffectVivid 선명도 강화 없음
CIColorControls 밝기/대비/채도 brightness, contrast, saturation
CIGaussianBlur 가우시안 블러 inputRadius
CIVignette 비네트 효과 inputIntensity, inputRadius
CISepiaTone 세피아 톤 inputIntensity
CIPixellate 픽셀화 inputScale
CIEdges 엣지 검출 inputIntensity

// 여러 필터 체이닝 예시

func applyMultipleFilters(to image: CIImage) -> CIImage {

var result = image



// 1. 채도 조절

if let colorFilter = CIFilter(name: "CIColorControls") {

colorFilter.setValue(result, forKey: kCIInputImageKey)

colorFilter.setValue(1.5, forKey: "inputSaturation")  // 채도 1.5배

colorFilter.setValue(0.1, forKey: "inputBrightness")  // 밝기 +0.1

result = colorFilter.outputImage ?? result

}



// 2. 비네트 효과

if let vignetteFilter = CIFilter(name: "CIVignette") {

vignetteFilter.setValue(result, forKey: kCIInputImageKey)

vignetteFilter.setValue(0.8, forKey: "inputIntensity")

vignetteFilter.setValue(1.5, forKey: "inputRadius")

result = vignetteFilter.outputImage ?? result

}



return result

}
💡 성능 팁!
CIContext는 매 프레임마다 새로 만들면 안 돼. 한 번 만들어서 재사용해야 해. 특히 Metal 기반 컨텍스트를 쓰면 GPU 가속으로 훨씬 빨라져:
let context = CIContext(options: [.useSoftwareRenderer: false])

🔍 Vision Framework와 연동 — 얼굴 인식, QR 코드

AVFoundation으로 프레임을 받아오면, Apple의 Vision Framework와 연동해서 강력한 AI 기능을 추가할 수 있어.
얼굴 인식, 텍스트 인식, QR/바코드 스캔, 물체 감지 같은 것들이 다 가능해. 그것도 실시간으로! 🤖

실시간 얼굴 인식


import Vision



class FaceDetectionViewController: UIViewController {



var faceDetectionRequest: VNDetectFaceRectanglesRequest!

var overlayLayer: CALayer!



override func viewDidLoad() {

super.viewDidLoad()

setupFaceDetection()

}



func setupFaceDetection() {

faceDetectionRequest = VNDetectFaceRectanglesRequest { [weak self] request, error in

guard let self = self else { return }



if let error = error {

print("얼굴 인식 오류: \(error)")

return

}



guard let results = request.results as? [VNFaceObservation] else { return }



DispatchQueue.main.async {

self.drawFaceBoxes(faces: results)

}

}

}



// VideoDataOutput 델리게이트에서 호출

func detectFaces(in sampleBuffer: CMSampleBuffer) {

guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }



let requestHandler = VNImageRequestHandler(

cvPixelBuffer: pixelBuffer,

orientation: .up,

options: [:]

)



do {

try requestHandler.perform([faceDetectionRequest])

} catch {

print("Vision 요청 실패: \(error)")

}

}



func drawFaceBoxes(faces: [VNFaceObservation]) {

// 기존 레이어 제거

overlayLayer.sublayers?.forEach { $0.removeFromSuperlayer() }



for face in faces {

// Vision 좌표 → 화면 좌표 변환

let boundingBox = face.boundingBox

let size = CGSize(

width: boundingBox.width * view.bounds.width,

height: boundingBox.height * view.bounds.height

)

let origin = CGPoint(

x: boundingBox.minX * view.bounds.width,

y: (1 - boundingBox.maxY) * view.bounds.height  // Y축 반전!

)



let faceLayer = CALayer()

faceLayer.frame = CGRect(origin: origin, size: size)

faceLayer.borderColor = UIColor.green.cgColor

faceLayer.borderWidth = 2

faceLayer.cornerRadius = 4

overlayLayer.addSublayer(faceLayer)

}

}

}
⚠️ Vision 좌표계 주의!
Vision Framework는 좌하단이 원점(0,0)인 좌표계를 사용해. 반면 UIKit은 좌상단이 원점이야. 그래서 Y축을 반전시켜야 해: y = (1 - boundingBox.maxY) * height

QR 코드 / 바코드 스캔

QR 코드 스캔은 AVCaptureMetadataOutput을 사용하는 게 더 간단하고 빨라:


var metadataOutput: AVCaptureMetadataOutput!



func setupQRScanner() {

metadataOutput = AVCaptureMetadataOutput()



if captureSession.canAddOutput(metadataOutput) {

captureSession.addOutput(metadataOutput)



metadataOutput.setMetadataObjectsDelegate(self, queue: .main)



// 지원하는 코드 타입 설정

metadataOutput.metadataObjectTypes = [

.qr,          // QR 코드

.ean8,        // EAN-8 바코드

.ean13,       // EAN-13 바코드

.code128,     // Code 128

.pdf417,      // PDF417

.aztec        // Aztec 코드

]



// 스캔 영역 제한 (선택사항 — 성능 향상)

metadataOutput.rectOfInterest = CGRect(x: 0.2, y: 0.2, width: 0.6, height: 0.6)

}

}



extension CameraViewController: AVCaptureMetadataOutputObjectsDelegate {



func metadataOutput(_ output: AVCaptureMetadataOutput,

didOutput metadataObjects: [AVMetadataObject],

from connection: AVCaptureConnection) {



guard let metadataObject = metadataObjects.first,

let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,

let stringValue = readableObject.stringValue else { return }



// 진동 피드백

AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))



print("✅ QR 코드 인식: \(stringValue)")

handleQRCode(stringValue)

}

}

🎬 AVAsset으로 비디오 편집하기

촬영한 영상을 편집하고 싶다면 AVAsset과 AVMutableComposition을 사용해.
영상 자르기, 합치기, 오디오 추가, 트랜지션 효과까지 모두 가능해. 마치 코드로 영상 편집 프로그램을 만드는 느낌이야! ✂️

영상 자르기 (Trim)


func trimVideo(inputURL: URL,

startTime: Double,

endTime: Double,

completion: @escaping (URL?) -> Void) {



let asset = AVAsset(url: inputURL)



// 내보내기 세션 생성

guard let exportSession = AVAssetExportSession(

asset: asset,

presetName: AVAssetExportPresetHighestQuality

) else {

completion(nil)

return

}



// 출력 파일 경로

let outputURL = FileManager.default.temporaryDirectory

.appendingPathComponent("trimmed_\(Date().timeIntervalSince1970).mp4")



exportSession.outputURL = outputURL

exportSession.outputFileType = .mp4

exportSession.shouldOptimizeForNetworkUse = true



// 시간 범위 설정

let start = CMTime(seconds: startTime, preferredTimescale: 600)

let end = CMTime(seconds: endTime, preferredTimescale: 600)

exportSession.timeRange = CMTimeRange(start: start, end: end)



// 내보내기 실행

exportSession.exportAsynchronously {

switch exportSession.status {

case .completed:

print("✅ 영상 자르기 완료")

completion(outputURL)

case .failed:

print("❌ 실패: \(exportSession.error?.localizedDescription ?? "")")

completion(nil)

case .cancelled:

print("취소됨")

completion(nil)

default: break

}

}

}

여러 영상 합치기 (Merge)


func mergeVideos(urls: [URL], completion: @escaping (URL?) -> Void) {

let composition = AVMutableComposition()



guard let videoTrack = composition.addMutableTrack(

withMediaType: .video,

preferredTrackID: kCMPersistentTrackID_Invalid

),

let audioTrack = composition.addMutableTrack(

withMediaType: .audio,

preferredTrackID: kCMPersistentTrackID_Invalid

) else { return }



var currentTime = CMTime.zero



for url in urls {

let asset = AVAsset(url: url)

let duration = asset.duration

let timeRange = CMTimeRange(start: .zero, duration: duration)



// 비디오 트랙 추가

if let assetVideoTrack = asset.tracks(withMediaType: .video).first {

try? videoTrack.insertTimeRange(timeRange,

of: assetVideoTrack,

at: currentTime)

}



// 오디오 트랙 추가

if let assetAudioTrack = asset.tracks(withMediaType: .audio).first {

try? audioTrack.insertTimeRange(timeRange,

of: assetAudioTrack,

at: currentTime)

}



currentTime = CMTimeAdd(currentTime, duration)

}



// 내보내기

let outputURL = FileManager.default.temporaryDirectory

.appendingPathComponent("merged_\(Date().timeIntervalSince1970).mp4")



guard let exportSession = AVAssetExportSession(

asset: composition,

presetName: AVAssetExportPresetHighestQuality

) else { return }



exportSession.outputURL = outputURL

exportSession.outputFileType = .mp4



exportSession.exportAsynchronously {

if exportSession.status == .completed {

completion(outputURL)

} else {

completion(nil)

}

}

}

영상에 텍스트/이미지 오버레이 추가


func addTextOverlay(to asset: AVAsset,

text: String,

completion: @escaping (URL?) -> Void) {



let composition = AVMutableComposition()

let videoSize = asset.tracks(withMediaType: .video).first?.naturalSize ?? CGSize(width: 1920, height: 1080)



// 비디오 컴포지션 설정

let videoComposition = AVMutableVideoComposition()

videoComposition.renderSize = videoSize

videoComposition.frameDuration = CMTime(value: 1, timescale: 30)



// CALayer로 텍스트 레이어 생성

let textLayer = CATextLayer()

textLayer.string = text

textLayer.fontSize = 60

textLayer.foregroundColor = UIColor.white.cgColor

textLayer.alignmentMode = .center

textLayer.frame = CGRect(x: 0, y: 50, width: videoSize.width, height: 100)



// 배경 레이어

let backgroundLayer = CALayer()

backgroundLayer.frame = CGRect(origin: .zero, size: videoSize)



// 비디오 레이어

let videoLayer = CALayer()

videoLayer.frame = CGRect(origin: .zero, size: videoSize)



// 레이어 합성

let parentLayer = CALayer()

parentLayer.frame = CGRect(origin: .zero, size: videoSize)

parentLayer.addSublayer(videoLayer)

parentLayer.addSublayer(backgroundLayer)

parentLayer.addSublayer(textLayer)



videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(

postProcessingAsVideoLayer: videoLayer,

in: parentLayer

)



// ... (내보내기 코드 동일)

}

⚡ 성능 최적화 & 실전 팁

카메라 앱은 성능이 생명이야. 프레임 드롭이 생기거나 배터리를 너무 많이 먹으면 사용자가 바로 삭제해버리거든. 😅
실전에서 꼭 알아야 할 최적화 팁들을 정리해봤어.

성능 최적화 핵심 포인트 🧵 스레드 관리 세션 시작/중지는 백그라운드 스레드에서 UI 업데이트는 메인 스레드에서 DispatchQueue 필수! 🖥️ Metal GPU 활용 CIContext를 Metal로 초기화하면 GPU 가속 필터 처리 속도 10배 이상 향상 가능 useSoftwareRenderer: false ♻️ 객체 재사용 CIContext, CIFilter 매 프레임 생성 금지 한번 만들고 계속 재사용하기 메모리 할당 최소화 📐 해상도 최적화 실시간 처리 시 medium 프리셋 사용 불필요한 고해상도는 배터리 낭비 용도에 맞는 프리셋 선택 🔋 배터리 절약 앱이 백그라운드 시 세션 중지 필수 viewWillDisappear에서 stopRunning() 호출 리소스 해제 철저히! 🎯 프레임 드롭 방지 alwaysDiscards LateVideoFrames = true 처리 큐를 별도로 분리해서 관리 전용 DispatchQueue 사용 이 6가지만 지켜도 카메라 앱 성능이 확 달라져!

앱 생명주기와 세션 관리


// 앱 생명주기에 맞춰 세션 관리

override func viewWillAppear(_ animated: Bool) {

super.viewWillAppear(animated)

DispatchQueue.global(qos: .userInitiated).async {

if !self.captureSession.isRunning {

self.captureSession.startRunning()

}

}

}



override func viewWillDisappear(_ animated: Bool) {

super.viewWillDisappear(animated)

DispatchQueue.global(qos: .userInitiated).async {

if self.captureSession.isRunning {

self.captureSession.stopRunning()

}

}

}



// 앱 백그라운드 진입 시

override func viewDidLoad() {

super.viewDidLoad()

NotificationCenter.default.addObserver(

self,

selector: #selector(appDidEnterBackground),

name: UIApplication.didEnterBackgroundNotification,

object: nil

)

NotificationCenter.default.addObserver(

self,

selector: #selector(appWillEnterForeground),

name: UIApplication.willEnterForegroundNotification,

object: nil

)

}



@objc func appDidEnterBackground() {

DispatchQueue.global().async {

self.captureSession.stopRunning()

}

}



@objc func appWillEnterForeground() {

DispatchQueue.global().async {

self.captureSession.startRunning()

}

}

카메라 인터럽트 처리

전화가 오거나 다른 앱이 카메라를 사용하면 세션이 인터럽트될 수 있어. 이걸 처리해줘야 해:


// 세션 인터럽트 감지

NotificationCenter.default.addObserver(

self,

selector: #selector(sessionWasInterrupted),

name: .AVCaptureSessionWasInterrupted,

object: captureSession

)



NotificationCenter.default.addObserver(

self,

selector: #selector(sessionInterruptionEnded),

name: .AVCaptureSessionInterruptionEnded,

object: captureSession

)



@objc func sessionWasInterrupted(notification: NSNotification) {

guard let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?,

let reasonIntegerValue = userInfoValue.integerValue,

let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) else { return }



switch reason {

case .videoDeviceNotAvailableInBackground:

print("백그라운드에서 카메라 사용 불가")

case .audioDeviceInUseByAnotherClient:

print("다른 앱이 마이크 사용 중")

case .videoDeviceInUseByAnotherClient:

print("다른 앱이 카메라 사용 중")

case .videoDeviceNotAvailableWithMultipleForegroundApps:

print("멀티태스킹 환경에서 카메라 사용 불가")

default: break

}

}



@objc func sessionInterruptionEnded(notification: NSNotification) {

print("카메라 인터럽트 종료 — 재개 가능")

DispatchQueue.global().async {

self.captureSession.startRunning()

}

}

📱 멀티 카메라 & 최신 기능들 (iOS 13+)

iPhone 11 이후로 멀티 카메라 시스템이 도입됐어. AVCaptureMultiCamSession을 사용하면 전면과 후면 카메라를 동시에 사용할 수 있어. 유튜버들이 자주 쓰는 PIP(Picture-in-Picture) 촬영 같은 거 말이야! 🎥

멀티 카메라 세션


// iOS 13+ 멀티 카메라

import AVFoundation



class MultiCamViewController: UIViewController {



var multiCamSession: AVCaptureMultiCamSession!



func setupMultiCam() {

// 멀티 카메라 지원 여부 확인

guard AVCaptureMultiCamSession.isMultiCamSupported else {

print("이 기기는 멀티 카메라를 지원하지 않습니다.")

return

}



multiCamSession = AVCaptureMultiCamSession()



multiCamSession.beginConfiguration()



// 후면 카메라 추가

if let backCamera = AVCaptureDevice.default(

.builtInWideAngleCamera, for: .video, position: .back

),

let backInput = try? AVCaptureDeviceInput(device: backCamera),

multiCamSession.canAddInput(backInput) {

multiCamSession.addInputWithNoConnections(backInput)

}



// 전면 카메라 추가

if let frontCamera = AVCaptureDevice.default(

.builtInWideAngleCamera, for: .video, position: .front

),

let frontInput = try? AVCaptureDeviceInput(device: frontCamera),

multiCamSession.canAddInput(frontInput) {

multiCamSession.addInputWithNoConnections(frontInput)

}



multiCamSession.commitConfiguration()



DispatchQueue.global(qos: .userInitiated).async {

self.multiCamSession.startRunning()

}

}

}

최신 카메라 기능들 (iOS 16+)

🔭 Depth Data (깊이 데이터)
TrueDepth 카메라나 LiDAR 스캐너가 있는 기기에서 깊이 정보를 함께 캡처할 수 있어. 포트레이트 모드, AR 기능에 활용돼.

📸 RAW 포맷 촬영
AVCapturePhotoSettings에서 RAW 포맷을 지원하면 DNG 파일로 저장 가능. 전문 사진 앱에서 사용해.

🎬 ProRes 영상 녹화
iPhone 13 Pro 이상에서 Apple ProRes 코덱으로 영상 녹화 가능. 영상 편집 전문가용.

🌙 야간 모드 (Night Mode)
AVCapturePhotoSettings.photoQualityPrioritization으로 야간 모드 제어 가능.


// 깊이 데이터 캡처 설정

func setupDepthCapture() {

let photoSettings = AVCapturePhotoSettings()



// 깊이 데이터 포함 여부

if photoOutput.isDepthDataDeliverySupported {

photoOutput.isDepthDataDeliveryEnabled = true

photoSettings.isDepthDataDeliveryEnabled = true

}



// 포트레이트 효과 매트 (배경 분리)

if photoOutput.isPortraitEffectsMatteDeliverySupported {

photoOutput.isPortraitEffectsMatteDeliveryEnabled = true

photoSettings.isPortraitEffectsMatteDeliveryEnabled = true

}



// 품질 우선순위 설정

photoSettings.photoQualityPrioritization = .quality  // .speed / .balanced / .quality



photoOutput.capturePhoto(with: photoSettings, delegate: self)

}



// 깊이 데이터 처리

func photoOutput(_ output: AVCapturePhotoOutput,

didFinishProcessingPhoto photo: AVCapturePhoto,

error: Error?) {



// 깊이 데이터 추출

if let depthData = photo.depthData {

let depthMap = depthData.converting(toDepthDataType:

kCVPixelFormatType_DepthFloat32)

let depthPixelBuffer = depthMap.depthDataMap

print("깊이 데이터 크기: \(CVPixelBufferGetWidth(depthPixelBuffer))x\(CVPixelBufferGetHeight(depthPixelBuffer))")

}

}

🛠️ 실전 프로젝트 — 인스타그램 스타일 카메라 앱 구조

지금까지 배운 내용을 종합해서 실제 앱 구조를 잡아보자.
재능넷 같은 플랫폼에서 재능을 공유할 때 포트폴리오로 카메라 앱을 만들어 보여주면 정말 임팩트 있어! 💪
아래는 인스타그램 스타일 카메라 앱의 전체 아키텍처야:

인스타그램 스타일 카메라 앱 아키텍처 UI 레이어 비즈니스 레이어 데이터 레이어 하드웨어 레이어 CameraViewController 미리보기 + 컨트롤 FilterViewController 필터 선택 UI EditViewController 편집 UI GalleryViewController 갤러리 UI CameraManager 세션 / 입출력 관리 카메라 제어 FilterProcessor Core Image 필터 Metal 렌더링 VideoEditor AVComposition 내보내기 AVCaptureSession 입력/출력 파이프라인 세션 설정 PhotoLibrary PHPhotoLibrary 저장 / 불러오기 FileManager 임시 파일 관리 캐시 처리 후면 카메라 광각/망원/초광각 전면 카메라 TrueDepth 마이크 오디오 입력 LiDAR / 센서 깊이 / AR 각 레이어는 단방향 의존성 — 위에서 아래로만 의존 MVVM 또는 Clean Architecture 패턴 적용 권장 Combine / async-await로 비동기 처리

CameraManager 싱글톤 패턴


// 카메라 관련 로직을 분리한 Manager 클래스

class CameraManager: NSObject {



static let shared = CameraManager()

private override init() {}



// 세션

let session = AVCaptureSession()

private let sessionQueue = DispatchQueue(label: "com.app.cameraSession")



// 출력

private(set) var photoOutput = AVCapturePhotoOutput()

private(set) var videoDataOutput = AVCaptureVideoDataOutput()



// 현재 장치

private(set) var currentDevice: AVCaptureDevice?



// 상태

var isRunning: Bool { session.isRunning }



// 콜백

var onPhotoCapture: ((UIImage) -> Void)?

var onVideoFrame: ((CVPixelBuffer) -> Void)?



func configure(completion: @escaping (Bool) -> Void) {

sessionQueue.async { [weak self] in

guard let self = self else { return }

self.session.beginConfiguration()

let success = self.setupInputs() && self.setupOutputs()

self.session.commitConfiguration()

DispatchQueue.main.async { completion(success) }

}

}



func start() {

sessionQueue.async {

if !self.session.isRunning {

self.session.startRunning()

}

}

}



func stop() {

sessionQueue.async {

if self.session.isRunning {

self.session.stopRunning()

}

}

}



private func setupInputs() -> Bool {

guard let device = AVCaptureDevice.default(

.builtInWideAngleCamera, for: .video, position: .back

),

let input = try? AVCaptureDeviceInput(device: device),

session.canAddInput(input) else { return false }



session.addInput(input)

currentDevice = device

return true

}



private func setupOutputs() -> Bool {

guard session.canAddOutput(photoOutput),

session.canAddOutput(videoDataOutput) else { return false }



session.addOutput(photoOutput)

session.addOutput(videoDataOutput)



videoDataOutput.videoSettings = [

kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA

]

videoDataOutput.setSampleBufferDelegate(self, queue: sessionQueue)



return true

}

}

🧪 테스트 & 디버깅 팁

카메라 앱 개발에서 테스트는 좀 특별해. 시뮬레이터에서는 실제 카메라를 사용할 수 없거든.
그래서 실기기 테스트가 필수야. 몇 가지 유용한 팁을 알려줄게! 🔧

시뮬레이터 대응 코드


// 시뮬레이터와 실기기 분기 처리

#if targetEnvironment(simulator)

// 시뮬레이터: 더미 이미지 사용

func setupForSimulator() {

let placeholderImage = UIImage(systemName: "camera.fill")

previewImageView.image = placeholderImage

previewImageView.tintColor = .systemGray

print("⚠️ 시뮬레이터 환경 — 카메라 기능 제한됨")

}

#else

// 실기기: 실제 카메라 설정

func setupCamera() {

setupCaptureSession()

}

#endif

자주 발생하는 오류와 해결법

오류 원인 해결법
세션이 시작 안 됨 권한 미설정 또는 메인 스레드 실행 권한 확인 + 백그라운드 스레드 사용
미리보기 검은 화면 previewLayer 프레임 미설정 viewDidLayoutSubviews에서 frame 업데이트
사진이 회전됨 EXIF 방향 정보 미처리 imageOrientation 처리 또는 fixOrientation 함수 사용
프레임 드롭 발생 처리 시간이 너무 길거나 메인 스레드 블로킹 alwaysDiscardsLateVideoFrames = true + 전용 큐 사용
메모리 누수 델리게이트 강한 참조 [weak self] 캡처 리스트 사용
canAddOutput이 false 세션 설정 충돌 beginConfiguration/commitConfiguration 사용

// 사진 방향 수정 유틸리티

extension UIImage {

func fixOrientation() -> UIImage {

if imageOrientation == .up { return self }



UIGraphicsBeginImageContextWithOptions(size, false, scale)

draw(in: CGRect(origin: .zero, size: size))

let normalizedImage = UIGraphicsGetImageFromCurrentImageContext()

UIGraphicsEndImageContext()



return normalizedImage ?? self

}

}



// 메모리 경고 처리

override func didReceiveMemoryWarning() {

super.didReceiveMemoryWarning()

// 캐시된 이미지 해제

imageCache.removeAllObjects()

print("⚠️ 메모리 경고 — 캐시 해제")

}

🚀 마무리 — 더 나아가기 위한 로드맵

와, 여기까지 따라왔다면 진짜 대단한 거야! 👏
AVFoundation은 정말 깊고 넓은 프레임워크야. 오늘 다룬 내용만 해도 꽤 많지만, 사실 이게 시작이야.
더 깊이 파고들고 싶다면 아래 로드맵을 참고해봐:

1
Metal + MetalKit — GPU 기반 실시간 렌더링. Core Image보다 훨씬 빠른 커스텀 필터 구현 가능
2
Vision Framework 심화 — 포즈 추정, 손 인식, 물체 추적, 텍스트 인식 등 AI 기능 확장
3
ARKit 연동 — AVFoundation + ARKit으로 AR 카메라 앱 제작
4
Core ML 통합 — 커스텀 ML 모델로 실시간 이미지 분류, 스타일 트랜스퍼
5
ReplayKit — 화면 녹화 기능 추가, 라이브 스트리밍 구현
6
PhotoKit (PHPhotoLibrary) — 갤러리 접근, 앨범 관리, 에셋 편집

iOS 카메라 개발은 진짜 재미있는 분야야. 사용자가 직접 눈으로 보고 느끼는 기능이라서 완성했을 때 성취감도 엄청나거든. 🎉

만약 카메라 앱 개발 관련 도움이 필요하거나, 실력 있는 iOS 개발자를 찾고 있다면 재능넷에서 다양한 개발 재능을 가진 분들을 만나볼 수 있어. 실제로 AVFoundation 전문가들이 프리랜서로 활동하고 있으니까 참고해봐! 😊

오늘 배운 내용을 정리하면:

✅ AVCaptureSession — 카메라 파이프라인의 핵심
✅ AVCapturePhotoOutput — 고품질 사진 촬영
✅ AVCaptureMovieFileOutput — 비디오 파일 녹화
✅ AVCaptureVideoDataOutput — 실시간 프레임 처리
✅ Core Image — 필터 적용
✅ Vision Framework — AI 기반 인식
✅ AVAsset / AVComposition — 영상 편집
✅ 성능 최적화 — 스레드 관리, Metal GPU 활용
✅ 생명주기 관리 — 세션 시작/중지, 인터럽트 처리

코드 한 줄 한 줄 직접 쳐보면서 익히는 게 최고야. 처음엔 복잡해 보여도 패턴이 보이기 시작하면 금방 익숙해져. 화이팅! 💪🎬

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.