Firebase iOS Codelab: FriendlyChat

1. 개요

iOS용 FriendlyChat Codelab에 오신 것을 환영합니다. 이 Codelab에서는 Firebase와 SwiftUI, Swift 동시 실행, 관찰 프레임워크와 같은 최신 Swift 기능을 사용하여 모든 기능을 갖춘 실시간 채팅 iOS 애플리케이션을 빌드하는 방법을 알아봅니다.

학습할 내용

  • 최신 관찰 프레임워크 (@Observable) 로 상태를 관리합니다.
  • Firebase 인증 을 사용하여 사용자를 인증합니다.
  • Cloud Firestore 를 사용하여 채팅 메시지를 실시간으로 동기화합니다.
  • Firebase용 Cloud Storage 를 사용하여 이미지를 저장하고 제공합니다.
  • 최신 Swift 동시 실행 기능을 사용하여 Firebase API에 액세스합니다.

필요한 항목

  • Xcode 최신 버전
  • iOS 시뮬레이터 또는 실제 테스트 기기
  • Firebase 프로젝트를 만들고 관리할 Google 계정

2. 샘플 코드 가져오기

명령줄에서 GitHub 저장소를 클론합니다.

$ git clone https://github.com/firebase/codelab-friendlychat-ios

codelab-friendlychat-ios 저장소에는 다음과 같은 여러 샘플 프로젝트가 있습니다.

  • ios-starter/swift-starter : SwiftUI 레이아웃 스켈레톤과 Firebase로 앱 기능을 구현할 TODO 주석이 포함된 시작 Xcode 프로젝트입니다.
  • ios/swift : 완성된 샘플 애플리케이션이 포함된 완료된 Xcode 프로젝트입니다.

3. 시작 앱 빌드

시작 애플리케이션을 빌드하고 실행하여 사용자 인터페이스에 익숙해집니다.

  1. Finder 또는 터미널에서 ios-starter/swift-starter 디렉터리로 이동합니다.
  2. FriendlyChatSwift.xcodeproj 를 더블클릭하여 Xcode에서 프로젝트를 엽니다.
  3. Xcode는 백그라운드에서 Swift Package Manager 종속 항목을 자동으로 확인합니다 (GitHub에서 firebase-ios-sdk v12 이상 다운로드).
  4. iOS 시뮬레이터 (예: iPhone 17 Pro)를 선택하고 실행 버튼 (cmd+R)을 클릭합니다.

몇 초 후 FriendlyChat 로그인 화면이 표시됩니다. 이때 로그인 버튼과 메시지 피드는 자리표시자입니다. 다음 단계에서 Firebase를 앱에 연결합니다.

4. Firebase 프로젝트 설정

새 Firebase 프로젝트 만들기

  1. Google 계정을 사용하여 Firebase Console에 로그인합니다.
  2. 버튼을 클릭하여 새 프로젝트를 만든 다음 프로젝트 이름 (예: FriendlyChat)을 입력합니다.
  3. 계속 을 클릭합니다.
  4. 메시지가 표시되면 Firebase 약관을 검토하고 이에 동의한 다음 계속을 클릭합니다.
  5. (선택사항) Firebase Console에서 AI 지원('Firebase의 Gemini'라고 함)을 사용 설정합니다.
  6. 이 Codelab에서는 Google 애널리틱스가 필요하지 않으므로 Google 애널리틱스 옵션을 사용 중지 합니다.
  7. 프로젝트 만들기 를 클릭하고 프로젝트가 프로비저닝될 때까지 기다린 후 계속 을 클릭합니다.

Firebase 요금제 업그레이드

Firebase용 Cloud Storage를 사용하려면 Firebase 프로젝트가 사용한 만큼만 지불하는 (Blaze) 요금제를 사용하고 있어야 합니다. 즉, Cloud Billing 계정에 연결되어 있어야 합니다.

  • Cloud Billing 계정에는 신용카드와 같은 결제 수단이 필요합니다.
  • Firebase와 Google Cloud를 처음 사용하는 경우 $300 크레딧과 무료 체험판 Cloud Billing 계정을 받을 자격이 되는지 확인하세요.
  • 이벤트의 일환으로 이 Codelab을 진행하는 경우 주최자에게 사용 가능한 Cloud 크레딧이 있는지 문의하세요.

프로젝트를 Blaze 요금제로 업그레이드하려면 다음 단계를 따르세요.

  1. Firebase Console에서 요금제를 업그레이드할 요금제를 선택합니다.
  2. Blaze 요금제를 선택합니다. 화면에 표시된 안내에 따라 Cloud Billing 계정을 프로젝트에 연결합니다.
    이 업그레이드의 일환으로 Cloud Billing 계정을 만들어야 하는 경우 업그레이드를 완료하기 위해 Firebase Console의 업그레이드 흐름으로 돌아가야 할 수 있습니다.

