letsecureKeychain=Keychain(service:"com.example.app").accessibility(.whenPasscodeSetThisDeviceOnly,authenticationPolicy:.biometryCurrentSet)try?secureKeychain.authenticationPrompt("로그인 토큰 접근").set("token-123",key:"biometric_token")// 읽을 때 자동으로 Face ID 프롬프트lettoken=trysecureKeychain.get("biometric_token")
직접 SecItem (의존성 X)
funcsaveToKeychain(_value:String,key:String)->Bool{letdata=Data(value.utf8)letquery:[String:Any]=[kSecClassasString:kSecClassGenericPassword,kSecAttrAccountasString:key,kSecAttrServiceasString:"com.example.app",kSecValueDataasString:data,kSecAttrAccessibleasString:kSecAttrAccessibleAfterFirstUnlock,]SecItemDelete(queryasCFDictionary)// 기존 제거letstatus=SecItemAdd(queryasCFDictionary,nil)returnstatus==errSecSuccess}funcreadKeychain(_key:String)->String?{letquery:[String:Any]=[kSecClassasString:kSecClassGenericPassword,kSecAttrAccountasString:key,kSecAttrServiceasString:"com.example.app",kSecReturnDataasString:true,kSecMatchLimitasString:kSecMatchLimitOne,]varitem:CFTypeRef?guardSecItemCopyMatching(queryasCFDictionary,&item)==errSecSuccess,letdata=itemas?Dataelse{returnnil}returnString(data:data,encoding:.utf8)}
Accessibility 옵션
.whenUnlocked: 잠금 해제 후만.
.afterFirstUnlock: 첫 잠금 해제 후 (재부팅 후 첫 1회만 잠김).
.whenPasscodeSetThisDeviceOnly: passcode 설정 + 디바이스 한정 (백업 안 됨).
.whenUnlockedThisDeviceOnly: 잠금 해제 + 디바이스 한정.
🤔 의사결정 기준
데이터
저장
Auth token (access/refresh)
Keychain — afterFirstUnlock
사용자 비밀번호 (앱 내)
Keychain — whenPasscodeSet + biometric
결제 정보
Apple Pay (Keychain X)
OAuth client secret
❌ 배포 binary 에 박지 마라. PKCE 사용.
로컬 설정 (theme, preference)
UserDefaults
큰 데이터 (이미지, document)
Files / Core Data
❌ 안티패턴
UserDefaults 에 token: plain text, 백업 노출.
NSData 그대로 print log: 비밀 출력.
Keychain access group 미설정 + 같은 팀 다른 앱 공유 기대: 안 됨.
biometric 인증 결과 캐시 + 영구 사용: 사용자 의도와 다름. 매 민감 작업마다 요청.
상수 값을 keychain 에 저장 + 매번 읽기: 성능. 메모리 캐시 + invalidation.