콘솔로 이동

iOS에서 데이터 읽기 및 쓰기

FIRDatabaseReference 가져오기

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

Swift

var ref: DatabaseReference!
ref = Database.database().reference()

Objective-C

@property (strong, nonatomic) FIRDatabaseReference *ref;
self.ref = [[FIRDatabase database] reference];

데이터 읽기 및 쓰기

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

Firebase 데이터는 FIRDatabase 참조에 기록되며, 데이터를 검색하려면 참조에 비동기 리스너를 연결합니다. 리스너는 데이터의 초기 상태가 확인될 때 한 번 호출된 후 데이터가 변경될 때마다 다시 호출됩니다.

기본 쓰기 작업

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

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

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

Swift

self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

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

Swift

self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

value 이벤트 수신 대기

특정 경로의 데이터를 읽고 변경을 수신 대기하려면 FIRDatabaseReferenceobserveEventType:withBlock 또는 observeSingleEventOfType:withBlock 메소드를 사용하여 FIRDataEventTypeValue 이벤트를 관찰합니다.

이벤트 유형 일반적인 용도
FIRDataEventTypeValue 경로의 전체 내용에 대한 변경을 읽고 수신 대기합니다.

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

다음 예제에서는 데이터베이스에서 글의 세부정보를 검색하는 소셜 블로깅 애플리케이션을 보여 줍니다.

Swift

refHandle = postRef.observe(DataEventType.value, with: { (snapshot) in
  let postDict = snapshot.value as? [String : AnyObject] ?? [:]
  // ...
})

Objective-C

_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

리스너는 이벤트 발생 시점에 데이터베이스에서 지정된 위치에 있던 데이터를 value 속성에 포함하는 FIRDataSnapshot을 수신합니다. 이 값을 NSDictionary 등의 적절한 기본 유형에 대입할 수 있습니다. 해당 위치에 데이터가 없는 경우 valuenil입니다.

데이터 한 번 읽기

한 번만 호출되고 즉시 삭제되는 콜백이 필요한 경우가 있습니다. 이후에 변경되지 않는 UI 요소를 초기화할 때가 그 예입니다. 이러한 경우 observeSingleEventOfType 메소드를 사용하면 시나리오가 단순해집니다. 이렇게 추가된 이벤트 콜백은 한 번 호출된 후 다시 호출되지 않습니다.

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

Swift

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

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

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

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

ref.child("users").child(user.uid).setValue(["username": username]) {
  (error:Error?, ref:DatabaseReference) in
  if let error = error {
    print("Data could not be saved: \(error).")
  } else {
    print("Data saved successfully!")
  }
}

Objective-C

[[[_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에서 벗어날 때 데이터 동기화를 자동으로 중지하지 않습니다. 관찰자를 적절히 삭제하지 않으면 데이터가 계속 로컬 메모리에 동기화됩니다. 관찰자가 더 이상 필요하지 않은 경우 removeObserverWithHandle 메소드에 해당 FIRDatabaseHandle을 전달하여 삭제하세요.

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

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

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

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

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

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

Swift

ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
  if var post = currentData.value as? [String : AnyObject], let uid = Auth.auth().currentUser?.uid {
    var stars: Dictionary<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

[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입니다. 서버는 초기 값과 현재 값을 비교하여 값이 일치하면 트랜잭션을 수락하고, 그렇지 않으면 거부합니다. 트랜잭션이 거부되면 서버에서 현재 값을 클라이언트에 반환하며, 클라이언트는 업데이트된 값으로 트랜잭션을 다시 실행합니다. 트랜잭션이 수락되거나 시도가 일정 횟수를 초과할 때까지 이 과정이 반복됩니다.

오프라인으로 데이터 쓰기

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

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

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

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

다음 단계