오늘은 Firebase의 Authentication에서
사용자 계정을 등록하는 것부터 시작하여 로그인 및 로그아웃 기능을 구현하는 과정을 기록하고자 한다.!!
앱스쿨에서 MVP를 진행해보면서 계정 관리 기능 부분을 따로 기록해 두어야겠다고 생각함 : )
이번 글은 회원가입 !! 😋
0. 결과물
1. Firebase와 iOS Project 연결하기
Firebase와 iOS 프로젝트를 연결하는 과정의 자세한 기록은 생략하였다.! Firebase 프로젝트 생성 후에 하라는대로 하면 되기 때🚪
📝 간략하게 정리하자면,
Firebase에서 새 프로젝트를 추가하고 iOS 프로젝트의 Bundle ID로 연결해준 후에, GoogleService-Info.plist 파일을 프로젝트에 담아주고 sdk를 추가해준다. 그리고 plist파일 추가와 App Delegate 코드를 추가해주면 끝 !!
🚨 문제 발생 !!
이때, GoogleService-Info.plist 파일명은 무조건 정확하게 써주어야 한다 !! Xcode firebase 프레임 워크는 코드 작성을 할 때GoogleService-Info.plist를 통해 firebase에 연결되는데 .plist 파일이 정확히 호출되지 않으면 오류가 표시된다.
.plist 파일이름에 다른 문자를 가지고있는 순간 firebase 프레임워크는 그것을 인식하지 못함.
프레임워크 작업은 'GoogleService-Info-2.plist'가 아닌 'GoogleService-Info.plist'만 검색하기 때문이다. 저번 MVP 때문에 GoogleService-Info.plist 파일은 다운 받은 적이 있어서 새로 받아주게 되면서 -2가 붙었는데 괜찮을 줄 알고 그대로 추가했다가 오류가 났다.. 찾아보니 파일명이 변경되면 안 되는 거였음 !!!
출처 : https://zeddios.tistory.com/47
그런데 sdk를 추가하는 부분에서 Dependency Rule을 Branch에서 Up to Next Major Version으로 변경해줬는데 이유를 잘 모르겠음.. 따로 찾아봐야겠다. (˘̩̩̩ε˘̩ƪ)
2. 뷰 구성하기 - 회원가입, 로그인
import Firebase
@State private var email: String = ""
@State private var password: String = ""
TextField("Enter your Email", text: $email)
SecureField("Enter your Password", text: $password)
이때, 자동완성이랑 대문자 입력을 막기 위해서 수정자를 추가해주었다.
.disableAutocorrection(true)
.textInputAutocapitalization(.never)
3. ViewModel에서 회원가입, 로그인 함수 생성
View 파일에서는 해당 View를 구성하는 요소의 코드들만 보여주고, 서버와 통신하는 코드들은 분리시키는 게 좋다고 배웠기에 Authentication class를 따로 생성하주고 계정을 생성하고 로그인하는 함수를 만들어주었다.
import Foundation
import Firebase
// 수정 전 : struct Authentication {
class Authentication: ObservableObject {
var userIsSignedUp: Bool = false
func register(email: String, password: String) {
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if error != nil {
self.userIsSignedUp = false
print(error!.localizedDescription)
}
self.userIsSignedUp = true
}
}
func login(email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if error != nil {
print(error!.localizedDescription)
}
}
}
}
출처 : https://youtube.com/watch?v=6b2WAePdiqA&si=EnSIkaIECMiOmarE
4. 회원가입 뷰 호출
var authentication: Authentication = Authentication()
@State private var showSignUpModal = false
// ContentView
Button {
// log in action
authentication.login(email: email, password: password) // login func 호출
} label: {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.accentColor)
.frame(width: 300, height: 40)
.overlay {
Text("LOG IN")
.foregroundColor(.white)
.bold()
}
.padding(.top, 40)
} // Login Button
Button {
// sign up action
showSignUpModal = true
} label: {
Text("Going to sign up")
.bold()
.padding(.top, 60)
} // Sign up Button
.sheet(isPresented: $showSignUpModal) {
SignupView()
} // Go to SignupView Button
Sheet - 회원가입은 로그인 화면에서 회원가입 버튼을 눌렀을 때 시트로 뜨도록 하였다.
시트를 띄우기 위해서는 상태변수를 통해 시트가 띄워짐을 알려주어야 하므로 showSignUpModal 변수를 false로 초기화 해두고 버튼 클릭 시 true로 바꾸어 주었다.
5. 회원가입 함수 호출
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
// SignupView
Button {
// sign up action
authentication.register(email: email, password: password)
if authentication.userIsSignedUp { // 회원가입 성공 시에만 시트 닫기
self.presentationMode.wrappedValue.dismiss()
}
} label: {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.accentColor)
.frame(width: 300, height: 40)
.overlay {
Text("SIGN UP")
.foregroundColor(.white)
.bold()
}
.padding(.top, 70)
} // Signup Button
Sheet dismiss
회원가입 뷰에서는 회원가입이 정상적으로 성공했을 때만 시트가 닫겨야 한다. 그러므로 보여주는(presentation) 상태(state)를 수정해서 스스로 닫도록 뷰에게 알리는 방식을 이용하였다.
먼저, 새로운 environment property를 닫고자 하는 뷰에 추가하고 wrappedValue.dismiss()를 통해 뷰가 스스로 닫기도록 해주었다.
🚨 문제 발생 !!
회원가입이 성공했을 때만 시트가 닫겨야 해서 성공했는지 알 수 있는 상태 변수가 필요했다. 처음에는 @State, @Binding으로 Authentication ViewModel과 SignUpView에서 데이터를 주고 받는 식으로 짰는데 계속 true로 바뀌지 않는 것임...!
찾아본 결과.. State, Binding은 어떤 뷰가 하나 이상의 하위 뷰를 가지고 있고, 동일한 상태 프로퍼티에 대해 접근해야 할 때 씀 !!
즉, 내가 만든 Authentication은 View가 아니기 때문이었던 것...?!
➡️ 해결 : Authentication ViewModel을 ObservableObject 프로토콜을 따르도록 설정해주고, SignUpView에서 @EnvironmentObject로 선언해서 해당 View Model 안의 상태변수에 접근해야했다.
// SignUpView
@EnvironmentObject private var authentication: Authentication
App 파일에서도 아래와 같이 EnvironmentObject를 추가해준다.
@main
struct FirestoreSwiftUIDemo: App {
// register app delegate for Firebase setup
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@StateObject var authentication = Authentication()
var body: some Scene {
WindowGroup {
NavigationView {
ContentView().environmentObject(authentication)
}
}
}
}
🚨 문제 발생 ...
register 함수 호출을 하고 성공 시에 시트를 닫게 하니까 버튼을 2번 눌러야 시트가 내려감... 왜 몰랐지..?
register 함수에서 상태 변수를 변화시켜주게 되면서 버튼을 눌렀을 때 계정이 생성되고 true로 바뀌고, 한 번 더 눌러야 true로 변화된 authentication.userIsSignedUp 값으로 인해 if문이 실행되면서 sheet가 dismiss 된다...🥹
➡️ 해결 : 비동기 처리를 해줘야 한다 !!
// 수정된 Authentication View Model
enum AuthenticationState {
case unauthenticated
case authenticating
case authenticated
}
class Authentication: ObservableObject {
@Published var errorMessage = ""
@Published var authenticationState: AuthenticationState = .unauthenticated
func register(email: String, password: String) async -> Bool {
authenticationState = .authenticating
do {
try await Auth.auth().createUser(withEmail: email, password: password)
errorMessage = ""
authenticationState = .authenticated
return true
}
catch {
print(error.localizedDescription)
errorMessage = error.localizedDescription
authenticationState = .unauthenticated
return false
}
}
}
Authentication ViewModel에서 register 함수 자체를 비동기 처리 해주고 반환값을 추가해주었다. 그리고 비동기 처리이므로 서버와 통신 중임을 UI로 표현해주기 위해서 enum도 추가해주었다. SignUpView도 수정해줌 !!
비밀번호 재입력도 필요할 것 같아서 추가해주었다. 비밀번호 2개가 같을 때만 회원가입 버튼을 활성화시켜주는 방식으로 구성했는데 register 함수의 error message랑 에러 출력 방식(?)이 달라서 일관된 방식으로 해주고 싶지만 마땅한 방식이 안 떠오름..
일단은 이렇게 뷰를 구성해야겠다..!
// 수정된 SignUpView
private func signUpWithEmailPassword() {
Task {
if await authentication.register(email: email, password: password) == true {
self.presentationMode.wrappedValue.dismiss() // 회원가입 성공 시 sheet 닫기
}
}
}
// in body
Button {
// sign up action
signUpWithEmailPassword() // register 함수 호출과 동시에 회원가입 성공 유무를 반환함.
} label: {
// 비동기 작업이 끝나기 전까지(작업 중)는 ProgressView를 띄워서 회원가입 버튼을 없앤다.
if authentication.authenticationState == .authenticating {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .black))
.frame(maxWidth: .infinity)
}
else { // 비동기 작업 전이거나, 끝난 후에는 다시 회원가입 버튼을 띄워준다.
RoundedRectangle(cornerRadius: 10)
.foregroundColor(!password.isEmpty && password == password2 ? .accentColor : .gray)
.frame(width: 300, height: 40)
.overlay {
Text("SIGN UP")
.foregroundColor(.white)
.bold()
}
.padding(.top, 70)
}
} // Signup Button
.disabled(!password.isEmpty && password == password2 ? false : true)
.frame(height: 100)
// Sign up error message
if !authentication.errorMessage.isEmpty {
Text(authentication.errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 5)
}
🚨 문제 발생
보라색 괴물 등장 👾
// error log
2022-12-18 21:20:08.965473+0900 FirestoreSwiftUIDemo[5105:144686] [SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
Background queue에서 UI 관련된 작업을 수행하면 다음 문구가 Xcode에 보라색으로 발생한다고 한다. 다시 말하면, SwiftUI에서 async 콘텍스트를 통해 UI 와 관련된 변수를 업데이트할 때 발생한다. 이를 해결하기 위해서는 Main queue에서 실행할 수 있도록 바꿔주면 된다.
➡️ 해결 : @MainActor 어노테이션을 사용 !
출처 : https://daewonyoon.tistory.com/471
@MainActor
func register(email: String, password: String) async -> Bool {
authenticationState = .authenticating
do {
try await Auth.auth().createUser(withEmail: email, password: password)
errorMessage = ""
authenticationState = .authenticated
return true
}
catch {
print(error.localizedDescription)
errorMessage = error.localizedDescription
authenticationState = .unauthenticated
return false
}
}
Authentication ViewModel의 register 함수 최종 코드 !!! 보라색 괴물 감쪽같이 사라졌다 ㅎㅅㅎ
팀원들과 MVP 진행할 때는 내가 처음부터 코드를 짠 게 아니기도 하고, 눈으로만 보고 있는 상황도 많았어서 머리에 남아있는 게 없었다..
처음부터 끝까지 내 손으로 짜보니까 팀원들과 할 땐 나타나지 않았던 에러들도 많았고,
내가 원하는 View 전환 방식으로 구현하려다 보니 기존의 코드가 있음에도 불구하고 0에서 시작하는 너낌이었슴 🫠
그리고 View와 ViewModel의 역할을 최대한 분리시키기 위해서 노력을 했는데 잘 된 건지..ㅎㅅㅎ
회원가입 하나 하는데 왤케 오래 걸린 거야 ㅠ_ㅠ
'iOS > SwiftUI' 카테고리의 다른 글
[SwiftUI] UserDefaults와 @AppStorage (0) | 2023.02.04 |
---|---|
[SwiftUI] SwiftUI의 Stack, Frame, GeometryReader (0) | 2022.11.30 |
[SwiftUI] SwiftUI로 커스텀 뷰 수정하기 (0) | 2022.11.22 |
[SwiftUI] SwiftUI로 커스텀 뷰 생성하기 (0) | 2022.11.14 |
[SwiftUI] Xcode 파일 뜯어보기 (0) | 2022.11.06 |