๐ŸŒ™

[Swift] KeychainManager

yeggrrr๐Ÿผ 2025. 9. 21. 19:06
728x90

 

 



iOS ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด ์•ฑ ๋‚ด์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.
๋‹จ์ˆœํ•œ ์„ค์ • ๊ฐ’์€ UserDefaults๋กœ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ,
์—‘์„ธ์Šค ํ† ํฐ์ด๋‚˜ ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด์ฒ˜๋Ÿผ ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋Š” ๋ณด์•ˆ์ด ๊ฐ•ํ™”๋œ Keychain์— ์ €์žฅํ•˜๋ฉด ์ข‹๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๊ธฐ์กด์— ๋งŒ๋“ค์–ด๋‘” UserDefaultsManager์™€ ์œ ์‚ฌํ•œ ํŒจํ„ด์œผ๋กœ,
Keychain์„ ์กฐ๊ธˆ ๋” ์†์‰ฝ๊ฒŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋Š” KeychainManager๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค :)






Keychain์€ ์‹œ์Šคํ…œ์—์„œ ์ง์ ‘ ์•”ํ˜ธํ™”ํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋Š” ์•ˆ์ „ํ•œ ์ €์žฅ์†Œ์ง€๋งŒ,
API๊ฐ€ ๋‹ค์†Œ ๋ณต์žกํ•˜๊ณ  ๋ฐ˜๋ณต์ ์ธ ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
๋”ฐ๋ผ์„œ UserDefaults์ฒ˜๋Ÿผ ๊ฐ„๋‹จํ•˜๊ฒŒ '์ €์žฅ, ์ฝ๊ธฐ, ์‚ญ์ œ' ๋ฉ”์„œ๋“œ๋งŒ ํ˜ธ์ถœํ•˜๋ฉด ๋˜๋„๋ก ๋ž˜ํ•‘ํ•˜๋ฉด
์‹ค๋ฌด์—์„œ ํ›จ์”ฌ ์‚ฌ์šฉํ•˜๊ธฐ ํŽธ๋ฆฌํ•  ๊ฒƒ ๊ฐ™์•˜๋‹ค.

์ด๋ฒˆ์— ๊ตฌํ˜„ํ•œ KeychainManager๋Š” Enum ํ‚ค ๊ธฐ๋ฐ˜์œผ๋กœ
ํƒ€์ž… ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ•˜๋ฉด์„œ๋„, ๋ถˆํ•„์š”ํ•œ ์ค‘๋ณต ์ฝ”๋“œ๋ฅผ ์ตœ์†Œํ™”ํ•˜๋„๋ก ๊ตฌ์„ฑํ–ˆ๋‹ค.

<KeyChainManager>

import Security
import Foundation

struct KeychainManager {
    enum Keys: String {
        case accessToken
        case refreshToken
        case userIdentifier
        case email
        case password
    }

    // ์ €์žฅ
    static func save(_ value: String, for key: Keys) -> Bool {
        guard let data = value.data(using: .utf8) else { return false }
        
        // ๊ธฐ์กด ๊ฐ’ ์ œ๊ฑฐ
        delete(for: key)
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue,
            kSecValueData as String: data
        ]
        
        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }

    // ์ฝ๊ธฐ
    static func load(for key: Keys) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        
        guard status == errSecSuccess,
              let data = item as? Data,
              let value = String(data: data, encoding: .utf8) else {
            return nil
        }
        
        return value
    }

    // ์‚ญ์ œ
    static func delete(for key: Keys) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess
    }
}

 

<์‚ฌ์šฉ ์˜ˆ์‹œ>

// ์ €์žฅ
KeychainManager.save("my_access_token", for: .accessToken)

// ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
if let token = KeychainManager.load(for: .accessToken) {
    print("ํ† ํฐ: \(token)")
}

// ์‚ญ์ œ
KeychainManager.delete(for: .accessToken)

๊ตฌํ˜„ํ•œ ์ฝ”๋“œ๋ฅผ ๊ฐ„๋žตํ•˜๊ฒŒ ์„ค๋ช…ํ•˜์ž๋ฉด,
Keychain์— ์ €์žฅํ•  ํ•ญ๋ชฉ๋“ค์„ Enum์œผ๋กœ ์ •์˜ํ–ˆ๋‹ค.

