Apple 플랫폼에서 데이터 읽기 및 쓰기

(선택사항) Firebase Local Emulator Suite으로 프로토타입 제작 및 테스트

앱이 Realtime Database의 데이터를 읽고 쓰는 방법에 대해 설명하기 전에 Realtime Database 기능을 프로토타입으로 제작하고 테스트하는 데 사용할 수 있는 도구 모음인 Firebase Local Emulator Suite을 소개합니다. 다양한 데이터 모델을 사용해 보거나, 보안 규칙을 최적화하거나, 백엔드와 상호작용할 수 있는 가장 비용 효율적인 방법을 찾는 경우 라이브 서비스를 배포하지 않고 로컬에서 작업할 수 있다면 큰 도움이 될 것입니다.

Realtime Database 에뮬레이터는 Local Emulator Suite의 일부이며 앱에서 에뮬레이션된 데이터베이스 콘텐츠와 구성은 물론 필요에 따라 에뮬레이션된 프로젝트 리소스(함수, 기타 데이터베이스, 보안 규칙)와 상호작용할 수 있게 해줍니다.

Realtime Database 에뮬레이터를 사용하려면 다음 몇 단계만 거치면 됩니다.

  1. 에뮬레이터에 연결하려면 앱의 테스트 구성에 코드 줄을 추가합니다.
  2. 로컬 프로젝트 디렉터리의 루트에서 firebase emulators:start를 실행합니다.
  3. 평소와 같이 Realtime Database 플랫폼 SDK를 사용하거나 Realtime Database REST API를 사용하여 앱의 프로토타입 코드에서 호출합니다.

자세한 내용은 Realtime DatabaseCloud Functions 관련 둘러보기를 참조하세요. Local Emulator Suite 소개도 살펴보세요.

FIRDatabaseReference 가져오기

데이터베이스에서 데이터를 읽거나 쓰려면 FIRDatabaseReference 인스턴스가 필요합니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

데이터 쓰기

이 문서에서는 Firebase 데이터를 읽고 쓰는 기초적인 방법을 설명합니다.

Firebase 데이터는 Database 참조에 쓰여지며, 참조에 비동기 리스너를 연결하여 검색할 수 있습니다. 리스너는 데이터의 초기 상태가 확인될 때 한 번 트리거된 후 데이터가 변경될 때마다 다시 트리거됩니다.

기본 쓰기 작업

기본 쓰기 작업의 경우 setValue를 사용하여 지정된 참조에 데이터를 저장하고 해당 경로에 있는 기존 데이터를 대체할 수 있습니다. 이 메서드의 용도는 다음과 같습니다.

  • 사용 가능한 JSON 유형에 해당하는 다음과 같은 유형을 전달합니다.
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

예를 들어 다음과 같이 setValue로 사용자를 추가할 수 있습니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

이 방법으로 setValue를 사용하면 지정된 위치에서 하위 노드를 포함하여 모든 데이터를 덮어씁니다. 그러나 전체 객체를 다시 쓰지 않고도 하위 항목을 업데이트하는 방법이 있습니다. 사용자가 프로필을 업데이트하는 것을 허용하려면 다음과 같이 사용자 이름을 업데이트할 수 있습니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

데이터 읽기

값 이벤트를 수신 대기하여 데이터 읽기

경로에서 데이터를 읽고 변경사항을 수신 대기하려면 FIRDatabaseReferenceobserveEventType:withBlock을 사용하여 FIRDataEventTypeValue 이벤트를 관찰합니다.

이벤트 유형 일반적인 용도
FIRDataEventTypeValue 경로의 전체 콘텐츠를 읽고 변경사항을 수신 대기합니다.

FIRDataEventTypeValue 이벤트를 사용하면 이벤트 발생 시점에 존재하는 지정된 경로에서 데이터를 읽을 수 있습니다. 이 메서드는 리스너가 연결될 때 한 번 트리거된 후 하위 요소를 포함하여 데이터가 변경될 때마다 다시 트리거됩니다. 하위 데이터를 포함하여 해당 위치의 모든 데이터를 포함하는 snapshot이 이벤트 콜백에 전달됩니다. 데이터가 없으면 스냅샷은 exists()를 호출할 때 false를 반환하고 value 속성을 읽을 때 nil을 반환합니다.

다음은 데이터베이스에서 게시물의 세부정보를 검색하는 소셜 블로깅 애플리케이션의 예시입니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