iOS 앱 연결

  1. 프로젝트 개요 화면에서 iOS 아이콘을 클릭하여 앱 설정 워크플로를 시작합니다.
  2. 번들 ID를 입력합니다.
    com.google.firebase.codelab.FriendlyChatSwift
    
  3. 앱 등록 을 클릭합니다.

Xcode 프로젝트에 GoogleService-Info.plist 추가

  1. GoogleService-Info.plist 다운로드 를 클릭하여 구성 파일을 저장합니다.
  2. Xcode에서 다운로드한 GoogleService-Info.plist 파일을 프로젝트 탐색기의 FriendlyChatSwift 기본 그룹으로 드래그합니다.
  3. 메시지가 표시되면 필요한 경우 항목 복사 가 선택되어 있고 FriendlyChatSwift 타겟이 선택되어 있는지 확인한 다음 완료 를 클릭합니다.

FriendlyChatSwiftApp.swift에서 Firebase 구성

마지막으로 SwiftUI 애플리케이션이 시작될 때 프로젝트 구성으로 Firebase를 초기화합니다. FriendlyChatSwiftApp.swift 를 열고 TODO 자리표시자를 import FirebaseCoreFirebaseApp.configure()로 바꿉니다.

import SwiftUI
import FirebaseCore

@main
struct FriendlyChatSwiftApp: App {
  init() {
    FirebaseApp.configure()
  }

  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

5. 사용자 인증

Firebase Console에서 인증 구성

사용자를 인증하려면 먼저 Firebase Console에서 원하는 인증 제공업체를 사용 설정해야 합니다. 이 가이드에서는 이메일/비밀번호 인증을 사용합니다.

  1. Firebase Console에서 보안 > 인증으로 이동한 다음 시작하기를 클릭합니다.
  2. 로그인 방법 탭을 선택합니다.
  3. 이메일/비밀번호를 클릭하고 사용 설정 전환 스위치를 클릭한 다음 저장을 클릭합니다.

인증 규칙으로 Cloud Firestore 보호

다음으로 사용자가 메시지를 읽거나 쓰기 전에 인증을 받아야 합니다.

  1. Firebase Console에서 데이터베이스 및 스토리지 > Firestore 데이터베이스 로 이동한 다음 데이터베이스 만들기 를 클릭합니다.
  2. 위치를 선택하고 다음 을 클릭합니다.
  3. 테스트 모드에서 시작 을 선택하고 만들기 를 클릭합니다.
  4. 규칙 탭을 선택하고 보안 규칙을 다음과 같이 업데이트합니다.
    rules_version = '2';
    
    service cloud.firestore {
      match /databases/{database}/documents {
        match /messages/{messageId} {
          allow read, write: if request.auth != null;
        }
      }
    }
    
    이러한 규칙을 사용하면 인증된 모든 사용자가 데이터베이스에서 메시지를 읽거나 쓸 수 있으므로 학습 목적으로 적합합니다. 보안 규칙에 관한 자세한 내용은 보안 규칙 문서를 참고하세요.
  5. 게시 를 클릭합니다.

UserViewModel.swift에서 인증 상태 관찰

SwiftUI에서 Firebase 인증 상태를 반응형으로 관찰하면 로그인 화면과 채팅 인터페이스 간에 UI가 원활하게 전환됩니다.

ViewModels/UserViewModel.swift 를 열고 init()TODOauthStateChanges AsyncSequence를 사용하여 인증 상태 변경을 관찰하도록 바꿉니다.

  init() {
    authTask = Task {
      for await user in Auth.auth().authStateChanges {
        self.user = user
      }
    }
  }

Swift 동시 실행으로 로그인, 가입, 로그아웃 구현

UserViewModel.swift에서 나머지 TODO 주석을 최신 async/await 인증 메서드로 바꿉니다.

  func signIn(email: String, password: String) async {
    errorMessage = nil
    if email.isEmpty || password.isEmpty {
      showError("Please enter both email and password.")
      return
    }
    do {
      try await Auth.auth().signIn(withEmail: email, password: password)
    } catch {
      showError(error.localizedDescription)
    }
  }

