🔐 Local Authentication 완전정복
Face ID와 Touch ID로 앱을 안전하게! 생체 인증부터 비밀번호 대체까지 완벽 가이드.
⭐ 난이도: ⭐
⏱️ 예상 시간: 30min
📂 System & Network
✨ Local Authentication이란?
Local Authentication은 Face ID, Touch ID, 기기 암호를 사용하여 사용자를 인증하는 프레임워크입니다. 로그인, 결제 승인, 민감한 정보 접근 시 안전하고 편리한 인증을 제공합니다. 모든 생체 데이터는 기기 내에서만 처리되어 서버로 전송되지 않습니다.
📦 주요 기능
기능 개요
✅ Face ID / Touch ID 인증
✅ 기기 암호 인증 (비밀번호)
✅ 생체 인증 가용성 확인
✅ 인증 실패 처리 및 재시도
✅ 커스텀 인증 UI
✅ 비밀번호 대체 인증🔍 생체 인증 가용성 확인
BiometryCheck.swift
import LocalAuthentication func checkBiometryAvailability() -> (Bool, String) { let context = LAContext() var error: NSError? // 생체 인증 가능 여부 확인 let canEvaluate = context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) if canEvaluate { // 생체 인증 타입 확인 switch context.biometryType { case .faceID: return (true, "Face ID 사용 가능") case .touchID: return (true, "Touch ID 사용 가능") case .opticID: return (true, "Optic ID 사용 가능") case .none: return (false, "생체 인증 미지원") @unknown default: return (false, "알 수 없는 타입") } } else { // 오류 처리 if let error = error { switch LAError.Code(rawValue: error.code) { case .biometryNotEnrolled: return (false, "생체 인증 미등록") case .biometryNotAvailable: return (false, "생체 인증 미지원") case .passcodeNotSet: return (false, "기기 암호 미설정") default: return (false, error.localizedDescription) } } return (false, "알 수 없는 오류") } } // 사용 예시 let (available, message) = checkBiometryAvailability() print(message) // "Face ID 사용 가능"
🔐 기본 생체 인증
BasicAuth.swift
import LocalAuthentication func authenticateUser() async -> Bool { let context = LAContext() var error: NSError? // 생체 인증 가능 여부 확인 guard context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) else { print("생체 인증 불가: \(error?.localizedDescription ?? "")") return false } do { // 인증 시도 let success = try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "앱에 로그인하려면 인증이 필요합니다" ) return success } catch let error { // 인증 실패 처리 if let laError = error as? LAError { switch laError.code { case .authenticationFailed: print("인증 실패 (얼굴/지문 불일치)") case .userCancel: print("사용자가 취소함") case .userFallback: print("비밀번호 입력 선택") case .systemCancel: print("시스템이 취소 (앱 전환 등)") case .biometryLockout: print("너무 많은 시도로 잠김") default: print("인증 오류: \(laError.localizedDescription)") } } return false } } // 사용 예시 let authenticated = await authenticateUser() if authenticated { print("로그인 성공!") }
🔑 기기 암호 포함 인증
PasscodeAuth.swift
import LocalAuthentication func authenticateWithPasscode() async -> Bool { let context = LAContext() // 생체 인증 실패 시 기기 암호 입력 허용 context.localizedFallbackTitle = "암호 입력" do { let success = try await context.evaluatePolicy( .deviceOwnerAuthentication, // 생체 인증 + 기기 암호 localizedReason: "계정에 접근하려면 인증하세요" ) return success } catch { print("인증 실패: \(error.localizedDescription)") return false } } // 커스텀 취소 버튼 텍스트 func authenticateWithCustomButton() async -> Bool { let context = LAContext() context.localizedCancelTitle = "나중에" context.localizedFallbackTitle = "비밀번호로 로그인" do { return try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "민감한 정보에 접근합니다" ) } catch { return false } }
📱 SwiftUI 통합
AuthView.swift
import SwiftUI import LocalAuthentication struct AuthenticationView: View { @State private var isAuthenticated = false @State private var showError = false @State private var errorMessage = "" var body: some View { VStack(spacing: 20) { if isAuthenticated { // 인증 후 화면 VStack(spacing: 16) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 80)) .foregroundStyle(.green) Text("인증 성공") .font(.title) .bold() Text("안전하게 로그인되었습니다") .foregroundStyle(.secondary) Button("로그아웃") { isAuthenticated = false } .buttonStyle(.borderedProminent) } } else { // 로그인 화면 VStack(spacing: 16) { Image(systemName: "faceid") .font(.system(size: 80)) .foregroundStyle(.blue) Text("생체 인증 필요") .font(.title) .bold() Text("Face ID로 안전하게 로그인하세요") .foregroundStyle(.secondary) Button("인증하기") { Task { await authenticate() } } .buttonStyle(.borderedProminent) } } } .padding() .alert("인증 실패", isPresented: $showError) { Button("확인", role: .cancel) { } } message: { Text(errorMessage) } .onAppear { // 앱 시작 시 자동 인증 시도 Task { await authenticate() } } } func authenticate() async { let context = LAContext() var error: NSError? guard context.canEvaluatePolicy( .deviceOwnerAuthentication, error: &error ) else { errorMessage = error?.localizedDescription ?? "인증 불가" showError = true return } do { let success = try await context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: "앱 접근을 위해 인증이 필요합니다" ) await MainActor.run { isAuthenticated = success } } catch let error { await MainActor.run { errorMessage = error.localizedDescription showError = true } } } }
🔒 실전 예제: 민감 정보 보호
SecureDataView.swift
import SwiftUI import LocalAuthentication struct SecureDataView: View { @State private var isUnlocked = false @State private var sensitiveData = "****-****-****-1234" var body: some View { List { Section("계좌 정보") { HStack { Text("계좌번호") Spacer() Text(isUnlocked ? "1234-5678-9012-3456" : "****-****-****-3456") .font(.system(.body, design: .monospaced)) } HStack { Text("잔액") Spacer() Text(isUnlocked ? "1,234,567원" : "*******원") } } Section { Button(isUnlocked ? "정보 숨기기" : "정보 보기") { if isUnlocked { isUnlocked = false } else { Task { await authenticate() } } } } } .navigationTitle("내 계좌") } func authenticate() async { let context = LAContext() do { let success = try await context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: "민감한 계좌 정보를 보려면 인증하세요" ) if success { await MainActor.run { withAnimation { isUnlocked = true } } } } catch { print("인증 실패") } } }
⏱️ 인증 컨텍스트 재사용
ContextReuse.swift
import LocalAuthentication class AuthenticationManager { private let context = LAContext() init() { // 인증 유효 시간 설정 (기본 10초) context.touchIDAuthenticationAllowableReuseDuration = 30 // 30초 } func authenticate(reason: String) async -> Bool { do { // 30초 내 재인증 시 생체 인증 생략 return try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: reason ) } catch { return false } } func invalidate() { // 컨텍스트 무효화 (즉시 재인증 필요) context.invalidate() } } // 사용 예시 let authManager = AuthenticationManager() // 첫 인증 (생체 인증 필요) let success1 = await authManager.authenticate(reason: "결제 승인") // 30초 내 재인증 (생체 인증 생략) let success2 = await authManager.authenticate(reason: "추가 결제") // 로그아웃 시 컨텍스트 무효화 authManager.invalidate()
🎯 고급 기능: 도메인 상태
DomainState.swift
import LocalAuthentication class BiometricStateMonitor { private var domainState: Data? func saveBiometricState() { let context = LAContext() var error: NSError? guard context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) else { return } // 현재 생체 인증 상태 저장 domainState = context.evaluatedPolicyDomainState UserDefaults.standard.set(domainState, forKey: "BiometricState") } func hasBiometricChanged() -> Bool { let context = LAContext() var error: NSError? guard context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) else { return true } // 저장된 상태와 비교 guard let savedState = UserDefaults.standard.data(forKey: "BiometricState") else { return true } let currentState = context.evaluatedPolicyDomainState return savedState != currentState } func handleBiometricChange() { if hasBiometricChanged() { print("⚠️ 생체 인증 정보가 변경되었습니다!") print("보안을 위해 다시 로그인하세요.") // 앱 데이터 초기화 또는 재로그인 요구 logoutUser() saveBiometricState() } } func logoutUser() { // 로그아웃 로직 UserDefaults.standard.set(false, forKey: "IsLoggedIn") } } // 사용 예시 let monitor = BiometricStateMonitor() monitor.saveBiometricState() // 로그인 시 // 앱 시작 시 확인 monitor.handleBiometricChange()
💡 HIG 가이드라인
HIG 권장사항
✅ DO
1. 명확한 이유 제공
- "결제를 승인하려면 Face ID가 필요합니다"
- 사용자에게 왜 인증이 필요한지 명확히 설명
2. 대체 수단 제공
- 생체 인증 실패 시 기기 암호 허용
- .deviceOwnerAuthentication 정책 사용
3. 적절한 타이밍
- 민감한 작업 직전에만 요청
- 너무 자주 요청하지 않기
4. 오류 처리
- 인증 실패 시 명확한 피드백
- 재시도 옵션 제공
❌ DON'T
1. 앱 시작마다 인증 요구 (불필요한 경우)
2. 생체 데이터를 서버로 전송
3. 인증 없이도 기본 기능은 사용 가능하게
4. "Touch ID"라고 명시 (기기별로 다름)🔧 실무 활용 팁
실전 패턴
import LocalAuthentication // 1. 인증 헬퍼 클래스 class BiometricAuth { static let shared = BiometricAuth() private init() {} var biometricType: LABiometryType { let context = LAContext() _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) return context.biometryType } var biometricName: String { switch biometricType { case .faceID: return "Face ID" case .touchID: return "Touch ID" case .opticID: return "Optic ID" case .none: return "생체 인증" @unknown default: return "생체 인증" } } func authenticate(reason: String) async throws -> Bool { let context = LAContext() return try await context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: reason ) } } // 2. SwiftUI에서 사용 struct SettingsView: View { @AppStorage("useBiometric") private var useBiometric = false var body: some View { Form { Section("보안") { Toggle("\(BiometricAuth.shared.biometricName) 사용", isOn: $useBiometric) } } } } // 3. 결제 승인 예시 func processPayment(amount: Int) async -> Bool { do { let authenticated = try await BiometricAuth.shared.authenticate( reason: "\(amount)원 결제를 승인하시겠습니까?" ) if authenticated { // 결제 처리 await performPaymentTransaction(amount) return true } } catch { print("결제 취소됨") } return false } func performPaymentTransaction(_ amount: Int) async { // 실제 결제 API 호출 }
🔐 Info.plist 설정
Info.plist
<!-- Face ID 사용 시 필수 --> <key>NSFaceIDUsageDescription</key> <string>안전한 로그인을 위해 Face ID를 사용합니다</string> <!-- 또는 한국어로 --> <key>NSFaceIDUsageDescription</key> <string>계정 보안을 위해 Face ID 인증이 필요합니다</string>
💡 Local Authentication 핵심
✅ 모든 생체 데이터는 기기 내에서만 처리
✅ Secure Enclave에 안전하게 저장
✅ 생체 인증 실패 시 기기 암호 대체 수단 제공
✅ 앱은 인증 성공/실패만 확인 가능
✅ 실제 생체 데이터는 앱에서 접근 불가