리스너는 이벤트 발생 시점에 데이터베이스에서 지정된 위치에 있는 데이터를 포함한 FIRDataSnapshotvalue 속성에 수신합니다. 이 값을 NSDictionary와 같은 적절한 네이티브 유형에 할당할 수 있습니다. 해당 위치에 데이터가 없으면 valuenil입니다.

데이터 한 번 읽기

getData()를 사용하여 한 번 읽기

SDK는 앱이 온라인이든 오프라인이든 상관없이 데이터베이스 서버와의 상호작용을 관리하도록 설계되었습니다.

일반적으로 위에서 설명한 값 이벤트 기법을 사용하여 데이터를 읽어 백엔드에서 데이터에 대한 업데이트 알림을 수신해야 합니다. 이러한 기법은 사용량과 청구 비용을 줄여주고 사용자가 온라인과 오프라인으로 전환할 때 최상의 환경을 제공하도록 최적화되어 있습니다.

데이터가 한 번만 필요한 경우 getData()를 사용하여 데이터베이스에서 데이터의 스냅샷을 가져올 수 있습니다. 어떠한 이유로든 getData()가 서버 값을 반환할 수 없는 경우 클라이언트는 로컬 스토리지 캐시를 프로브하고 값을 여전히 찾을 수 없으면 오류를 반환합니다.

다음은 사용자의 공개 사용자 이름을 데이터베이스에서 한 번 검색하는 방법을 보여주는 예시입니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid];
[[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) {
  if (error) {
    NSLog(@"Received an error %@", error);
    return;
  }
  NSString *userName = snapshot.value;
}];

불필요한 getData() 사용은 대역폭 사용을 증가시키고 성능 저하를 유발할 수 있지만 위와 같이 실시간 리스너를 사용하면 이를 방지할 수 있습니다.

관찰자를 사용하여 데이터 한 번 읽기

경우에 따라 서버의 업데이트된 값을 확인하는 대신 로컬 캐시의 값을 즉시 반환하고 싶을 수 있습니다. 이 경우에는 observeSingleEventOfType를 사용하여 로컬 디스크 캐시에서 데이터를 즉시 가져올 수 있습니다.

이 방법은 한 번 로드된 후 자주 변경되지 않거나 능동적으로 수신 대기할 필요가 없는 데이터에 유용합니다. 예를 들어 위 예시의 블로깅 앱에서는 사용자가 새 게시물을 작성하기 시작할 때 이 메서드로 사용자의 프로필을 로드합니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
let userID = Auth.auth().currentUser?.uid
ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in
  // Get user value
  let value = snapshot.value as? NSDictionary
  let username = value?["username"] as? String ?? ""
  let user = User(username: username)

  // ...
}) { error in
  print(error.localizedDescription)
}

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
NSString *userID = [FIRAuth auth].currentUser.uid;
[[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  // Get user value
  User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]];

  // ...
} withCancelBlock:^(NSError * _Nonnull error) {
  NSLog(@"%@", error.localizedDescription);
}];

데이터 업데이트 또는 삭제

특정 필드 업데이트

다른 하위 노드를 덮어쓰지 않고 특정 하위 노드에 동시에 쓰려면 updateChildValues 메서드를 사용합니다.

updateChildValues를 호출할 때 키 경로를 지정하여 더 낮은 수준의 하위 항목 값을 업데이트할 수 있습니다. 확장성 개선을 위해 데이터를 여러 위치에 저장한 경우 데이터 팬아웃을 사용하여 해당 데이터의 모든 인스턴스를 업데이트할 수 있습니다. 예를 들어 소셜 블로깅 앱에서 게시물을 생성한 후 최근 활동 피드 및 게시자의 활동 피드에 동시에 업데이트해야 할 수 있습니다. 이러한 경우 다음과 같은 코드를 사용합니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
guard let key = ref.child("posts").childByAutoId().key else { return }
let post = ["uid": userID,
            "author": username,
            "title": title,
            "body": body]