  func signUp(email: String, password: String, displayName: String) async {
    errorMessage = nil
    if email.isEmpty || password.isEmpty {
      showError("Please enter both email and password.")
      return
    }
    do {
      let result =
        try await Auth.auth().createUser(withEmail: email, password: password)
      let changeRequest = result.user.createProfileChangeRequest()
      changeRequest.displayName = displayName.isEmpty ? email : displayName
      try await changeRequest.commitChanges()
      self.user = Auth.auth().currentUser
    } catch {
      showError(error.localizedDescription)
    }
  }

  func updateDisplayName(_ displayName: String) async {
    guard let currentUser = Auth.auth().currentUser else { return }
    do {
      let changeRequest = currentUser.createProfileChangeRequest()
      changeRequest.displayName = displayName
      try await changeRequest.commitChanges()
      self.user = Auth.auth().currentUser
    } catch {
      showError(error.localizedDescription)
    }
  }

  func signOut() {
    do {
      try Auth.auth().signOut()
    } catch {
      showError(error.localizedDescription)
    }
  }

사용자 인증 테스트

  1. Xcode에서 실행 버튼을 클릭합니다.
  2. 앱 시뮬레이터에서 가입을 클릭하고 표시 이름, 이메일, 비밀번호를 입력한 다음 가입을 탭합니다.
  3. 자동으로 인증되고 빈 메시지 화면으로 이동됩니다.

6. Cloud Firestore에서 읽기

Firebase Console에서 샘플 메시지 추가

샘플 메시지로 데이터베이스를 채웁니다.

  1. Firebase Console에서 Firestore 데이터베이스 로 이동하고 데이터 탭을 선택합니다.
  2. 컬렉션 시작 을 클릭합니다.
  3. **컬렉션 ID** 에 messages를 입력한 다음 다음을 클릭합니다.
  4. 문서 ID 를 자동 생성으로 설정된 상태로 두거나 자동 ID 를 클릭합니다.
  5. 문서에 다음 필드를 추가합니다.
    • text (유형: string, 값: Hello)
    • displayName (유형: string, 값: anonymous)
  6. 저장 을 클릭합니다.
  7. 선택적으로 문서 추가 를 클릭하여 샘플 메시지를 더 추가합니다.

FriendlyMessageViewModel.swift에서 메시지 동기화

SwiftUI에서 @Observable 모델은 선언적 UI 업데이트를 구동합니다. ViewModels/FriendlyMessageViewModel.swift 를 열고 snapshots AsyncSequence를 사용하여 startListening()stopListening()을 구현합니다.

  func startListening() {
    stopListening()
    listenerTask = Task {
      let db = Firestore.firestore()
      do {
        for try await snapshot in db.collection("messages").snapshots {
          self.messages = snapshot.documents.compactMap { document in
            try? document.data(as: FriendlyMessage.self)
          }
        }
      } catch {
        print("Error listening for messages: \(error)")
      }
    }
  }

  func stopListening() {
    listenerTask?.cancel()
    listenerTask = nil
    messages.removeAll()
  }

SwiftUI에서 메시지를 렌더링하고 자동 스크롤하는 방법

Views/ContentView.swift에서 ScrollViewReaderLazyVStack이 채팅 대화창을 렌더링하고 messages.count가 변경될 때마다 최신 메시지로 자동 스크롤하는 방법을 확인합니다.

ScrollViewReader { scrollViewReader in
  ScrollView {
    LazyVStack(spacing: 12) {
      ForEach(messageViewModel.messages) { message in
        FriendlyMessageView(friendlyMessage: message)
          .id(message.id)
      }
    }
    .padding(.horizontal)
    .onChange(of: messageViewModel.messages.count) { _, count in
      guard count > 0,
            let lastId = messageViewModel.messages.last?.id else { return }
      withAnimation(.easeInOut) {
        scrollViewReader.scrollTo(lastId, anchor: .bottom)
      }
    }
  }
}

SwiftUI를 사용하면 뷰 모델을 통해 데이터베이스 상태를 UI 상태에 직접 연결하여 앱 코드가 전환을 애니메이션에 깔끔하게 매핑할 수 있습니다. 다음 단계에서는 앱에서 데이터베이스에 데이터를 작성하여 이 애니메이션이 작동하는지 확인합니다.

7. 메시지 보내기

Swift 동시 실행으로 sendMessage 구현

addDocument()를 사용하여 컬렉션에 문서를 추가하면 Cloud Firestore는 각 채팅 메시지에 고유한 ID를 생성합니다.

ViewModels/FriendlyMessageViewModel.swift에서 sendMessage(text:imageUrl:)TODOasync throws 메서드로 바꿉니다.

