문제: 계정 전환 후 이전 계정 데이터 노출
같은 디바이스에서 다른 계정으로 로그인할 수 있는 앱에서, 로그아웃 시 UserDefaults 토큰만 삭제하면 SwiftData 로컬 DB가 그대로 남습니다. 다음 사용자가 로그인하면 이전 사용자의 자녀 정보와 일정이 화면에 잠깐 노출되는 심각한 프라이버시 버그가 발생했습니다.
해결: 로그아웃 시 4단계 완전 초기화
// AuthService.swift
func logout() async {
// 1. 서버에 로그아웃 알림 (토큰 무효화)
try? await APIClient.shared.request("auth/logout", method: "POST")
// 2. Keychain 전체 삭제
KeychainManager.deleteAll()
// 3. UserDefaults 초기화
let domain = Bundle.main.bundleIdentifier!
UserDefaults.standard.removePersistentDomain(forName: domain)
// 4. SwiftData LocalDB wipe
await MainActor.run {
do {
let context = ModelContext.shared
try context.delete(model: LocalChild.self)
try context.delete(model: LocalSchedule.self)
try context.delete(model: LocalAcademy.self)
try context.save()
} catch {
// 실패 시 저장소 파일 직접 삭제
resetSwiftDataStore()
}
}
// 5. TCA Store 리셋
await store.send(.logout).finish()
}
Keychain 단일 저장소 통일
초기에는 토큰을 UserDefaults와 Keychain 두 곳에 저장했습니다. 로그아웃 시 한 곳만 삭제하면 다른 곳에 남아있는 문제가 생겼습니다. 해결책은 Keychain 단일 저장소로 통일하는 것입니다.
enum KeychainManager {
static let service = "com.ivent.app"
static func save(_ value: String, for key: String) {
let data = value.data(using: .utf8)!
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: key,
kSecValueData: data
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
static func load(for key: String) -> String? {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: key,
kSecReturnData: true
]
var result: AnyObject?
SecItemCopyMatching(query as CFDictionary, &result)
return (result as? Data).flatMap { String(data: $0, encoding: .utf8) }
}
static func deleteAll() {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service
]
SecItemDelete(query as CFDictionary)
}
}
userId 기반 데이터 격리 (이중 방어)
@Model class LocalChild {
var id: String
var userId: String // 계정 격리를 위한 필드
var name: String
}
// 조회 시 항상 userId 필터 적용
let userId = KeychainManager.load(for: "IVENT_USER_ID") ?? ""
let descriptor = FetchDescriptor<LocalChild>(
predicate: #Predicate { $0.userId == userId }
)
교훈
다중 계정 앱에서 로그아웃은 토큰 삭제가 아닙니다. 로컬 DB, 캐시, UserDefaults, Keychain의 모든 레이어에서 이전 사용자 데이터를 완전히 제거해야 합니다. userId 기반 격리를 이중으로 적용하는 것이 안전합니다.