오늘의 목표
럽카이브(Lovechive)의 Firebase에 Firestore를 추가하고, XCTest를 통해 CRUD 테스트 진행하기
프로젝트에서 Firestore를 사용하기 위해서는 우선 Firebase 프로젝트를 생성해야 한다.(Firebase 사이트 링크)
번들 ID, 서버 등의 설정을 마치면 GoogleService-Info라는 plist 파일을 하나 만들어 주는데, 이 파일을 Xcode 프로젝트에 넣어주어야 한다.

이 때, .gitignore에 미리 plist 파일은 커밋되지 않도록 설정해두면 좋다. plist 파일에는 API ID 같은 중요한 정보가 담기기 때문에 GitHub Repository가 Public한 제한 상태라면 특히 주의하는 것이 좋다.
plist 파일을 추가한 이후에는 Firebase 사이트에서 프로젝트에 접근하여 Firestore를 생성해 주면 되는데, 좌측의 빌드 항목에서 Firestore Database를 선택해주고 새로 생성을 해주면 된다.
이 때, Database ID는 기본값이 Defaults로 설정이 되어 있는데, 만약 실제 서비스용 Database와 개발용 Database를 나누고 싶다면 Dev 등의 ID로 추가로 생성해서 사용하고, 실제 배포시에는 Defaults ID를 사용할 수 있다.
이 경우 프로젝트에서 별도로 DB ID를 지정해서 사용해야 하는 번거로움이 있지만 개발용과 배포용이 나뉘어 관리가 쉬워지는 이점이 있다.
// DB ID를 이용한 초기화 예시
let settings = Firestore.firestore().settings
settings.database = "(default)" // 또는 "dev"
Firestore.firestore().settings = settings
이제 완전히 비어있는 Firestore 프로젝트가 생성되었을텐데, 여기서 "컬렉션"을 추가해서 사용할 수 있다.
럽카이브는 Firestore에서 유저 정보, 연인 정보, 다이어리 데이터, 일정 데이터의 4가지 항목을 다룰 예정이기 때문에 아래와 같은 컬렉션 구조를 만들어 주었다.
📂 users
└ 📄 {userId}
├── name: "홍길동"
├── email: "user@example.com"
├── coupleId: "abc123"
├── createdAt: timestamp
📂 couples
└ 📄 {coupleId}
├── user1: "userId1"
├── user2: "userId2"
├── dDay: timestamp
📂 schedules
└ 📄 {scheduleId}
├── coupleId: "abc123"
├── title: "기념일"
├── date: timestamp
├── createdBy: "userId1"
📂 diaries
└ 📄 {diaryId}
├── coupleId: "abc123"
├── author: "userId1"
├── content: "오늘 데이트 너무 좋았다!"
├── createdAt: timestamp
마지막으로 가장 중요할 수도 있는 규칙에 대해 설정할 수 있는데, 기본적인 규칙 설정은 아래와 같다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
코드를 통해 규칙을 설정하는데, 기본적으로 쓰기/읽기 모두 금지되어 있는 상태이다. 이를 수정하거나 규칙을 추가해서 DB의 규칙을 만들어 줄 수 있다.
아직은 프로젝트를 구현하는 단계이기 때문에 특별히 규칙을 만들어두진 않았지만, 추후 프로젝트를 구현해 나가며 규칙도 함께 업데이트 할 예정이다.
이제 Xcode 프로젝트 파일로 돌아와서 새로운 XCTest 파일을 만들어준다.
기존에 만들어져 있는 테스트 파일을 사용해도 되지만, 구분성을 위해서 새로 만들어 주기로 했다.
총 3가지의 테스트를 만들어서 진행할 예정이고, 테스트 내용은 아래와 같다.
우선 setUpWithError에서 Firebase를 초기화 해주자. 그 전에 중요한 점은, Firebase 코드를 사용하기 위해서는 테스트 파일에도 Firebase를 import 해주어야 한다는 사실이다.
override func setUpWithError() throws {
try super.setUpWithError()
if FirebaseApp.app() == nil {
FirebaseApp.configure()
}
}
위 코드는 테스트 메소드가 실행되기 전에 실행되는 코드로, Firebase를 초기화 시켜주는 코드인데, Firebase는 하나의 앱에서 하나만 존재해야 하기 때문에 if문을 사용해 Firebase가 없을 경우에만 초기화 해주도록 구현한 것이다.
다음으로 tearDownWithError를 설정해준다.
override func tearDownWithError() throws {
try super.tearDownWithError()
Firestore.firestore().terminate { error in
if let error = error {
print("🔥 Firestore 종료 중 오류 발생: \(error.localizedDescription)")
} else {
print("✅ Firestore 정상 종료")
}
}
}
위 코드는 테스트 메소드가 종료된 후 실행되는 코드로, Firebase를 명시적으로 해제하는 코드이다. 이는 혹시 모를 문제상황을 예방하기 위해 Firebase를 명시적으로 해제해줌으로써 Firebase에 의한 오류를 최소화하기 위한 코드이다.
이제 Firestore의 CRUD 메소드를 작성해보자.
// Firestore 쓰기 테스트
func testWriteFirestore() {
// given: Firestore 인스턴스와 테스트할 데이터 준비
let db = Firestore.firestore()
let testDocRef = db.collection("testCollection").document("testDocument")
let testData: [String: Any] = [
"name": "Lovechive Test",
"timestamp": FieldValue.serverTimestamp()
]
let expectation = self.expectation(description: "Firestore 데이터 쓰기 완료")
// when: Firestore에 데이터 저장
testDocRef.setData(testData) { error in
// then: 저장이 성공했는지 검증
XCTAssertNil(error, "Firestore 쓰기 실패: \(error?.localizedDescription ?? "unkowned error")")
expectation.fulfill()
}
waitForExpectations(timeout: 5)
}
// Firestore 읽기 테스트
func testReadFromFirestore() {
// given: Firestore 인스턴스와 문서 참조
let db = Firestore.firestore()
let testDocRef = db.collection("testCollection").document("testDocument")
let expectation = self.expectation(description: "Firestore 데이터 읽기 완료")
// when: Firestore에서 문서를 가져옴
testDocRef.getDocument { (document, error) in
// then: 데이터를 제대로 가져왔는지 검증
XCTAssertNil(error, "Firestore 읽기 실패: \(error?.localizedDescription ?? "unkowned error")")
XCTAssertNotNil(document, "문서가 존재하지 않음")
XCTAssertEqual(document?.data()?["name"] as? String, "Lovechive Test", "데이터 불일치")
expectation.fulfill()
}
waitForExpectations(timeout: 5)
}
// Firestore 삭제 테스트
func testDeleteFromFirestore() {
// given: Firestore 인스턴스와 문서 참조
let db = Firestore.firestore()
let testDocRef = db.collection("testCollection").document("testDocument")
let expectation = self.expectation(description: "Firestore 데이터 삭제 완료")
// when: Firestore에서 문서 삭제
testDocRef.delete { error in
// then: 삭제가 성공했는지 검증
XCTAssertNil(error, "Firestore 삭제 실패: \(error?.localizedDescription ?? "unkowned error")")
expectation.fulfill()
}
waitForExpectations(timeout: 5)
}
위의 테스트 메소드에서 자주 사용된 expectation은 XCTest에서 비동기 작업을 기다릴 때 사용되는 객체이다.
Firestore 같은 네트워크 연산은 비동기 처리되므로, 테스트가 즉시 종료되지 않도록 expectation을 사용해야 하는데, expectation.fulfill()을 호출하면 XCTest가 테스트 완료를 인식하고 다음 단계로 넘어간다.
그리고 waitForExpectations(timeout: 5)을 사용해서 최대 5초 동안 응답을 기다리도록 하여 비동기 작업을 무사히 마칠 수 있도록 하는 것이다. 만약 expectation을 사용하지 않으면, XCTest가 Firestore의 응답을 기다리지 않고 테스트가 종료될 수 있기 때문에 사용하는 것을 권장하고 있다.
위 메소드들을 모두 테스트한 결과, Firestore에 데이터가 저장되고, 이를 불러오고, 삭제까지 완료되는 모습을 볼 수 있었다.
오늘의 목적 달성!