SwiftUi & Firebase를 이용해서 회원가입, 로그인/로그아웃 구현하기 (1)

농담고미고미·2024년 12월 24일
0

프론트엔드

목록 보기
6/12
post-thumbnail

우선 스위프트 파일에 의존성을 추가해준다.
General -> Frameworks에서 플러스 버튼을 눌러 FirebaseAuth를 추가한다.

Firebase 사이트에서 다운 받은 GoogleService-info도 하이라이키에 드로그 앤 드롭 해준다.

우선 로그인 화면부터 만들자.

이메일과 비밀번호로 받을 변수들을 선언해준다.

@State var emailID: String = ""
    @State var password: String = ""
TextField("Email", text: $emailID)
                    .textContentType(.emailAddress)

.textContentType(.emailAddress)는 SwiftUI에서 TextField에 적용하는 뷰 수정자입니다. 이 수정자를 사용하면 시스템에 해당 입력 필드가 이메일 주소를 받는다는 정보를 제공하게 됩니다. 이를 통해 자동 완성, 자동 대문자 변환 방지 등 사용자 입력을 도와주는 기능이 활성화됩니다.

화면 꾸미는 건 생략!

RegisterView()를 만든다. 참고로 레지스터뷰를 만들 때 아이폰8과 같이 화면 크기가 작은 기기도 있기 때문에 최적화를 위해서 아래의 코드를 적는다.

// MARK: - 작은 사이즈를 위해서
            ViewThatFits {
                ScrollView(.vertical, showsIndicators: false) {
                    HelperView()
                }
                HelperView()
            }