let childUpdates = ["/posts/\(key)": post,
                    "/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
NSString *key = [[_ref child:@"posts"] childByAutoId].key;
NSDictionary *post = @{@"uid": userID,
                       @"author": username,
                       @"title": title,
                       @"body": body};
NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post,
                               [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post};
[_ref updateChildValues:childUpdates];

이 예시에서는 childByAutoId를 사용하여 모든 사용자의 게시물을 포함하는 노드(/posts/$postid)에서 게시물을 작성하는 동시에 getKey()로 키를 검색합니다. 그런 다음 이 키를 사용하여 /user-posts/$userid/$postid에서 사용자의 게시물에 두 번째 항목을 작성합니다.

이 경로를 사용하면 이 예시에서 두 위치에 새 게시물을 생성한 것처럼 updateChildValues를 한 번만 호출하여 JSON 트리의 여러 위치에서 동시에 업데이트를 수행할 수 있습니다. 이러한 동시 업데이트는 원자적인 성격을 갖습니다. 즉, 모든 업데이트가 한꺼번에 성공하거나 실패합니다.

완료 블록 추가

데이터가 커밋된 시점을 파악하려면 완료 블록을 추가합니다. setValueupdateChildValues는 둘 다 쓰기가 데이터베이스에 커밋될 때 호출되는 선택적 완료 콜백을 사용합니다. 이 리스너는 저장된 데이터와 아직 동기화 중인 데이터를 추적하는 데 유용합니다. 호출이 실패하면 실패 이유를 나타내는 오류 객체가 리스너로 전달됩니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
do {
  try await ref.child("users").child(user.uid).setValue(["username": username])
  print("Data saved successfully!")
} catch {
  print("Data could not be saved: \(error).")
}

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
  if (error) {
    NSLog(@"Data could not be saved: %@", error);
  } else {
    NSLog(@"Data saved successfully.");
  }
}];

데이터 삭제

데이터를 삭제하는 가장 간단한 방법은 해당 데이터 위치의 참조에 removeValue를 호출하는 것입니다.

setValue 또는 updateChildValues 등의 다른 쓰기 작업 값으로 nil을 지정하여 삭제할 수도 있습니다. updateChildValues에 이 방법을 사용하면 API 호출 한 번으로 여러 하위 항목을 삭제할 수 있습니다.

리스너 분리

ViewController를 벗어나도 관찰자는 데이터 동기화를 자동으로 중지하지 않습니다. 관찰자를 적절히 삭제하지 않으면 데이터가 계속 로컬 메모리와 동기화됩니다. 관찰자가 더 이상 필요하지 않으면 연결된 FIRDatabaseHandleremoveObserverWithHandle 메서드에 전달하여 삭제하세요.

참조에 콜백 블록을 추가하면 FIRDatabaseHandle이 반환됩니다. 이 핸들을 사용하여 콜백 블록을 삭제할 수 있습니다.

하나의 데이터베이스 참조에 여러 리스너를 추가하면 이벤트가 발생할 때 각 리스너가 모두 호출됩니다. 해당 위치에서의 데이터 동기화를 중지하려면 removeAllObservers 메서드를 호출하여 특정 위치의 모든 관찰자를 삭제해야 합니다.

리스너에서 removeObserverWithHandle 또는 removeAllObservers를 호출해도 하위 노드에 등록된 리스너는 자동으로 삭제되지 않습니다. 이러한 참조 또는 핸들을 추적하여 삭제해야 합니다.

데이터를 트랜잭션으로 저장

증분 카운터와 같이 동시 수정으로 인해 손상될 수 있는 데이터를 사용하는 경우 트랜잭션 작업을 사용할 수 있습니다. 이 작업에 지정하는 두 인수는 업데이트 함수 및 선택적 완료 콜백입니다. 업데이트 함수는 데이터의 현재 상태를 인수로 취하고 이 데이터에 새로 기록하려는 값을 반환합니다.

예를 들어 소셜 블로깅 앱에서는 다음과 같이 사용자가 게시물에 별표를 주거나 삭제할 수 있고 게시물이 별표를 몇 개 받았는지 집계할 수 있습니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
  if var post = currentData.value as? [String: AnyObject],
    let uid = Auth.auth().currentUser?.uid {
    var stars: [String: Bool]
    stars = post["stars"] as? [String: Bool] ?? [:]
    var starCount = post["starCount"] as? Int ?? 0
    if let _ = stars[uid] {
      // Unstar the post and remove self from stars
      starCount -= 1
      stars.removeValue(forKey: uid)
    } else {
      // Star the post and add self to stars
      starCount += 1
      stars[uid] = true
    }
    post["starCount"] = starCount as AnyObject?
    post["stars"] = stars as AnyObject?

    // Set value and report transaction success
    currentData.value = post

    return TransactionResult.success(withValue: currentData)
  }
  return TransactionResult.success(withValue: currentData)
}) { error, committed, snapshot in
  if let error = error {
    print(error.localizedDescription)
  }
}

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) {
  NSMutableDictionary *post = currentData.value;
  if (!post || [post isEqual:[NSNull null]]) {
    return [FIRTransactionResult successWithValue:currentData];
  }

  NSMutableDictionary *stars = post[@"stars"];
  if (!stars) {
    stars = [[NSMutableDictionary alloc] initWithCapacity:1];
  }
  NSString *uid = [FIRAuth auth].currentUser.uid;
  int starCount = [post[@"starCount"] intValue];
  if (stars[uid]) {
    // Unstar the post and remove self from stars
    starCount--;
    [stars removeObjectForKey:uid];
  } else {
    // Star the post and add self to stars
    starCount++;
    stars[uid] = @YES;
  }
  post[@"stars"] = stars;
  post[@"starCount"] = @(starCount);

  // Set value and report transaction success
  currentData.value = post;
  return [FIRTransactionResult successWithValue:currentData];
} andCompletionBlock:^(NSError * _Nullable error,
                       BOOL committed,
                       FIRDataSnapshot * _Nullable snapshot) {
  // Transaction completed
  if (error) {
    NSLog(@"%@", error.localizedDescription);
  }
}];

