[SwiftUI] Firebase로 로그인, 회원가입 구현

꾸Jun·2024년 11월 16일
0

🍎 iOS

목록 보기
15/15

사진 출처

내가 구현한 로그인, 회원가입은 MVVM (Model-View-ViewModel) 아키텍처 패턴을 따르고 있다. 이러한 구조를 선택한 이유는 다음과 같다.

  1. 관심사의 분리: 데이터 모델, 비즈니스 로직, UI를 분리하여 코드의 가독성과 유지보수성을 향상시킨다.
  2. 테스트 용이성: ViewModel을 독립적으로 테스트할 수 있어 단위 테스트가 쉬워진다.
  3. SwiftUI와의 호환성: SwiftUI는 MVVM 패턴과 잘 어울리며, @Observable 속성을 통해 데이터 바인딩을 쉽게 구현할 수 있다.
  4. 재사용성: 각 컴포넌트를 독립적으로 개발하고 재사용할 수 있다.


코드 구현

1. User.swift(Model)

//
//  User.swift
//  InstagramClone
//
//  Created by JunKyu Lee on 11/6/24.
//

import Foundation

struct User: Codable {
    var userId: String
    var email: String
    var userName: String
    var name: String
    var bio: String?
    var profileImageUrl: String?
}

User 모델은 사용자 정보를 저장하는 구조체다. Codable 프로토콜을 채택하여 Firebase Firestore와의 데이터 직렬화/역직렬화를 쉽게 한다.


2. LoginViewModel.swift (ViewModel)

//
//  LoginVIewModel.swift
//  InstagramClone
//
//  Created by JunKyu Lee on 11/16/24.
//

import Foundation

@Observable
class LoginViewModel {
    var email: String = ""
    var password: String = ""
    
    func signin() async {
        await AuthManager.shared.signin(email: email, password: password)
    }
}

LoginViewModel은 로그인 화면의 상태를 관리한다. @Observable 속성을 사용하여 SwiftUI 뷰와의 데이터 바인딩을 쉽게 한다.


3. SignUpViewModel.swift (ViewModel)

//
//  SignUpViewModel.swift
//  InstagramClone
//
//  Created by JunKyu Lee on 11/1/24.
//

import Foundation
import FirebaseAuth

@Observable
class SignUpViewModel {
    var email: String = ""
    var password: String = ""
    var name: String = ""
    var userName: String = ""
    
    func createUser() async {
        await AuthManager.shared.createUser(email: email, password: password, name: name, userName: userName)
        email = ""
        password = ""
        name = ""
        userName = ""
    }
}

SignUpViewModel은 회원가입 화면의 상태를 관리한다. createUser() 메서드는 AuthManager를 통해 새 사용자를 생성하고, 작업이 완료되면 입력 필드를 초기화한다.


4. AuthManager.swift (Service)

//
//  AuthManager.swift
//  InstagramClone
//
//  Created by JunKyu Lee on 11/6/24.
//

import Foundation
import FirebaseAuth
import Firebase

@Observable
class AuthManager {
    static let shared: AuthManager = AuthManager()
    
    var currentUserSession: FirebaseAuth.User?
    
    init() {
        self.currentUserSession = Auth.auth().currentUser
    }
    
    func createUser(email: String, password: String, name: String, userName: String) async {
        print("email: ", email)
        print("password: ", password)
        print("name: ", name)
        print("userName: ", userName)
        
        do {
            let result: AuthDataResult = try await Auth.auth().createUser(withEmail: email, password: password)
            currentUserSession = result.user
            guard let userId: String = currentUserSession?.uid else { return }
            await uploadUserData(userId: userId, email: email, userName: userName, name: name)
        } catch {
            print("DEBUG: Failed to create user with error \(error.localizedDescription)")
        }
    }
    
    func uploadUserData(userId: String, email: String, userName: String, name: String) async {
        let user: User = User(userId: userId, email: email, userName: userName, name: name)
        do {
            let encodedUser = try Firestore.Encoder().encode(user)
            try await Firestore.firestore().collection("users").document(user.userId).setData(encodedUser)
        } catch {
            print("DEBUG: Failed to upload user data with error \(error.localizedDescription)")
        }
    }
    
    func signin(email: String, password: String) async {
        do {
            let result = try await Auth.auth().signIn(withEmail: email, password: password)
            currentUserSession = result.user
        } catch {
            print("DEBUG: Failed to sign in with error \(error.localizedDescription)")
        }
    }
    
