
앞서 로그인과 회원가입을 구현한 후 로그인하면 사용자(닉네임, 이메일, 그리고 프로필 설정 사진)의 정보가 나오는 프로필 뷰를 간단하게 구현하였다. 이 부분에서 Firebase Storage를 이용하여 사용자가 지정한 사진을 데이터베이스에 저장한 후, 사용자가 로그인하면 데이터베이스의 정보를 가져와 사용자가 지정한 프로필의 내용이 나오도록 하려고 했다.
SwiftUI 초기에는 이미지 관련 기능을 사용하려면 UIKit과의 브리징으로UIImagePickerController를 사용하여 이미지를 선택하고 업로드하는 기능을 구현해야 했는데, PhotoPicker를 사용하게 되어 SwiftUI만으로도 이미지를 선택하고 업로드하는 기능을 구현할 수 있게 되었다.
그렇기에 PhotoPicker API를 이용하여 사진첩의 사진을 프로필로 설정할 수 있게 해놓고, Firebase Storage에 이미지를 저장하는 구현을 하려고 한다.
VStack(alignment: .center) {
if let profileImage = profileImage {
Image(uiImage: profileImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.onTapGesture {
shouldShowPhotoPicker.toggle()
}
} else {
Image(systemName: "person.circle")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.onTapGesture {
shouldShowPhotoPicker.toggle()
}
}
}
.frame(maxWidth: .infinity)
.padding(.bottom, 30)
.photosPicker(isPresented: $shouldShowPhotoPicker, selection: $selectedItem, matching: .images)
.onSubmit {
Task {
if let selectedItem,
let data = try? await selectedItem.loadTransferable(type: Data.self) {
if let image = UIImage(data: data) {
profileImage = image
}
}
selectedItem = nil
}
}
프로필의 이미지를 교체하는 과정에 위 코드와 같이 구현하던 중, 프로필 이미지가 변경이 한 템포씩 느린 오류를 발견했는데. 이 문제를 해결하기 위해 생각보다 많은 시간이 들었다.. 결론적으로는 제가 무지했기 때문이었습니다:)
원래 사용한 .onSubmit은 텍스트 필드의 입력에 대응하는 동작을 정의하는 데 사용되며, 사용자의 입력이 완료되고 `return'키를 누르면 호출되는 메서드였다. 따라서 이미지 선택과 같은 작업에는 적합하지 않는 기능이었던 것이다.
.onChange는 특정 상태의 값이 변경될 때 마다 감지를 하고 호출하는 메서드였고, 이번 구현처럼 사진을 선택하면 UI에 바로 업데이트를 해야 하는 구현에 아주 용이한 메서드이다. 아래와 같이 .onChange를 사용하여 프로필 사진의 변경을 바로 업데이트 할 수 있게 수정하였다.!
.onChange(of: selectedItem) {_, _ in
Task {
if let selectedItem,
let data = try? await selectedItem.loadTransferable(type: Data.self) {
if let image = UIImage(data: data) {
profileImage = image
}
}
selectedItem = nil
}
}
Firebase Storage는 Firebase의 클라우드 저장소 서비스로, 사용자가 생성한 데이터를 안전하게 저장하고 공유할 수 있게 지원해준다. 이 서비스는 용량의 크기 상관없이 파일을 저장하고 전송하는 데 특히 유용하고, 이미지, 동영상, 오디오 파일 등 다양한 형식의 파일을 저장할 수 있다. (이전에 3D모델인 .usdz까지 되더라)
아래와 같이 단계별로 봐도 이해되기 좋을것 같다.
// 1. 사용자가 선택한 이미지를 Data 객체로 변환.
// 이때, 이미지의 품질을 조절하기 위해 jpegData(compressionQuality:) 메서드를 사용할 수 있다.
guard let imageData = image.jpegData(compressionQuality: 0.5) else {
print("이미지를 Data로 변환하는데 실패했습니다.")
completion(nil)
return
}
// 2.firebase Storage에 이미지를 업로드한다.
// StorageReference 객체를 사용하여 업로드할 위치를 지정한다.
// 지정한 업로드 장소는 childe에 넣어줘야 하겠죠.
let storageRef = storage.reference().child("profile_images/\(userId).jpg")
storageRef.putData(imageData, metadata: nil) { metadata, error in
if let error = error {
print("이미지 업로드 실패: \(error.localizedDescription)")
completion(nil)
return
}
// 3. 이미지 업로드가 성공하면, 업로드된 이미지의 URL을 가져온다.
// 이 URL은 나중에 이미지를 다운로드하거나 공유하는 데 사용할 수 있다.
storageRef.downloadURL { url, error in
if let error = error {
print("이미지 URL 가져오기 실패: \(error.localizedDescription)")
completion(nil)
} else {
completion(url)
}
}
업로드하는 함수를 구현했다면, 추후에 로그인 후 이미지를 받아와야 하기 때문에,
사용자의 프로필 정보에 이미지 URL을 스토리지가 아닌 데이터베이스에 저장하는 함수를 만들어줘야 한다.
이렇게 하면 사용자가 로그인할 때마다 프로필 이미지를 다시 다운로드할 필요 없이, 저장된 URL을 통해 이미지를 불러올 수 있다.
두 가지를 사용하지만 각각의 역할이 다르다! 데이터를 가져와야 하는곳은 Storage 아닌 Firebase Firestore 에서 해야한다.
Firebase Storage는 사용자가 업로드한 파일(이미지, 동영상 등)을 저장하는 공간이고, 이러한 파일은 URL을 통해 접근할 수 있다.
반면에 Firebase Firestore 또는 Realtime Database는 앱의 데이터를 다양한 형태(String, Array, Date...)로 저장하고 관리하는데 사용된다.
func saveProfileImageUrl(userId: String, url: URL, completion: @escaping (Bool) -> Void) {
db.collection("users").document(userId).updateData(["profileImageUrl": url.absoluteString]) { error in
if let error = error {
print("프로필 이미지 URL 저장 실패: \(error.localizedDescription)")
completion(false)
} else {
completion(true)
}
}
}
이미지를 URL로 다운로드하는 이유
이미지를 업로드하고 다운로드 하는 이유는 무엇일까?
효율성: 이미지 파일은 크기가 크기 때문에, 직접 데이터를 전송하는 것보다 URL을 통해 참조하는 것이 훨씬 효율적이다. URL을 사용하면 필요할 때만 이미지를 다운로드할 수 있으므로, 네트워크 대역폭을 절약하고 앱의 성능을 향상시킬 수 있다.
동적 업데이트: 이미지 URL을 사용하면, 서버에서 이미지를 업데이트하면 자동으로 모든 클라이언트에서 새 이미지를 볼 수 있습니다. 이는 앱을 업데이트하거나 새 데이터를 다운로드하지 않고도 콘텐츠를 실시간으로 업데이트할 수 있게 한다.
저장 공간 절약: 모바일 기기의 저장 공간은 한정되어 있다. 따라서 모든 이미지를 기기에 저장하는 대신, 필요할 때만 이미지를 다운로드하여 보여주는 것이 저장 공간을 효율적으로 활용하는 방법이다.
보안: 이미지를 직접 다운로드하면, 악의적인 사용자가 이미지 데이터를 조작하는 등의 보안 위협에 노출될 수 있는데, 이미지 URL을 사용하면 서버에서 직접 이미지를 제공하므로 이러한 위협을 줄일 수 있.
따라서 Firebase Storage와 같은 클라우드 저장소 서비스에서는 이미지를 업로드하고 URL을 반환하는 방식을 사용한다. 이 방식을 사용하면 위에서 언급한 이점들을 모두 활용할 수 있다.
이전에는 아래와 같은 스타일로 Firestore에 데이터를 저장하도록 설정했놨다.
struct User: Codable {
var userId: String
var nickname: String
var email: String
var profileImage: String?
}
회원가입이 완료되면 Firestore Database에 만든 users라는 컬렉션에 모델과 동일한 형태의 데이터가 생성이 됩니다. 여기서 Firestore는 NoSQL 클라우드 데이터베이스로, 데이터를 문서와 컬렉션의 형태로 저장한다. 이 구조는 개발자가 데이터를 쉽게 저장하고 검색할 수 있도록 도와준다고 하는데.. 좀 더 알아보자..ㅎ
이제 Firestore에서 사용자의 정보를 프로필에 가져오는 방법을 살펴보자.
// 로그인한 사용자 정보 가져오기
func fetchCurrentUser(completion: @escaping (Bool) -> Void) {
guard let userId = auth.currentUser?.uid else {
print("현재 사용자 ID를 가져오는데 실패했습니다.")
completion(false)
return
}
db.collection("users").document(userId).getDocument { document, error in
if let error = error {
print("사용자 정보 가져오기 실패: \(error.localizedDescription)")
completion(false)
} else if let document = document, document.exists {
do {
let user = try document.data(as: User.self)
self.currentUser = user
completion(true)
} catch {
print("사용자 정보 디코딩 실패: \(error.localizedDescription)")
completion(false)
}
} else {
print("사용자 정보가 존재하지 않습니다.")
completion(false)
}
}
위의 fetchCurrentUser 함수는 Firestore에서 로그인한 사용자의 정보를 가져와서 정보가 없던 프로필에 사용자의 정보를 표시하는 역할을 한다.
이 함수는 로그인한 사용자의 ID를 사용하여 Firestore의 users 컬렉션에서 해당 사용자의 문서를 가져옵니다. 문서가 존재하면, 그 데이터를 User 타입으로 디코딩하여 현재 사용자 정보를 업데이트한다.
이렇게 Firestore를 사용하면 실시간으로 데이터를 동기화하고, 오프라인 지원을 제공하며, 여러 데이터베이스 위치에서 데이터를 호스팅할 수 있게 해준다.
아래의 같이 정보가 없던 프로필에 정보를 업데이트 할 수가 있었다.