  func sendMessage(text: String?, imageUrl: String?) async throws {
    guard let currentUser = Auth.auth().currentUser else { return }
    let message = FriendlyMessage(
      text: text,
      displayName: currentUser.displayName ?? currentUser.email ?? "Anonymous",
      imageUrl: imageUrl,
      userId: currentUser.uid
    )
    let db = Firestore.firestore()
    _ = try db.collection("messages").addDocument(from: message)
  }

메시지 전송 테스트

  1. Xcode에서 실행 버튼을 클릭합니다.
  2. 계정에 로그인합니다.
  3. 하단 텍스트 필드에 메시지를 입력하고 보내기 아이콘을 탭합니다.
  4. 시뮬레이터에 메시지가 즉시 표시되는지 확인합니다.

8. 이미지 저장 및 수신

Firebase용 Cloud Storage 설정

  1. Firebase Console에서 데이터베이스 및 스토리지 > 스토리지로 이동한 다음 시작하기를 클릭합니다.
  2. 테스트 모드에서 시작 을 선택하고 다음 을 클릭합니다.
  3. 기본 스토리지 위치를 그대로 사용하고 완료 를 클릭합니다.

SwiftUI PhotosPicker를 사용한 네이티브 이미지 선택

최신 SwiftUI (iOS 16 이상)는 import PhotosUIPhotosPicker 를 통해 네이티브 사진 선택을 제공합니다. UIKit 위임 브리지 또는 UIImagePickerController 래퍼가 필요하지 않습니다.

Views/FooterView.swift에서 PhotosPicker가 선택된 이미지를 바인딩하는 방법을 확인합니다.

PhotosPicker(selection: $selectedItem, matching: .images) {
  Image(systemName: "photo.on.rectangle.angled")
    .font(.system(size: 26))
    .foregroundStyle(.blue)
    .accessibilityLabel("Select photo")
}
.onChange(of: selectedItem) { _, newItem in
  Task {
    if let data = try? await newItem?.loadTransferable(type: Data.self) {
      await uploadAndSendImage(data: data)
    }
  }
}

이미지 업로드 구현

사진을 선택하면 이미지 데이터를 Cloud Storage에 업로드하고 공개 다운로드 URL을 Cloud Firestore에 저장합니다.

Views/FooterView.swift에서 uploadAndSendImage(data:)TODO를 바꿉니다.

  private func uploadAndSendImage(data: Data) async {
    guard let uid = Auth.auth().currentUser?.uid else { return }
    isUploading = true
    defer {
      isUploading = false
      selectedItem = nil
    }
    do {
      let filename = "\(uid)/\(UUID().uuidString).jpg"
      let storageRef =
        Storage.storage().reference().child("images").child(filename)
      let metadata = StorageMetadata()
      metadata.contentType = "image/jpeg"

      _ = try await storageRef.putDataAsync(data, metadata: metadata)
      let downloadURL = try await storageRef.downloadURL()
      try await viewModel.sendMessage(
        text: nil, imageUrl: downloadURL.absoluteString)
    } catch {
      print("Error uploading image: \(error.localizedDescription)")
    }
  }

Cloud Storage를 사용한 이미지 공유

Views/FriendlyMessageImageView.swift에서 이미지 URL은 Swift 동시 실행 및 Cloud Storage SDK를 사용하여 비동기식으로 로드됩니다.

  • 표준 HTTP/HTTPS 다운로드 URL은 URLSession을 통해 비동기식으로 검색됩니다.
  • Google Cloud Storage URL (예: gs://...)은 Storage.storage().reference(forURL:)을 사용하여 가져옵니다.

이미지 메시지 테스트

  1. Xcode에서 실행 버튼을 클릭합니다.
  2. 계정에 로그인합니다.
  3. 사진 아이콘을 탭하고 라이브러리에서 이미지를 선택한 다음 채팅에 업로드되어 표시되는지 확인합니다.

9. 결론

수고하셨습니다. Swift와 Firebase를 사용하여 실시간 iOS 채팅 애플리케이션을 빌드했습니다.

학습한 내용

  • firebase-ios-sdkSwift Package Manager (SPM) Xcode 통합
  • SwiftUI 선언적 레이아웃, PhotosPicker, ScrollViewReader 자동 스크롤
  • 인증, Cloud Firestore, Cloud Storage 전반의 Swift 동시 실행 (async/await)
  • 반응형 상태 관찰을 통한 이메일/비밀번호 및 익명 인증
  • Cloud Firestore 실시간 스냅샷 수신 대기 및 자동 ID 쓰기
  • Firebase용 Cloud Storage 바이너리 업로드 및 공개 다운로드 URL

자세히 알아보기