본문 바로가기

Project/MovieClip

📌 회원탈퇴 구현 (Storage, Firestore Database)

 

✅ 회원 탈퇴 로직 

 

  1. Firebase Storage에서 프로필 이미지 삭제
    → deleteProfilePhoto(for: userID)
    이미지가 없으면 실패해도 계속 진행 (catch { _ in Just(()) })
  2. Firestore에서 유저 데이터 삭제
    → collectionUsers(deleteUser: userID)
  3. Firebase Authentication에서 유저 계정 삭제
    → deleteAccount()

 

 

1️⃣ Firebase Storage 에서 프로필 이미지 삭제

final class StorageManager {
    
    
    // MARK: - Variable
    static let shared = StorageManager()
    
    let storage = Storage.storage()
    
    
    // MARK: - Function
    /// 프로필 이미지를 firestore에 저장하는 메서드
    func uploadProfilePhoto(with userID: String, image: Data, metaData: StorageMetadata) -> AnyPublisher<StorageMetadata, Error> {
        ...
    }
    
    
    func getDownloadURL(for id: String?) -> AnyPublisher<URL, Error> {
        guard let id = id else { return Fail(error: FireStorageError.invalidImageID).eraseToAnyPublisher() }
        
        return storage
            .reference(withPath: id)
            .downloadURL()
            .print()
            .eraseToAnyPublisher()
    }
    
    
    /// ✅ 특정 유저 ID를 받아 해당 유저의 프로필 이미지를 Firebase Storage에서 삭제
    func deleteProfilePhoto(for userID: String) -> AnyPublisher<Void, Error> {
        return getDownloadURL(for: "images/\(userID)/profileImage/profileImage.jpg") // ✅ Firebase에서 이미지 URL 가져오기
            .flatMap { imageURL in
                let reference = Storage.storage().reference(forURL: imageURL.absoluteString)
                
                return Future<Void, Error> { promise in
                    reference.delete { error in
                        if let error = error {
                            promise(.failure(error))
                        } else {
                            promise(.success(()))  // ✅ 성공 시 Void 반환
                        }
                    }
                }
            }
            .catch { _ in Just(()).setFailureType(to: Error.self) } // ✅ 이미지가 없을 경우 삭제 실패해도 계속 진행
            .eraseToAnyPublisher()
    }
}

 

flatMap을 사용하면 getDownloadURL()의 결과를 받은 후, 그 값을 사용하여 새로운 비동기 작업(Publisher)을 생성할 수 있다.
✅ Firebase에서 URL을 받아오는 작업과, 해당 URL을 사용한 삭제 작업을 순차적으로 실행할 수 있도록 보장한다.
✅ 이 방법이 없으면 reference.delete()를 바로 실행할 수 없고, 비동기 흐름이 깨지게 된다.

 

 

2️⃣ DatabaseManager - Firestore 에서 유저 데이터 삭제 

class DatabaseManager {
    
    
    // MARK: - Variable
    static let shared = DatabaseManager()
    
    let db = Firestore.firestore()
    let userPath: String = "users"
    
    
    // MARK: - Function
    /// 회원정보를 fireStore에 저장하는 메서드
    func collectionUsers(add user: User) -> AnyPublisher<Bool, Error> {
        ...
    }
    
    /// 회원정보를 반환하는 메서드
    func collectionUsers(retrieve id: String) -> AnyPublisher<MovieClipUser, Error> {
        ...
    }
    
    
    func collectionUsers(updateFields: [String: Any], for id: String) -> AnyPublisher<Bool, Error> {
        ...
    }
    
    /// ✅ 특정 유저 ID를 받아 해당 유저의 Firestore 데이터를 삭제
    func collectionUsers(deleteUser userID: String) -> AnyPublisher<Void, Error> {
        return Future<Void, Error> { promise in
            self.db.collection(self.userPath).document(userID).delete { error in
                if let error = error {
                    promise(.failure(error))
                } else {
                    promise(.success(()))
                }
            }
        }
        .eraseToAnyPublisher()
    }
}

 

 

3️⃣ AuthManager - Firebase Authentication에서 회원 탈퇴

class AuthManager {
    
    // MARK: - Variable
    static let shared = AuthManager()
    
    private init() { }
    
    
    // MARK: - Function
    
    /// 이메일과 비밀번호로 회원가입 하는 함수 <Future는 필수가 아님>
    func registerUser(email: String, password: String) -> AnyPublisher<User, Error> {
        ...
    }
    
    /// 이메일과 비밀번호로 로그인하는 함수
    func loginUser(email: String, password: String) -> AnyPublisher<User, Error> {
		...
    }
    
    
    /// ✅ Firebase Authentication에서 회원 탈퇴
    func deleteUser() -> AnyPublisher<Void, Error> {
        guard let user = Auth.auth().currentUser else {
            return Fail(error: NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "사용자가 로그인되어 있지 않습니다."]))
                .eraseToAnyPublisher()
        }
        
        return Future<Void, Error> { promise in
            user.delete { error in
                if let error = error {
                    promise(.failure(error))
                } else {
                    promise(.success(()))
                }
            }
        }
        .eraseToAnyPublisher()
    }

}

 

 

4️⃣ ProfileViewModel - 회원 탈퇴 기능 구현

final class ProfileViewModel: ObservableObject {
    
    @Published var user: MovieClipUser   // ✅ 유저 정보 (초기값 기본값으로 설정)
    @Published var error: String?
    @Published var isUserDeleted: Bool = false  // ✅ 회원 탈퇴 완료 여부
    
    
    private var cancellable: Set<AnyCancellable> = []
    
    // ✅ 빈 유저로 초기화 후, Firebas에서 업데이트
    init(user: MovieClipUser = MovieClipUser()) {
        self.user = user
        retreiveUser()
    }
    
    
    func retreiveUser() {
        ...
    }
    
    // ✅ 회원 탈퇴 기능 - Firebase Storage, Firestore, Authentication 순차적 삭제
    func deleteUser() {
        guard let userID = Auth.auth().currentUser?.uid else {
            self.error = "유저 정보 없음"
            return
        }
        
        StorageManager.shared.deleteProfilePhoto(for: userID)
            .flatMap { DatabaseManager.shared.collectionUsers(deleteUser: userID) }
            .flatMap { AuthManager.shared.deleteUser() }
            .sink { [weak self] completion in
                switch completion {
                case .failure(let error):
                    self?.error = "회원 탈퇴 실패: \(error.localizedDescription)"
                case .finished:
                    self?.isUserDeleted = true  // 회원 탈퇴 완료 처리
                }
            } receiveValue: { }
            .store(in: &cancellable)
    }
}

 

5️⃣ ProfileViewController 에서 회원 탈퇴 실행

private func deleteUser() {
    viewModel.deleteUser()

    viewModel.$isUserDeleted
        .sink { isDeleted in
            if isDeleted {
                print("회원 탈퇴")
                
                // ✅ 기존의 모든 화면을 닫고, OnboardingViewController로 이동
                DispatchQueue.main.async {
                    self.view.window?.rootViewController?.dismiss(animated: true, completion: {
                        if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate {
                            sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: OnboardingViewController())
                        }
                    })
                }
            }
        }
        .store(in: &cancelable)
}

 

 

🔥 최종 결과

  1. Firebase Storage의 프로필 이미지 삭제
  2. Firestore 유저 데이터 삭제
  3. Firebase Authentication에서 회원 탈퇴
  4. 탈퇴 후 온보딩 화면으로 이동