본문 바로가기

UIKIT/Firebase

🔥 MVVM + Combine을 활용한 소셜 로그인 & 이메일 회원가입

https://explorer89.tistory.com/360

 

🔥 Firebase의 회원가입 (이메일&비밀번호 / 소셜로그인)

// ✅ 소셜 로그인 (Facebook, Google, Apple 등)func signIn(with credential: AuthCredential) -> AnyPublisher { return Future { promise in Auth.auth().signIn(with: credential) { authResult, error in if let error = error { promise(.failure(error)) //

explorer89.tistory.com

 

✅ 1. AuthenticationService (Firebase 연동)

우선, Firebase Authentication을 처리할 AuthenticationService 클래스를 만든다.
이 클래스는 Firebase API를 Combine을 이용해서 Publisher로 변환하는 역할을 한다.

import FirebaseAuth
import Combine

final class AuthenticationService {
    
    static let shared = AuthenticationService()
    private init() {}
    
    /// 📌 이메일과 비밀번호로 회원가입
    func registerUser(email: String, password: String) -> AnyPublisher<User, Error> {
        return Future { promise in
            Auth.auth().createUser(withEmail: email, password: password) { authResult, error in
                if let error = error {
                    promise(.failure(error))  // ❌ 실패 시 에러 반환
                } else if let user = authResult?.user {
                    promise(.success(user))   // ✅ 성공 시 User 반환
                }
            }
        }
        .eraseToAnyPublisher()
    }
    
    /// 📌 소셜 로그인 (Google, Apple 등)
    func signIn(with credential: AuthCredential) -> AnyPublisher<User, Error> {
        return Future { promise in
            Auth.auth().signIn(with: credential) { authResult, error in
                if let error = error {
                    promise(.failure(error))  // ❌ 실패 시 에러 반환
                } else if let user = authResult?.user {
                    promise(.success(user))   // ✅ 성공 시 User 반환
                }
            }
        }
        .eraseToAnyPublisher()
    }
}

 

 

✅ 2. AuthViewModel (비즈니스 로직 & 상태 관리)

AuthenticationService를 사용해서 회원가입과 로그인을 처리하는 AuthViewModel을 만든다.
@Published를 이용해 UI가 자동으로 업데이트되도록 한다.

import Combine
import FirebaseAuth

final class AuthViewModel: ObservableObject {
    
    @Published var user: User? = nil  // 현재 로그인한 사용자 정보
    @Published var errorMessage: String? = nil // 에러 메시지
    
    private var cancellables = Set<AnyCancellable>()
    
    /// 📌 이메일 회원가입
    func register(email: String, password: String) {
        AuthenticationService.shared.registerUser(email: email, password: password)
            .receive(on: DispatchQueue.main)  // UI 업데이트는 메인 스레드에서
            .sink { completion in
                switch completion {
                case .failure(let error):
                    self.errorMessage = "회원가입 실패: \(error.localizedDescription)"
                case .finished:
                    print("✅ 회원가입 성공!")
                }
            } receiveValue: { user in
                self.user = user
            }
            .store(in: &cancellables)
    }
    
    /// 📌 소셜 로그인
    func signInWithCredential(_ credential: AuthCredential) {
        AuthenticationService.shared.signIn(with: credential)
            .receive(on: DispatchQueue.main)
            .sink { completion in
                switch completion {
                case .failure(let error):
                    self.errorMessage = "로그인 실패: \(error.localizedDescription)"
                case .finished:
                    print("✅ 소셜 로그인 성공!")
                }
            } receiveValue: { user in
                self.user = user
            }
            .store(in: &cancellables)
    }
}

 

 

🔥 MVVM + Combine에서 enum을 사용한 로그인/회원가입 통합 처리

 

✅ 1. AuthMethod (인증 방식 구분)

import FirebaseAuth

/// 로그인 / 회원가입 방식을 구분하는 enum
enum AuthMethod {
    case email(email: String, password: String)  // 이메일 회원가입/로그인
    case social(credential: AuthCredential)     // 소셜 로그인 (Google, Apple 등)
}

 

✅ 2. AuthenticationService (Firebase 연동)

Firebase 인증 API를 Combine과 함께 사용하여, AuthMethod를 활용한 단일 메서드로 로그인 & 회원가입을 통합 처리한다.

import FirebaseAuth
import Combine

final class AuthenticationService {
    
    static let shared = AuthenticationService()
    private init() {}

    /// `AuthMethod`를 이용해 이메일 또는 소셜 로그인 처리
    func signInOrRegister(using method: AuthMethod) -> AnyPublisher<User, Error> {
        switch method {
        case .email(let email, let password):
            return registerUser(with: email, password: password)
        case .social(let credential):
            return signIn(with: credential)
        }
    }
    
    /// 이메일 & 비밀번호 회원가입
    private func registerUser(with email: String, password: String) -> AnyPublisher<User, Error> {
        return Future { promise in
            Auth.auth().createUser(withEmail: email, password: password) { authResult, error in
                if let error = error {
                    promise(.failure(error))  // ❌ 실패 시 에러 반환
                } else if let user = authResult?.user {
                    promise(.success(user))   // ✅ 성공 시 User 반환
                }
            }
        }
        .eraseToAnyPublisher()
    }
    
    /// 소셜 로그인 (Google, Apple 등)
    private func signIn(with credential: AuthCredential) -> AnyPublisher<User, Error> {
        return Future { promise in
            Auth.auth().signIn(with: credential) { authResult, error in
                if let error = error {
                    promise(.failure(error))  // ❌ 실패 시 에러 반환
                } else if let user = authResult?.user {
                    promise(.success(user))   // ✅ 성공 시 User 반환
                }
            }
        }
        .eraseToAnyPublisher()
    }
}

 

✅ 3. AuthViewModel (비즈니스 로직 & 상태 관리)

AuthMethod를 사용해서 signInOrRegister(using:) 메서드를 호출한다.

import Combine
import FirebaseAuth

final class AuthViewModel: ObservableObject {
    
    @Published var user: User? = nil  // 현재 로그인한 사용자 정보
    @Published var errorMessage: String? = nil // 에러 메시지
    
    private var cancellables = Set<AnyCancellable>()
    
    /// `AuthMethod`를 사용한 로그인 / 회원가입 처리
    func authenticate(using method: AuthMethod) {
        AuthenticationService.shared.signInOrRegister(using: method)
            .receive(on: DispatchQueue.main)  // UI 업데이트는 메인 스레드에서
            .sink { completion in
                switch completion {
                case .failure(let error):
                    self.errorMessage = "❌ 로그인/회원가입 실패: \(error.localizedDescription)"
                case .finished:
                    print("✅ 로그인/회원가입 성공!")
                }
            } receiveValue: { user in
                self.user = user
            }
            .store(in: &cancellables)
    }
}