본문 바로가기

iOS/PROJECT

[burstcamp] 유저정보를 효율적으로 관리하는 방법 (with. 파이어베이스, KeyChain)

이미지를 클릭하면 burstcamp github로 이동합니다.

 

 

생각을 정리한 글입니다. 팩트가 아닐 수도 있습니다.

 

유저 정보는 모든 화면에서 쓰이고, 자주 업데이트가 되는 모델입니다.

이 유저 정보를 어떻게 모든 화면에서 접근하고 효율적으로 저장할지 생각의 과정을 적어보겠습니다.


첫번째

만약에 유저 정보가 필요한 곳에서 필요할 때마다 DB에서 유저정보를 호출을 하게된다면?

저희는 Firestore에 유저 정보를 저장하고 업데이트를 하고있습니다.

필요한 곳마다 유저 정보를 호출하면 같은 유저 정보를 받는데 비용이 큰 네트워크 요청이 정말 많아질 것입니다.

 

같은 유저 정보를 저장한다는 의미에 초점을 두면 한번만 불러와서 여러 화면에서 유저정보를 사용하면 될 것이라는 생각이 들었습니다.

 


두번째

싱글톤에 유저 정보를 저장하고 앱 시작할 때 Firestore에서 유저 정보를 가져와 저장하자

 

유저 정보를 파이어베이스에서 앱 시작할 때 불러와서 저장을 합니다.

그렇다면 모든 뷰컨에서 싱글톤의 유저 정보에 접근할 수 있기 때문에 유저 정보 요청을 한번만 하여 비용을 줄일 수 있었습니다.

 

하지만 여러 문제점들이 발생하였습니다.

 

1. 유저 정보가 계속 업데이트 되는데 

- 싱글톤의 정보 업데이트

- DB의 정보 업데이트

이렇게 따로 업데이트를 하면 동기화 문제가 발생할 수 있을 것이라고 생각하여

user 정보에 addSnapshotListener를 추가하여

DB의 정보가 업데이트되면 유저 싱글톤의 정보도 업데이트 될 수 있게 처리하였습니다.

 

2. 앱을 실행할 때 유저 정보를 가져오기 시작하면 네트워크가 느릴 때 View를 그리는 것보다 유저정보가 늦을 수도 있습니다.

이 문제를 처리하기 위해서 퍼블리셔를 적용했습니다.

var userUpdatePublisher = PassthroughSubject<User, Never>()

비동기 처리로 유저 정보가 업데이트가 됐을 때 화면을 업데이트 하기위해 싱글톤에 퍼블리셔를 만들고 

Snapshot Listener 로 업데이트가 된 것을 알았을 때 user를 send하는 코드로 변경하였습니다.

private func addUserListener() {
    guard let userUUID = Auth.auth().currentUser?.uid else { return }
    userPath.document(userUUID)
        .addSnapshotListener { snapshot, error in
            if let error = error {
                print("user 업데이트 실패 \(error.localizedDescription)")
                return
            }
            guard let dictionary = snapshot?.data() else { return }
            let user = User(dictionary: dictionary)
            self.user = user
            self.userUpdatePublisher.send(user)
        }
}

 


세번째

앱 시작할 때 로컬에서 정보를 가져오면 훨씬 빠를텐데..

 

위의 방법으로 대부분의 문제는 해결되었지만 

처음 앱에 접속할 때 네트워크 요청을 기다리는 시간을 줄이고,

유저 정보를 퍼블리셔로 가져오지 않고 싱글톤에서만 바로 접근할 수 있도록 하기위해 키체인을 도입했습니다.

 

(마치 캐시와 비슷하게..)

 

UserDefaults가 아닌 KeyChain을 선택한 이유는 개인정보 보호법을 고려했기 때문입니다.

https://easylaw.go.kr/CSP/CnpClsMain.laf?popMenu=ov&csmSeq=1257&ccfNo=2&cciNo=1&cnpClsNo=1

 

개인정보보호 > 개인정보의 처리단계별 보호방안 > 개인정보의 수집·이용 > 개인정보의 수집·이

개인정보의 처리, 개인정보의 수집·이용, 정보주체의 동의, 목적 외 이용·제공

easylaw.go.kr

 

사실 저희가 저장하는 정보가 엄청나게 중요한 정보다..! (예를들어 닉네임과 공인인증서 중에서는 공인인증서가 더 중요한 정보)

는 아니지만, 개인을 특정할 수 있는 정보라고 생각이 들 수 있기 때문에 KeyChain의 방법을 도입하기로 했습니다.

 

KeyChain에 정보를 저장할 때와 정보를 가져올 때 암호화, 복호화 과정을 거치게 됩니다.

유저의 정보가 자주 업데이트되고, 이를 모두 키체인에 업데이트하면 암,복호화 비용이 너무 많이 들 것이라고 생각했습니다.

 

그래서 앱 종료시 싱글톤의 유저정보를 KeyChain에 저장하고

앱 시작시 싱글톤의 유저정보를 KeyChain의 정보로 저장하는 방법으로 결론을 지었습니다.

 

앱 종료시 유저 정보를 키체인에 저장하기 위해서

AppDelegate의 applicationWillTerminate 함수에서 유저 싱글톤 정보를 저장하도록 했습니다.

extension AppDelegate {
    func applicationWillTerminate(_ application: UIApplication) {
        KeyChainManager.deleteUser()
        KeyChainManager.save(user: UserManager.shared.user)
    }
}

 


회원가입할 때는 uuid가 없어서 앱 시작시 listener를 등록할 수가 없다.

앱 시작시 키체인의 정보를 불러오려고 했지만
회원가입 하기 전에는 uuid 정보가 없기 때문에 Snapshot listener를 등록할 수 없는 문제가 있었습니다.

 

burstcamp는 회원가입을 한 유저만 홈화면을 볼 수 있기 때문에
AppCoordinator의 TabBarFlow를 생성하는 로직에 UserManager를 start 하기로 했습니다.

 

UserManager의 코드입니다.

final class UserManager {

    static let shared = UserManager()

    private(set) var user = User(dictionary: [:])
    private let userPath = Firestore.firestore().collection(FireStoreCollection.user.path)
    let userUpdatePublisher = PassthroughSubject<User, Never>()

    private init() {}

    func start() {
        userByKeyChain()
        addUserListener()
    }

    private func userByKeyChain() {
        if let user = KeyChainManager.readUser() {
            self.user = user
        }
    }

    private func addUserListener() {
        guard let userUUID = Auth.auth().currentUser?.uid else { return }
        userPath.document(userUUID)
            .addSnapshotListener { snapshot, error in
                if let error = error {
                    print("user 업데이트 실패 \(error.localizedDescription)")
                    return
                }
                guard let dictionary = snapshot?.data() else { return }
                let user = User(dictionary: dictionary)
                self.user = user
                self.userUpdatePublisher.send(user)
            }
    }
}

 


키체인을 활용함으로써 유저 정보를 화면에 네트워크 요청 기다림없이 바로 보여줄 수 있었습니다.

 

좀 더 생각해봐야 할 것

keychain에 user 정보를 다 저장하는 비용

SnapshotListener를 대체할만한 것

유저의 개인정보마다 보안 수준을 다르게 처리할 것인지