1. 개요
앱을 만들다보면 서버를 이용해서 로그인하는 기능이 필요로할 때가 있다. 이 때, 자체적인 회원가입을 통해 로그인을 하게 할 수도 있다.
요즘에는 SNS 계정 연동으로 로그인하는 경우가 많지만, 이번 게시글에서는 Email Auth를 사용하여 FirebaseAuth에 회원가입을 통해 회원 계정 정보를 등록하고 등록한 정보로 로그인하는 과정을 기록한다.
또한 앱 종료 후 다시 실행시켰을 때 이전 로그인한 기록을 가지고 자동로그인하는 방법도 구현하였다.
2. Firebase와 프로젝트 연결하기
Firebase 프로젝트와 Xcode 프로젝트 생성은 이전 포스팅과 같다.
이미 방법을 알고 있다면 중간에 패키지 추가사항만 확인해두면 되겠다.
먼저, Xcode에서 프로젝트를 생성한다.
프로젝트 생성시 정해지는 Bundle Identifier 정보는 Firebase 프로젝트 생성할 때 필요한 정보이다.
물론 프로젝트 생성 후 변경할 수도 있다.
다음은, Firebase 프로젝트를 생성해보겠다.
프로젝트 이름은 임의대로 지정해주고,
Google 애널리틱스 설정은 권장 설정과 Default Account for Firebase를 선택하고 프로젝트를 생성해주자.
이제 프로젝트 개요 메인 화면에서 가운데 iOS+ 을 눌러 앱을 추가하여 시작한다.
앱 정보 입력
아까 얘기해둔 XCode 프로젝트 Bundle Identifier를 Apple 번들 ID에 입력한다. (필수)
앱 닉네임, App Store ID는 입력 해도 되고 안해도 되지만 되도록 입력 할 수 있는 정보는 모두 입력하자.
Google Info plist 추가
다음은 GoogleService-Info plist를 추가하는 것인데, 다운로드 한 뒤에 파일을 Xcode 프로젝트에 드래그 앤 드롭으로 넣어두자.
GoogleService-Info plist에는 Firebase와 프로젝트간 연결할 수 있는 정보들이 담겨있다.
다음은 Xcode 프로젝트에 Firebase SDK를 추가한다.
Xcode 상단 메뉴 바에서 File > Add Packages로 이동
https://github.com/firebase/firebase-ios-sdk
위 링크를 Search or Enter Package URL에 입력해준다.
Firebase 패키지 추가
Firebase SDK Product 선택 설치
firebase-ios-sdk 를 선택하고 Add Package를 클릭하고 이후에 패키지를 선택 추가 창이 나오면
FirebaseAuth 패키지를 선택하여 추가한다.
패키지까지 추가하고 Firebase에서 다음을 누르면,
위 처럼 초기화 코드를 Xcode 프로젝트에 입력하라고 하는데,
프로젝트이름App.swift 파일로 들어가 아래처럼 작성해준다.
3. FirebaseAuth 빌드
이제 Firebase 프로젝트에서 우리가 로그인에 사용할 Auth 기능을 빌드할 차례이다.
Firebase 프로젝트 메인에 좌측 빌드 메뉴를 클릭하면 Firebase에서 사용할 수 있는 여러가지 빌드 목록들이 나오는데, 우리는 계정과 관련된 기능을 사용할 것이므로, Authentication을 클릭한다.
Authentication을 빌드목록에서 선택하면 위와같은 메인화면이 나오는데, 시작하기를 눌러준다.
Firebase에서 기본으로 제공하는 로그인 수단들이 나열되어있다. 다양한 수단으로 할 수 있으며, 우리는 여기서 기본 제공업체에 있는 이메일/비밀번호를 사용한 계정을 관리할 것이다.
기본 제공업체 섹션에 있는 이메일/비밀번호를 클릭하여 시작한다.
우리는 이메일과 비밀번호로 회원가입하여 이를 사용한 로그인을 구현할 것이므로 이메일/비밀번호 토글을 사용설정으로 변경해주고 저장 눌러준다.
저장을 누르고 로그인 제공업체 목록에서 '이메일/비밀번호' 상태가 '사용 설정됨'으로 되어있으면 정상으로 된 것이다.
추가로, 이메일/비밀번호, Google계정, Facebook계정, AppleID 계정을 이용한 로그인을 한 프로젝트에서 모두 구현할 수 있다.
이 때에는 '새 제공업체 추가'를 클릭하여 로그인 제공업체 선택하는 과정을 반복하면 된다.
4. 회원가입 View 만들기
FirebaseAuth에 기본 제공업체를 통해서 로그인할 경우, 회원 정보를 스스로 등록하는 과정이 필요하다. 사용자의 '회원가입' 과정이 되겠다.
나의 계획은 회원 가입을 하고, 정상적으로 가입된 정보를 통해 로그인하는 과정을 다뤄볼 것이다.
먼저 프로젝트에서 새 파일 추가 'SignUpView.swift' 파일을 생성한다.
그리고 다음 코드를 추가한다.
struct SignUpView: View {
@State private var nameText: String = "" // 이름 Text
@State private var emailText: String = "" // email Text
@State private var passwordText: String = "" // 패스워드 Text
@State private var passwordConfirmText: String = "" // 패스워드 확인 Text
@State var isShowingProgressView = false // 로그인 비동기 ProgressView
@State var isShowingAlert: Bool = false // 로그인 완료 Alert
var body: some View {
ScrollView {
VStack(spacing: 30) {
VStack {
Text("이메일 ID 생성")
.font(.largeTitle)
Text("하나의 ID로 모든 서비스를 이용할 수 있습니다.")
}
.lineSpacing(10)
VStack(alignment: .leading) {
Text("이름")
.font(.headline)
TextField("이름을 입력해주세요", text: $nameText)
.padding()
.background(.thinMaterial)
.cornerRadius(10)
.textInputAutocapitalization(.never)
}
VStack(alignment: .leading) {
Text("이메일")
.font(.headline)
TextField("이메일을 입력해주세요", text: $emailText)
.padding()
.background(.thinMaterial)
.cornerRadius(10)
.textInputAutocapitalization(.never)
}
VStack(alignment: .leading) {
Text("비밀번호")
.font(.headline)
SecureField("비밀번호를 6자리 이상 입력해주세요", text: $passwordText)
.padding()
.background(.thinMaterial)
.cornerRadius(10)
}
VStack(alignment: .leading) {
Text("비밀번호 확인")
SecureField("비밀번호를 다시 입력해주세요", text: $passwordConfirmText)
.padding()
.background(.thinMaterial)
.border(.red, width: passwordConfirmText != passwordText ? 1 : 0)
.cornerRadius(10)
}
Button {
isShowingAlert = true
} label: {
Text("회원 가입")
.frame(width: 100, height: 50)
.background(.blue)
.foregroundColor(.white)
.cornerRadius(10)
.alert("회원가입", isPresented: $isShowingAlert) {
Button {
} label: {
Text("OK")
}
} message: {
Text("회원가입이 완료되었습니다.")
}
.padding()
}
}
.padding()
.padding(.bottom, 15)
}
}
}
위 코드는 단지 회원 가입에 필요한 View만 배치하고, State 변수를 바인딩한 결과이다.
추가로, 비밀번호는 일반 Textfield 뷰가 아니라 'SecureField'를 사용하여 보안성을 높여야한다.
코드를 바탕으로 생긴 View 구성은 스크린 샷과 같다.(iPhone 14 Pro 기준)
회원 가입 뷰에서 확인해야하는 사항은 다음과 같다.
1. 비밀번호는 6자리 이상으로 할 것
2. 비밀번호 확인 텍스트는 비밀번호 텍스트와 같아야 할 것
3. 이름, 이메일, 비밀번호, 비밀번호 확인 텍스트 중 한 개라도 비워져 있으면 회원 가입 버튼을 비활성화 할 것
4. 이메일 형식이나 이메일이 중복되어 가입이 불가능할 경우 알림창으로 사용자에게 알릴 것 -> async / await 함수로 구현
4-1. 비밀번호는 6자리 이상으로 할 것
회원 가입 뷰에서 비밀번호 6자리 이상으로 했는지 확인하는 사항을 코드로 구현해보자
@State var isPasswordCountError: Bool = false
...
VStack(alignment: .leading) {
Text("비밀번호")
.font(.headline)
SecureField("비밀번호를 6자리 이상 입력해주세요", text: $passwordText)
.padding()
.background(.thinMaterial)
.cornerRadius(10)
Text("비밀번호는 6자리 이상 입력해주세요.")
.font(.system(size: 15))
.foregroundColor(isPasswordCountError ? .red : .clear)
}
비밀번호 입력하는 SecureField가 포함된 VStack 내 코드이다.
isPasswordCountError 변수를 추가하고 만약 이 값이 true로 바뀐다면, "비밀번호는 6자리 이상 입력해주세요."의 텍스트 필드가 .red로 바뀌면서 사용자에게 보여질 것이다.
에러가 발생하지 않으면 에러 텍스트는 .clear이기 때문에 사용자에게 보이지 않는다.
4-2. 비밀번호 확인 텍스트와 비밀번호 텍스트는 같아야 할 것
열거형 케이스에 비밀번호가 서로 다를 경우, isPasswordUnCorrectError 변수를 추가한다.
@State var isPasswordUnCorrectError: Bool = false // 비밀번호 텍스트 일치 확인
VStack(alignment: .leading) {
Text("비밀번호 확인")
SecureField("비밀번호를 다시 입력해주세요", text: $passwordConfirmText)
.padding()
.background(.thinMaterial)
.border(.red, width: passwordConfirmText != passwordText ? 1 : 0)
.cornerRadius(10)
Text("비밀번호가 서로 다릅니다.")
.font(.system(size: 15))
.foregroundColor(isPasswordUnCorrectError ? .red : .clear)
}
비밀번호 확인 SecureField가 포함된 VStack 내 코드이다.
isPasswordUnCorrectError 변수를 추가하고 만약 이 값이 true로 바뀐다면, "비밀번호가 서로 다릅니다."의 텍스트 필드가 .red로 바뀌면서 사용자에게 보여질 것이다.
에러가 발생하지 않으면 에러 텍스트는 .clear이기 때문에 사용자에게 보이지 않는다.
4-3. 이름, 이메일, 비밀번호, 비밀번호 확인 텍스트 중 한 개라도 비워져 있으면 회원 가입 버튼을 비활성화 할 것
텍스트 중 한개라도 비워져있는지 조건을 확인하는 함수를 정의하고 View 파일 내 body 아래에 함수를 추가한다.
struct SignUpView: View {
...
var body: some View {
...
}
func checkSignUpCondition () -> Bool {
if nameText.isEmpty || emailText.isEmpty || passwordText.isEmpty || passwordConfirmText.isEmpty {
return false
}
return true
}
}
checkSignUpCondition 함수는 nameText, emailText, passwordText, passwordConfirmText 중 한개라도 비워져있으면 false를 반환한다.
그리고 View 구성을 변경한다.
Button {
isShowingAlert = true
} label: {
Text("회원 가입")
.frame(width: 100, height: 50)
.background(!checkSignUpCondition() ? .gray : .blue)
.foregroundColor(.white)
.cornerRadius(10)
.alert("회원가입", isPresented: $isShowingAlert) {
Button {
} label: {
Text("OK")
}
} message: {
Text("회원가입이 완료되었습니다.")
}
.padding()
}
.disabled(!checkSignUpCondition() ? true : false)
}
.padding()
.padding(.bottom, 15)
위 코드는 회원가입 Button에 대한 코드이다.
.backgound 수정자를 보면, checkSignUpCondition이 false로 반환되면 함수명 앞에 '!'로 true가 되어 바탕색이 회색으로 되고, true면 파란색으로 변한다.
.disabled 수정자를 보면, checkSignUpCondition이 false로 반환되면 함수명 앞에 '!'로 true가 되어 버튼 클릭이 비활성화가 된다.
따라서, checkSignUpCondition의 결과로 회원가입의 버튼 활성화/비활성화를 시각적으로도 사용자에게 나타낼 수 있게 된다.
4-4. 이메일 형식이나 이메일이 중복되어 가입이 불가능할 경우 알림창으로 사용자에게 알릴 것
서버에 회원가입 요청을 보내고 그로부터 오는 응답에 대한 처리를 하기 위해서는 여러 방법이 있지만 async/await 함수를 사용하여 구현하는 방법이 있다.
async/await 함수를 사용하지 않고서는 대표적으로 ViewModel 내 회원가입 완료 여부를 확인하는 프로퍼티를 선언하여 View가 이를 구동하여 상태 여부에 따라 View를 전환시키는 방법도 있다.
다음 게시글에 async/await 함수를 사용하여 에러 발생에 대한 후속처리를 구현하도록 하고 이번 게시글에서는 잠시 패스하겠다.
이제 회원가입 View의 구성은 완료되었다.
회원가입 로직을 구현할 함수를 ViewModel로 정의하여 구현해보겠다.
5. ViewModel - 회원가입
프로젝트에 새 파일 ViewModel.swift 추가하고 다음 코드를 입력한다.
import Foundation
import Firebase
import FirebaseAuth
class ViewModel: ObservableObject {
func emailAuthSignUp(email: String, userName: String, password: String) -> Int? {
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print("error: \(error.localizedDescription)")
return
}
if result != nil {
let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = userName
print("사용자 이메일: \(String(describing: result?.user.email))")
}
}
}
}
ViewModel에 정의된 emailAuthStringUp 함수는 FirebaseAuth에 회원가입 요청을 전달하게 되고, 에러가 발생하면 에러 값을 반환하고 함수를 종료하게 된다.
에러가 발생하지 않는다면 등록된 사용자 계정 프로필에 사용자 이름을 업데이트하고 함수를 종료한다.
6. 회원가입 View 기능 완성
프로젝트App.swift 파일 소스 코드에 viewModel을 @StateObject로 선언하고 프로젝트 모든 View에서 @EnvironmentObject 프로퍼티 래퍼를 사용하여 해당 변수를 참조할 수 있게 해준다.
// 프로젝트이름App.swift
import SwiftUI
import FirebaseCore
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
@main
struct YourApp: App {
// register app delegate for Firebase setup
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@StateObject var viewModel: ViewModel = ViewModel()
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
.environmentObject(viewModel)
}
}
}
}
// signUpView.swift
struct SignUpView: View {
@State private var nameText: String = "" // 이름 Text
@State private var emailText: String = "" // email Text
@State private var passwordText: String = "" // 패스워드 Text
@State private var passwordConfirmText: String = "" // 패스워드 확인 Text
@State var isShowingProgressView = false // 로그인 비동기 ProgressView
@State var isShowingAlert: Bool = false // 로그인 완료 Alert
@State var isPasswordCountError: Bool = false // 비밀번호 6자리 이상 확인
@State var isPasswordUnCorrectError: Bool = false // 비밀번호 텍스트 일치 확인
@State var isEmailError: Bool = false // 이메일 에러
@State var emailErrorText: String = "" // 이메일 에러 Text
@EnvironmentObject var viewModel: ViewModel // ViewModel
@Environment(\.dismiss) private var dismiss // View전환
var body: some View {
...
}
func checkSignUpCondition() -> Bool {
...
}
}
@Environment(\.dismiss) private var dismiss를 추가한다.
회원가입 완료 버튼을 누르면 View가 자동으로 NavigationStack에서 pop되어 이전 View로 이동되도록 하기 위해 사용한다.
이제 회원가입 로직을 완성해보자.
// (1)
Button {
isShowingProgressView = true // ProgressView 보이기
if passwordText.count < 6 {
isPasswordCountError = true
}
if passwordConfirmText != passwordText {
isPasswordUnCorrectError = true
}
if passwordText.count >= 6 && passwordConfirmText == passwordText {
viewModel.emailAuthSignUp(email: emailText, userName: nameText, password: passwordText)
isShowingAlert = true
}
} label: {
Text("회원 가입")
.frame(width: 100, height: 50)
.background(!checkSignUpCondition() ? .gray : .blue)
.foregroundColor(.white)
.cornerRadius(10)
.alert("회원가입", isPresented: $isShowingAlert) {
Button {
dismiss()
} label: {
Text("OK")
}
} message: {
Text("회원가입이 완료되었습니다.")
}
.padding()
}
.disabled(!checkSignUpCondition() ? true : false)
//(2)
if isShowingProgressView {
ProgressView()
}
(1): 기존 회원가입 버튼을 수정한 코드이다.
isShowingProgressView 변수는 버튼 하단에 있는 ProgressView를 보이게 하는 용도이다.
일단 버튼은 모든 텍스트에 텍스트가 입력되어있으면 활성화 -> 비밀번호가 6자리 이상 -> 비밀번호 확인 텍스트가 비밀번호 텍스트와 같을 경우 ViewModel 회원 가입 함수가 실행된다.
(2) 회원가입 버튼이 눌리면 상호작용 피드백으로 ProgressView가 보여지게 했다.
7. 로그인 View 그리기
import SwiftUI
struct SignInView: View {
@State var emailText: String = ""
@State var passwordText: String = ""
@State var signInProcessing: Bool = false
var body: some View {
NavigationStack {
VStack(spacing: 15) {
Text("이메일로 로그인하세요")
.font(.largeTitle)
.lineSpacing(10)
VStack {
TextField("이메일을 입력하세요", text: $emailText)
.padding()
.background(.thinMaterial)
.cornerRadius(10)
.textInputAutocapitalization(.never)
SecureField("비밀번호를 입력하세요", text: $passwordText)
.padding()
.background(.thinMaterial)
.cornerRadius(10)
.padding(.bottom, 30)
}
// 로그인 접속중에 signInProcessing = false 이거나 유저 정보가 비어있다면
if signInProcessing {
ProgressView()
}
Button {
signInProcessing = true
} label: {
Text("로그인")
.padding()
.foregroundColor(.white)
.background(emailText.isEmpty || passwordText.isEmpty == true ? .gray : .red)
.cornerRadius(10)
.padding(.bottom, 40)
}
.disabled(emailText.isEmpty || passwordText.isEmpty ? true : false)
// 회원가입 View로 이동
HStack {
Text("아이디가 없으십니까?")
NavigationLink {
SignUpView()
} label: {
HStack {
Text("지금 만드세요.")
Image(systemName: "arrow.up.forward")
}
}
}
}
.padding()
}
}
}
간단하게 로그인 View를 코드로 작성하였고, View 스크린 샷이 위와 같다.
또한 코드 하단부에 NavigationLink로 회원가입 View를 연결시켜줬다.
이제 ViewModel에 로그인 로직을 작성할 차례이다.
8. ViewModel - 로그인
ViewModel에서 로그인 함수를 작성하는 것도 회원가입 함수와 크게 다르지 않다.
@Published var state: SignInState = .signedOut
enum SignInState{
case signedIn
case signedOut
}
...
func emailAuthSignIn(email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error {
print("error: \(error.localizedDescription)")
return
}
if result != nil {
self.state = .signedIn
print("사용자 이메일: \(String(describing: result?.user.email))")
print("사용자 이름: \(String(describing: result?.user.displayName))")
}
}
}
로그인 함수도 회원 가입 함수와 다르지 않지만, 로그인 상태를 체크하여 View를 전환시켜줄 것이기 때문에 로그인 상태를 나타낼 열거형 SignInState가 정의되어 있으며 해당 열거형 타입 변수 state가 선언됐다.
로그인 함수에서 로그인을 통해 회원 정보 결과가 정상적으로 반환된 곳에 self.state = .signedIn 코드를 추가했다.
9. 로그인 View 기능 완성
//(1)
Button {
signInProcessing = true
viewModel.emailAuthSignIn(email: emailText, password: passwordText)
} label: {
Text("로그인")
.padding()
.foregroundColor(.white)
.background(emailText.isEmpty || passwordText.isEmpty == true ? .gray : .red)
.cornerRadius(10)
.padding(.bottom, 40)
}
.disabled(emailText.isEmpty || passwordText.isEmpty ? true : false)
위 코드는 기존 로그인 버튼을 수정한 것이다.
Button action 클로저 블록에 viewModel.emailAuthSignIn 함수를 추가했다. 매개변수로 email, password 텍스트를 전달해준다.
10. 로그인 로직 View 전환
로그인 로직, 회원가입 로직, 각 View의 구성 모두 완성됐다.
이제 로그인 됐을 때, View를 이동하는 것을 구현해보려고 한다.
먼저, 로그인 됐을 때 전환할 MainView.swift를 생성해주고, ContentView.swift 파일을 다음과 같이 수정한다.
import SwiftUI
import FirebaseAuth
struct ContentView: View {
@EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack {
if viewModel.state == .signedIn {
MainView()
} else {
SignInView()
}
}
.onAppear {
if Auth.auth().currentUser != nil {
viewModel.state = .signedIn
}
}
}
}
Auth.auth().currentUser는 기기 데이터에 저장되어있는 FirebaseAuth 객체의 정보를 말한다.
기기에서 로그인을 하면 기기 AppStorage 어딘가에 FirebaseAuth 객체의 정보가 남게되고, 이 정보로 FirebaseAuth의 계정관리가 가능하고 Firebase의 다른 빌드에 접근할 수 있다.
하지만 한번도 로그인을 한적 없거나, 로그아웃을 한 경우 기기에 계정 데이터가 없으므로, 로그인을 하는 과정을 거쳐야한다.
요약하자면, Auth.auth().currentUser에 정보가 남아있다면 아직 이 기기가 FirebaseAuth 와 연결된 상태라는 뜻이고, 낮은 접근?이 가능하다.
하지만 계정의 정보 변경, 탈퇴 등 계정에 대한 중요한 작업을 하기 위해서는 다시한번 인증을 하는 과정이 필요하다.
ContentView에서 Auth.auth().currentUser에 정보가 있다면 viewModel의 state 변수 값을 변경해주고 View와 바인딩 되어있는 state 변수 값에 따라 View를 다르게 보여주게 되므로, 자동로그인 기능도 사용할 수 있게 된다.
11. 시연 및 결과
회원가입이 정상적으로 완료되면 FirebaseAuth에 회원가입한 계정 리스트를 볼 수 있으며 여기서 계정 관리가 가능하다.
'SwiftUI' 카테고리의 다른 글
SwiftUI Firebase와 KakaoTalk 로그인 연결하기 (0) | 2023.01.25 |
---|---|
SwiftUI Firebase Auth - Google Account 연동하기 (0) | 2022.12.23 |
SwiftUI Firebase Realtime Database CRUD 제대로 사용하기 (12) | 2022.12.08 |
ATS Policy에 의한 HTTP 통신 제약 (0) | 2022.11.28 |
SwiftUI 문서 구조화 해보기 - 초급 (0) | 2022.11.07 |