본문 바로가기

Project/MovieClip

👤 Firebase에 이메일 & 비밀번호 회원가입 기능 (MVVM + Combine)

 

🔷 SceneDelegate.swift

🏆 전체 흐름 정리

1️⃣ 앱이 실행되면 scene(_:willConnectTo:options:)이 호출됨.
2️⃣ setupWindow()를 통해 UIWindow를 초기화하고, 화면을 표시할 준비를 함.
3️⃣ checkAuthentication()에서 현재 로그인 상태를 확인함.

  • 로그인이 안 되어 있다면 gotoController(with: OnboardingViewController()) 실행
  • 로그인이 되어 있다면 gotoController(with: MainTabBarController()) 실행

4️⃣ gotoController(with:)가 rootViewController를 변경하고 애니메이션을 적용하여 화면을 전환함.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        
        self.setupWindow(with: scene)
        checkAuthentication()

    }
    
    private func setupWindow(with scene: UIScene) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        let window = UIWindow(windowScene: windowScene)
        self.window = window
        self.window?.makeKeyAndVisible()
    }
    
    // ✅ Auth.auth().currentUser를 확인하여 로그인 상태를 검사
    public func checkAuthentication() {
        if Auth.auth().currentUser == nil {
            self.gotoController(with: UINavigationController(rootViewController: OnboardingViewController()))
        } else {
            
            // ✅ 기존에 MainTabBarController가 있으면 새로 만들지 않음
            if !(window?.rootViewController is MainTabBarController) {
                gotoController(with: MainTabBarController())
            }
        }
    }
    
    
    private func gotoController(with viewController: UIViewController) {
        DispatchQueue.main.async { [weak self] in
            UIView.animate(withDuration: 0.3) {
                self?.window?.layer.opacity = 0
            } completion: { [weak self] _ in
                let vc = viewController
                vc.modalPresentationStyle = .fullScreen
                self?.window?.rootViewController = vc
                
                UIView.animate(withDuration: 0.3) {
                    self?.window?.layer.opacity = 1
                }
            }
        }
    }
}

 

🔷 AuthManager.swift

 

  • Firebase Authentication을 사용하여 이메일 & 비밀번호 기반 회원가입을 수행
  • Combine의 Future를 사용하여 비동기 작업을 AnyPublisher<User, Error>로 변환
  • 성공하면 Firebase User 객체를 반환하고, 실패하면 Error를 반환

 

import Foundation
import Firebase
import FirebaseAuth
import FirebaseAuthCombineSwift
import Combine


class AuthManager {
    
    // MARK: - Variable
    static let shared = AuthManager()
    
    private init() { }
    
    
    // MARK: - Function
    
    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()
    }
}

 

🔷 AuthenticationViewModel.swift

  • Combine + MVVM 패턴을 적용하여 회원가입 로직을 구현
  • @Published 프로퍼티 (뷰와 데이터 바인딩)
  • validateAuthenticationForm() (이메일 & 비밀번호 유효성 검사)
  • createUser() (회원가입 요청)
import Foundation
import FirebaseAuth
import Combine


final class AuthenticationViewModel: ObservableObject {
    
    @Published var email: String?
    @Published var password: String?
    @Published var isAuthenticationFormValid: Bool = false
    @Published var user: User?
    @Published var error: String?
    
    private var cancelable: Set<AnyCancellable> = []
    
    // MARK: - Function
    
    /// 이메일, 비밀번호 입력 유효성 검사 메서드 
    func validateAuthenticationForm() {
        guard let email = email,
              let password = password else {
                isAuthenticationFormValid = false
                  return }
        
        isAuthenticationFormValid = isValidEmail(email) && password.count >= 8
    }
    
    /// email 양식 검토 메서드
    func isValidEmail(_ email: String) -> Bool {
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"

        let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: email)
    }
    
    /// 이메일, 비밀번호로 회원가입하는 메서드 
    func createUser() {
        guard let email = email,
              let password = password else { return }
        
        AuthManager.shared.registerUser(email: email, password: password)
            .sink { [weak self] completion in
                switch completion {
                case .failure(let error):
                    self?.error = error.localizedDescription
                case .finished:
                    print("회원가입 성공")
                }
            } receiveValue: { [weak self] user in
                self?.user = user
            }
            .store(in: &cancelable)

    }
}

 

 

🔷 RegisterViewController.swift

// MARK: - Function
    private func bindViews() {
        emailTextField.addTarget(self, action: #selector(didChangedEmailField), for: .editingChanged)
        passwordTextField.addTarget(self, action: #selector(didChangedPassword), for: .editingChanged)
        registerButton.addTarget(self, action: #selector(didTapRegister), for: .touchUpInside)
        
        viewModel.$isAuthenticationFormValid
            .sink { [weak self] validationState in
                self?.registerButton.isEnabled = validationState
            }
            .store(in: &cancelable)
        
        // 회원가입 성공에 따른 화면 이동
        viewModel.$user
            .sink { [weak self] user in
                guard user != nil else { return }
                
                // 현재 RegisterViewController가 띄워진 모든 모달을 닫음 (Onboarding 포함)
                self?.view.window?.rootViewController?.dismiss(animated: true, completion: {
                    // ✅ 모든 화면을 닫은 후, rootViewController를 변경 (MainTabBarController로)
                    if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate {
                        sceneDelegate.window?.rootViewController = MainTabBarController()
                    }
                })
            }
            .store(in: &cancelable)
    }
    
    /// 빈 곳을 누르면 키보드 내려가게 하는 메서드
    private func resigneKeyboard() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        view.addGestureRecognizer(tapGesture)
    }
    
    
    // MARK: - Action
    @objc private func didChangedEmailField() {
        viewModel.email = emailTextField.text
        viewModel.validateAuthenticationForm()
    }
    
    
    @objc private func didChangedPassword() {
        viewModel.password = passwordTextField.text
        viewModel.validateAuthenticationForm()
    }