๐ŸŒ KO

๐Ÿ” Local Authentication ์™„์ „์ •๋ณต

Secure your app with Face ID and Touch ID! Complete guide from biometric auth to password replacement.

โญ Difficulty: โญ โฑ๏ธ Est. Time: 30min ๐Ÿ“‚ System & Network

โœจ Local Authentication is?

Local Authentication is a framework that authenticates users using Face ID, Touch ID, or device passcode. It provides safe and convenient authentication for login, payment authorization, and accessing sensitive information. All biometric data is processed on-device only and never sent to a server.

๐Ÿ“ฆ Key Features

Feature Overview
โœ… 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 Integration

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
            }
        }
    }
}

๐Ÿ”’ Practical Example: Protecting Sensitive Data

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()

๐ŸŽฏ Advanced Features: ๋„๋ฉ”์ธ ์ƒํƒœ

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 Guidelines

HIG Recommendations
โœ… DO
1. ๋ช…ํ™•ํ•œ ์ด์œ  ์ œ๊ณต
   - "๊ฒฐ์ œ๋ฅผ ์Šน์ธํ•˜๋ ค๋ฉด Face ID๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"
   - ์‚ฌ์šฉ์ž์—๊ฒŒ ์™œ ์ธ์ฆ์ด ํ•„์š”ํ•œ์ง€ ๋ช…ํ™•ํžˆ ์„ค๋ช…

2. ๋Œ€์ฒด ์ˆ˜๋‹จ ์ œ๊ณต
   - ์ƒ์ฒด ์ธ์ฆ ์‹คํŒจ ์‹œ ๊ธฐ๊ธฐ ์•”ํ˜ธ ํ—ˆ์šฉ
   - .deviceOwnerAuthentication ์ •์ฑ… ์‚ฌ์šฉ

3. ์ ์ ˆํ•œ ํƒ€์ด๋ฐ
   - ๋ฏผ๊ฐํ•œ ์ž‘์—… ์ง์ „์—๋งŒ ์š”์ฒญ
   - ๋„ˆ๋ฌด ์ž์ฃผ ์š”์ฒญํ•˜์ง€ ์•Š๊ธฐ

4. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
   - ์ธ์ฆ ์‹คํŒจ ์‹œ ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ
   - ์žฌ์‹œ๋„ ์˜ต์…˜ ์ œ๊ณต

โŒ DON'T
1. ์•ฑ ์‹œ์ž‘๋งˆ๋‹ค ์ธ์ฆ ์š”๊ตฌ (๋ถˆํ•„์š”ํ•œ ๊ฒฝ์šฐ)
2. ์ƒ์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„๋กœ ์ „์†ก
3. ์ธ์ฆ ์—†์ด๋„ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์€ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ
4. "Touch ID"๋ผ๊ณ  ๋ช…์‹œ (๊ธฐ๊ธฐ๋ณ„๋กœ ๋‹ค๋ฆ„)

๐Ÿ”ง Practical Tips

Real-World Patterns
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 Configuration

Info.plist
<!-- Face ID ์‚ฌ์šฉ ์‹œ ํ•„์ˆ˜ -->
<key>NSFaceIDUsageDescription</key>
<string>์•ˆ์ „ํ•œ ๋กœ๊ทธ์ธ์„ ์œ„ํ•ด Face ID๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค</string>

<!-- ๋˜๋Š” ํ•œ๊ตญ์–ด๋กœ -->
<key>NSFaceIDUsageDescription</key>
<string>๊ณ„์ • ๋ณด์•ˆ์„ ์œ„ํ•ด Face ID ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค</string>

๐Ÿ’ก Local Authentication ํ•ต์‹ฌ
โœ… ๋ชจ๋“  ์ƒ์ฒด ๋ฐ์ดํ„ฐ๋Š” ๊ธฐ๊ธฐ ๋‚ด์—์„œ๋งŒ ์ฒ˜๋ฆฌ
โœ… Secure Enclave์— ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ
โœ… ์ƒ์ฒด ์ธ์ฆ ์‹คํŒจ ์‹œ ๊ธฐ๊ธฐ ์•”ํ˜ธ ๋Œ€์ฒด ์ˆ˜๋‹จ ์ œ๊ณต
โœ… ์•ฑ์€ ์ธ์ฆ ์„ฑ๊ณต/์‹คํŒจ๋งŒ ํ™•์ธ ๊ฐ€๋Šฅ
โœ… ์‹ค์ œ ์ƒ์ฒด ๋ฐ์ดํ„ฐ๋Š” ์•ฑ์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€

๐Ÿ“ฆ Learning Resources

๐Ÿ’ป
GitHub Project
๐Ÿ“–
Apple Official Docs
๐ŸŽจ
HIG ์ธ์ฆ ๊ฐ€์ด๋“œ

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐ŸŽฌ WWDC Sessions