    func signOut() {
        do {
            try Auth.auth().signOut()
            currentUserSession = nil
        } catch {
            print("DEBUG: Failed to sign out with error \(error.localizedDescription)")
        }
    }
}

AuthManager는 Firebase Authentication을 사용하여 실제 인증 작업을 처리하는 싱글톤 클래스다. 사용자 생성, 데이터 업로드, 로그인, 로그아웃 기능을 제공한다.

createUser 함수는 Firebase Auth의 createUser 메서드를 사용하여 이메일과 비밀번호로 새 계정을 만든다. 이 과정에서 Firebase Auth는 이메일, 비밀번호, userId만을 저장한다. 성공적으로 계정이 생성되면 currentUserSession을 업데이트하고, 이름과 사용자 이름 등 Firebase Auth에 저장되지 않는 추가 정보를 저장하기 위해 uploadUserData를 호출한다. 이는 로그아웃하거나 앱을 종료했을 때 이러한 추가 정보가 손실되는 것을 방지하기 위함이다.

uploadUserData 함수는 사용자의 모든 정보(userId, 이메일, 사용자 이름, 이름 등)를 Firestore에 업로드하는 비동기 함수다. 이 함수는 Firebase Auth에 저장되지 않는 추가 정보를 포함한 User 객체를 생성하고 Firestore.Encoder를 사용하여 인코딩한다. 인코딩된 데이터를 Firestore의 'users' 컬렉션에 사용자 ID를 문서 ID로 사용하여 저장한다. 이렇게 함으로써 로그아웃이나 앱 종료 후에도 사용자의 모든 정보가 보존되며, 다음 로그인 시 이 정보를 다시 불러올 수 있다.

signin 함수는 사용자 로그인을 처리하는 비동기 함수다. Firebase Auth의 signIn 메서드를 사용하여 이메일과 비밀번호로 로그인한다. 로그인에 성공하면 currentUserSession을 업데이트한다.

signOut 함수는 사용자 로그아웃을 처리한다. Firebase Auth의 signOut 메서드를 호출하여 현재 사용자를 로그아웃시킨다. 로그아웃에 성공하면 currentUserSession을 nil로 설정한다.


5. LoginView.swift(View)

//
//  LoginView.swift
//  InstagramClone
//
//  Created by JunKyu Lee on 10/18/24.
//

import SwiftUI

struct LoginView: View {
    @State var viewModel: LoginViewModel = LoginViewModel()
    
    var body: some View {
        ZStack {
            GradientBackgroundView()
            NavigationStack {
                Spacer()
                Image("instagramLogo")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 90, height: 90)
                Spacer()
                VStack {
                    TextField("이메일 주소", text: $viewModel.email)
                        .modifier(InstagramTextFieldModifier())
                    SecureField("비밀번호", text: $viewModel.password)
                        .modifier(InstagramTextFieldModifier())
                    BlueButtonView {
                        Task {
                            await viewModel.signin()
                        }
                    } content: {
                        Text("로그인")
                    }
                    Text("비밀번호를 잊으셨나요?")
                }
                .padding(.horizontal)

                Spacer()
                NavigationLink {
                    EnterEmailView()
                } label: {
                    Text("새 계정 만들기")
                        .foregroundStyle(.blue)
                        .frame(maxWidth: .infinity, maxHeight: 42)
                        .overlay {
                            RoundedRectangle(cornerRadius: 10)
                                .stroke(.blue, lineWidth: 1)
                        }
                }
                .padding(.horizontal)
            }
        }
        
    }
}

#Preview {
    LoginView()
}

LoginView는 로그인 화면의 UI를 구현한다. @State 속성 래퍼를 사용하여 LoginViewModel 인스턴스를 생성하고 관리한다. 이 뷰는 사용자 입력을 받아 ViewModel을 통해 로그인 기능을 수행한다.

이러한 구조를 통해 각 컴포넌트의 역할이 명확히 분리되어 있으며, 데이터 흐름과 상태 관리가 효율적으로 이루어진다. Firebase를 사용한 인증 시스템과 SwiftUI의 데이터 바인딩 기능이 잘 통합되어 있어, 사용자 경험과 개발 효율성을 모두 고려한 구조라고 할 수 있다.

profile
꾸준🐢

0개의 댓글