@ViewBuilder
    func HelperView() -> some View {
        VStack(spacing: 12) {
        ...
    }

@ViewBuilder는 SwiftUI에서 여러 개의 하위 뷰를 하나의 클로저로 묶어 반환할 수 있게 해주는 특수한 속성 래퍼. 이를 통해 여러 개의 뷰를 조합하여 하나의 단일 뷰처럼 사용할 수 있.

예를 들어, VStack, HStack, ZStack과 같은 SwiftUI의 컨테이너 뷰는 내부적으로 @ViewBuilder를 사용하여 자식 뷰들을 구성한다.

나는 로그인뷰에서 register now를 클릭하면 레지스터뷰가 뜨길 바라기 때문에 sheet로 연결해준다.

// MARK: sheet를 통한 Register View
        .fullScreenCover(isPresented: $createAccount) {
            RegisterView()
        }

유저의 프로필 사진을 helperView에 넣자.

if let userProfilePicData, let image = UIImage(data: userProfilePicData) {
                    Image(uiImage: image)
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                } else {
                    Image("NullProfile")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                } 

UIImage(data: userProfilePicData)는 userProfilePicData라는 Data 타입의 바이너리 데이터를 이용하여 UIImage 객체를 생성하는 초기화 메서드입니다. 이 메서드는 주어진 데이터가 유효한 이미지 형식(JPEG, PNG 등)일 경우 해당 데이터를 기반으로 UIImage 객체를 반환하며, 데이터가 손상되었거나 지원되지 않는 형식일 경우 nil을 반환합니다.

if let 구문을 사용하여 userProfilePicData가 nil이 아니고, UIImage(data:)를 통해 유효한 UIImage 객체로 변환될 수 있는지 확인합니다.

변환에 성공하면 image 상수에 UIImage 객체가 할당되고, 해당 블록이 실행됩니다.

성공 시 Image(uiImage: image): UIImage 객체를 SwiftUI의 Image 뷰로 변환하여 화면에 표시합니다.

@State var showImagePicker: Bool = false
    @State var photoItem: PhotosPickerItem?

를 이용해 휴대폰의 사진을 선택하면 프로필 사진이 변경되도록 합니다.

사진에다가 .onTapGesture 모디파이어를 추가합니다.

.photosPicker(isPresented: $showImagePicker, selection: $photoItem)

위 모디파이어를 통해 showImagePicker가 true일 때 갤러리 사진이 뜨고, 선택한 사진은 photoItem으로 매핑됩니다.

이제 고른 이미지로 프로필 사진으로 추출해봅시다.

.onChange(of: photoItem) { newValue in
            // MARK: Extracting UIImage From PhotoItem
            if let newValue {
                Task {
                    do {
                        guard let imageData = try await newValue.loadTransferable(type: Data.self) else {return}
                    } catch {
                        
                    }
                }
            }
        }

.onChange(of: photoItem) { newValue in ... }

이 부분은 SwiftUI의 onChange 수정자로, photoItem의 값이 변경될 때마다 지정된 클로저(newValue를 매개변수로 받음)를 실행합니다
if let newValue { ... }

Swift 5.7부터 도입된 옵셔널 바인딩 구문으로, newValue가 nil이 아닌 경우에만 내부 코드를 실행합니다.

loadTransferable(type: Data.self)는 SwiftUI의 PhotosPickerItem에서 제공하는 메서드로, 사용자가 선택한 사진을 지정된 타입(Data)으로 비동기적으로 로드하는 데 사용됩니다.

await MainActor.run(body: {
                            userProfilePicData = imageData
                        })

MainActor: Swift에서 UI 업데이트와 같은 작업을 메인 스레드에서 실행하도록 보장하는 특수한 액터입니다.

로그인 뷰에서 firebase를 import 합니다.

loginUser(), setError() 를 만듭니다.

func loginUser() {
        Task {
            do {
                try await Auth.auth().signIn(withEmail: emailID, password: password)
            } catch {
                await setError(error)
            }
        }
    }
    
    // MARK: Displaying Errors VIA Alert
    func setError(_ error: Error) async {
        // MARK: UI Must be Updated on Main Thread
        await MainActor.run(body: {
            errorMessage = error.localizedDescription
            showError.toggle()
        })
    }

Task 블록은 Swift의 동시성 모델에서 새로운 비동기 작업을 시작할 때 사용됩니다. 이를 통해 비동기 함수나 작업을 호출할 수 있으며, 내부에서는 await 키워드를 사용하여 다른 비동기 함수의 완료를 기다릴 수 있습니다.

try await Auth.auth().signIn(withEmail: emailID, password: password)는 Firebase Authentication을 사용하여 주어진 이메일(emailID)과 비밀번호(password)로 사용자를 비동기적으로 로그인시키는 Swift 코드입니다.

Auth.auth(): Firebase 인증의 싱글톤 인스턴스를 반환합니다.

signIn(withEmail:password:): 이메일과 비밀번호를 사용하여 사용자를 인증하는 메서드입니다.

try await: 이 메서드는 비동기적으로 동작하며 오류를 던질 수 있으므로, try와 await 키워드를 사용하여 호출합니다.
인증이 성공하면 현재 앱 세션에 사용자가 로그인되며, 이후 Auth.auth().currentUser를 통해 현재 로그인된 사용자 정보를 가져올 수 있습니다.

func resetPassword() {
        Task {
            do {
                try await Auth.auth().sendPasswordReset(withEmail: emailID)
                print("Link Sent")
            } catch {
                await setError(error)
            }
        }
    }

sendPasswordReset(withEmail:) 메서드를 호출하면, Firebase는 해당 이메일 주소로 비밀번호 재설정 링크가 포함된 이메일을 전송합니다.

사용자가 이메일에 포함된 링크를 클릭하면, 비밀번호를 재설정할 수 있는 페이지로 이동하게 됩니다.

registerUser가 레전드인데...

    func registerUser() {
        Task {
            do {
                // Step 1: Creating Firebase Account
                try await Auth.auth().createUser(withEmail: emailID, password: password)
                // Step 2: Uploading Profile Photo Into Firebase Storage
                guard let userUID = Auth.auth().currentUser?.uid else { return }
                guard let imageData = userProfilePicData else { return }
                let storageRef = Storage.storage().reference().child("Profile_Images").child(userUID)
                let _ = try await storageRef.putDataAsync(imageData)
                // Step 3: Downloading Photo URL
                let downloadURL = try await storageRef.downloadURL()
                // STEP 4: Creating a User Firestore Object
                let user = User(username: userName, userBio: userBio, userBioLink: userBioLink, userUID: userUID, userEmail: emailID, userProfileURL: downloadURL)
                // Step 5: Saving User Doc into Firestore Database
                let _ = try Firestore.firestore().collection("Users").document(userUID).setData(from: user, completion: {
                    error in
                    if error == nil {
                        // MARK: Print Saved Successfully
                        print("Saved Successfully")
                    }
                })
            } catch {
                await setError(error)
            }
        }
    }

Auth.auth().currentUser를 통해 현재 로그인된 사용자를 가져옵니다.
uid는 Firebase Authentication에서 각 사용자에게 부여하는 고유 식별자입니다.
userProfilePicData는 업로드하려는 프로필 사진의 Data 형식입니다.

guard let을 사용하여 imageData를 안전하게 추출하며, 데이터가 없을 경우 함수 실행을 종료합니다.

Storage.storage().reference()를 통해 Firebase Storage의 루트 참조를 가져옵니다.
child("Profile_Images")를 통해 Profile_Images 폴더를 지정하고, 그 아래에 userUID를 파일 이름으로 사용하여 고유한 경로를 생성합니다.

putDataAsync 메서드를 사용하여 imageData를 지정된 storageRef 경로에 업로드합니다.

Firestore.firestore()를 호출하여 Firestore 데이터베이스의 기본 인스턴스를 가져옵니다.

collection("Users")를 통해 "Users"라는 이름의 컬렉션을 참조합니다.
여기서는 "Users"라는 이름의 컬렉션을 참조하여 사용자 데이터를 저장하거나 가져올 수 있습니다.

document(userUID)를 사용하여 해당 컬렉션 내에서 userUID에 해당하는 문서를 참조합니다.

setData(from:) 메서드를 사용하여 user 객체의 데이터를 Firestore에 저장합니다.
이 메서드는 Codable 프로토콜을 준수하는 객체를 받아 Firestore 문서에 매핑합니다.

이제부터는 firebase Storage 연결을 해야하는데, 충격적이게도 Storage 기능은 업그레이드 계정만 가능하고 그 업그레이드 계정은 사업자 번호만 있어야만 만들 수 있다는 사실...

대체 어떤 책임자가 그런 결정을 내린거죠? 사업자번호 필수라는 이 이상한 필수사항은 뭔가요?
여튼 그래서... 사업자등록 하러 갔습니다 ^^.

profile
농담곰을 좋아해요 말랑곰탱이

0개의 댓글