트랜잭션을 사용하면 여러 사용자가 같은 게시물에 동시에 별표를 주거나 클라이언트 데이터의 동기화가 어긋나도 별표가 잘못 집계되지 않습니다. FIRMutableData 클래스에 포함되는 값은 애초에 해당 경로에 대해 클라이언트에 마지막으로 알려진 값이거나 값이 없는 경우 nil입니다. 서버는 초기 값과 현재 값을 비교하여 값이 일치하면 트랜잭션을 수락하고, 그렇지 않으면 거부합니다. 트랜잭션이 거부되면 서버에서 현재 값을 클라이언트에 반환하며, 클라이언트는 업데이트된 값으로 트랜잭션을 다시 실행합니다. 트랜잭션이 수락되거나 시도가 일정 횟수를 초과할 때까지 이 과정이 반복됩니다.

서버 측 원자적 증분

위의 사용 사례에서는 두 가지 값, 즉 게시물에 별표표시를 하거나 별표를 삭제한 사용자의 ID와 증가된 별표 수를 데이터베이스에 씁니다. 사용자가 게시물에 별표표시를 하고 있다는 것을 이미 안다면 트랜잭션 대신 원자적 증분 작업을 사용할 수 있습니다.

Swift

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
let updates = [
  "posts/\(postID)/stars/\(userID)": true,
  "posts/\(postID)/starCount": ServerValue.increment(1),
  "user-posts/\(postID)/stars/\(userID)": true,
  "user-posts/\(postID)/starCount": ServerValue.increment(1)
] as [String : Any]
Database.database().reference().updateChildValues(updates)

Objective-C

참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1],
                        [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]};
[[[FIRDatabase database] reference] updateChildValues:updates];

이 코드는 트랜잭션 작업을 사용하지 않으므로 충돌하는 업데이트가 있는 경우 코드가 자동으로 다시 실행되지 않습니다. 하지만 증분 작업은 데이터베이스 서버에서 직접 이루어지므로 충돌이 발생할 가능성은 없습니다.

사용자가 이전에 이미 별표표시한 게시물에 별표를 표시하는 상황처럼 애플리케이션별 충돌을 감지하고 거부하려면 해당 사용 사례에 대한 커스텀 보안 규칙을 작성해야 합니다.

오프라인으로 데이터 작업하기

클라이언트의 네트워크 연결이 끊겨도 앱은 계속 정상적으로 작동합니다.

Firebase 데이터베이스에 연결된 모든 클라이언트는 자체적으로 활성 데이터의 내부 버전을 유지합니다. 데이터를 쓰면 우선 로컬 버전에 기록됩니다. 그런 다음 Firebase 클라이언트가 해당 데이터를 원격 데이터베이스 서버 및 다른 클라이언트와 '최선을 다해' 동기화합니다.

이와 같이 데이터베이스에 대한 모든 쓰기 작업은 로컬 이벤트를 즉시 트리거하며, 그 이후에 서버에 데이터가 기록됩니다. 따라서 앱은 네트워크 지연 또는 연결 여부에 관계없이 응답성을 유지합니다.

네트워크에 다시 연결되면 앱에서 적절한 이벤트 세트를 수신하여 클라이언트와 현재 서버 상태를 동기화하므로 커스텀 코드를 별도로 작성할 필요가 없습니다.

오프라인 동작에 대한 자세한 내용은 온라인 및 오프라인 기능에 대해 자세히 알아보기에서 살펴보겠습니다.

다음 단계