enum Keys: String {
    case accessToken
    case refreshToken
    case userIdentifier
    case email
    case password
}

๊ทธ๋ฆฌ๊ณ  ๋ฌธ์ž์—ด ๊ฐ’์„ ์ €์žฅํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

static func save(_ value: String, for key: Keys) -> Bool {
    guard let data = value.data(using: .utf8) else { return false }
    
    // ๊ธฐ์กด ๊ฐ’ ์ œ๊ฑฐ
    delete(for: key)
    
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key.rawValue,
        kSecValueData as String: data
    ]
    
    let status = SecItemAdd(query as CFDictionary, nil)
    return status == errSecSuccess
}

๋‚ด๋ถ€์— String์„ Data ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , ๋™์ผํ•œ ํ‚ค๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•œ๋‹ค๋ฉด ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์‚ญ์ œํ•˜๋„๋ก ๊ตฌ์„ฑํ–ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  SecItemAdd๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Keychain์— ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค. (์„ฑ๊ณต ์—ฌ๋ถ€๋Š” Bool๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜)


๋‹ค์Œ์œผ๋กœ ์ฝ๊ธฐ(์กฐํšŒ) ๋ฉ”์„œ๋“œ๋Š”

static func load(for key: Keys) -> String? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key.rawValue,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]
    
    var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &item)
    
    guard status == errSecSuccess,
          let data = item as? Data,
          let value = String(data: data, encoding: .utf8) else {
        return nil
    }
    
    return value
}

SecItemCopyMatching์„ ํ†ตํ•ด key์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , 
์ฐพ์€ ๊ฐ’์„ Data์—์„œ String์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (๋งŒ์•ฝ ๊ฐ’์ด ์—†๊ฑฐ๋‚˜ ์‹คํŒจํ•  ๊ฒฝ์šฐ nil ๋ฐ˜ํ™˜)


๋งˆ์ง€๋ง‰์œผ๋กœ ์‚ญ์ œ ๋ฉ”์„œ๋“œ๋Š”

static func delete(for key: Keys) -> Bool {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key.rawValue
    ]
    
    let status = SecItemDelete(query as CFDictionary)
    return status == errSecSuccess
}

์‚ญ์ œ ๋Œ€์ƒ(key)์„ ์ง€์ •ํ•˜๋ฉด, SecItemDelete๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ•ด๋‹น ๊ฐ’์„ ์‚ญ์ œํ•œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์„ฑ๊ณต ์‹คํŒจ ์—ฌ๋ถ€๋ฅผ Bool๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 





์œ„์™€ ๊ฐ™์ด ๊ตฌ์„ฑํ•˜๊ฒŒ ๋˜๋ฉด,
ํ”„๋กœ์ ํŠธ ์ „๋ฐ˜์—์„œ Keychain์„ ํ™œ์šฉํ•  ๋•Œ ๋‹จ์ˆœํ•˜๊ณ  ์ผ๊ด€๋œ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฌผ๋ก  ์—ฌ๊ธฐ์„œ๋Š” ๊ธฐ๋ณธ์ ์ธ ์ €์žฅ·์กฐํšŒ·์‚ญ์ œ ๊ธฐ๋Šฅ๋งŒ ๊ตฌํ˜„ํ–ˆ์ง€๋งŒ,
ํ•„์š”ํ•˜๋‹ค๋ฉด ์ƒ์ฒด์ธ์ฆ ์˜ต์…˜, Keychain ๊ณต์œ  ๊ทธ๋ฃน, ์ ‘๊ทผ์„ฑ ์„ค์ • ๋“ฑ์„ ํ™•์žฅํ•˜์—ฌ ์ ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

์ด๋ฒˆ์— ๊ตฌ์„ฑํ•œ ๊ตฌ์กฐ๋Š”
Keychain์˜ ๋ณต์žกํ•œ API๋ฅผ ๊ฐ์‹ธ์„œ Swfit์—์„œ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ถ”์ƒํ™”ํ•œ ๊ตฌ์กฐ์ด๋‹ค.



 

 

728x90

'๐ŸŒ™' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Swift] Keychain  (0) 2025.09.14