Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기

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

(선택사항) Firebase 로컬 에뮬레이터 제품군으로 프로토타입 및 테스트

앱이 실시간 데이터베이스에서 읽고 쓰는 방법에 대해 이야기하기 전에 실시간 데이터베이스 기능을 프로토타이핑하고 테스트하는 데 사용할 수 있는 도구 모음인 Firebase 로컬 에뮬레이터 제품군을 소개하겠습니다. 다양한 데이터 모델을 시도하거나 보안 규칙을 최적화하거나 백엔드와 상호 작용하는 가장 비용 효율적인 방법을 찾기 위해 노력하는 경우 라이브 서비스를 배포하지 않고 로컬에서 작업할 수 있는 것이 좋은 아이디어가 될 수 있습니다.

실시간 데이터베이스 에뮬레이터는 로컬 에뮬레이터 제품군의 일부로, 앱이 에뮬레이트된 데이터베이스 콘텐츠 및 구성은 물론 선택적으로 에뮬레이트된 프로젝트 리소스(함수, 기타 데이터베이스 및 보안 규칙)와 상호 작용할 수 있도록 합니다.

실시간 데이터베이스 에뮬레이터를 사용하려면 다음 몇 단계만 수행하면 됩니다.

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

실시간 데이터베이스 및 Cloud Functions와 관련된 자세한 연습을 사용할 수 있습니다. Local Emulator Suite 소개 도 살펴봐야 합니다.

FIRDatabaseReference 가져오기

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

빠른

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

ref = Database.database().reference()

오브젝티브-C

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

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

데이터 쓰기

이 문서에서는 Firebase 데이터 읽기 및 쓰기의 기본 사항을 다룹니다.

Firebase 데이터는 Database 참조에 기록되고 참조에 비동기 리스너를 연결하여 검색합니다. 리스너는 데이터의 초기 상태에 대해 한 번 트리거되고 데이터가 변경될 때마다 다시 트리거됩니다.

기본 쓰기 작업

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

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

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

빠른

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

오브젝티브-C

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

이러한 방식으로 setValue 를 사용하면 자식 노드를 포함하여 지정된 위치의 데이터를 덮어씁니다. 그러나 전체 개체를 다시 작성하지 않고도 자식을 업데이트할 수 있습니다. 사용자가 프로필을 업데이트하도록 허용하려면 다음과 같이 사용자 이름을 업데이트할 수 있습니다.

빠른

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

오브젝티브-C

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

데이터 읽기

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

경로에서 데이터를 읽고 변경 사항을 수신하려면 FIRDatabaseReference의 FIRDatabaseReference observeEventType:withBlock 을 사용하여 FIRDataEventTypeValue 이벤트를 관찰하십시오.

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

FIRDataEventTypeValue 이벤트를 사용하여 이벤트가 발생했을 때 데이터가 있는 것처럼 주어진 경로에서 데이터를 읽을 수 있습니다. 이 메서드는 리스너가 연결될 때 한 번 트리거되고 자식을 포함한 데이터가 변경될 때마다 다시 트리거됩니다. 이벤트 콜백에는 하위 데이터를 포함하여 해당 위치의 모든 데이터가 포함된 snapshot 이 전달됩니다. 데이터가 없는 경우 스냅샷은 exist exists() 를 호출할 때 false 를 반환하고 value 속성을 읽을 때 nil 을 반환합니다.

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

빠른

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

오브젝티브-C

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

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

데이터를 한 번 읽기

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

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

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

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

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

빠른

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
ref.child("users/\(uid)/username").getData(completion:  { error, snapshot in
  guard error == nil else {
    print(error!.localizedDescription)
    return;
  }
  let userName = snapshot.value as? String ?? "Unknown";
});

오브젝티브-C

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
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 을 사용하여 로컬 디스크 캐시에서 즉시 데이터를 가져올 수 있습니다.

이것은 한 번만 로드하면 되며 자주 변경되지 않거나 적극적인 청취가 필요하지 않은 데이터에 유용합니다. 예를 들어, 이전 예제의 블로깅 앱은 사용자가 새 게시물 작성을 시작할 때 이 메서드를 사용하여 사용자 프로필을 로드합니다.

빠른

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
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)
}

오브젝티브-C

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
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 ​​를 호출할 때 키의 경로를 지정하여 하위 수준의 하위 값을 업데이트할 수 있습니다. 데이터가 더 나은 확장성을 위해 여러 위치에 저장되어 있는 경우 데이터 팬아웃 을 사용하여 해당 데이터의 모든 인스턴스를 업데이트할 수 있습니다. 예를 들어, 소셜 블로깅 앱은 게시물을 만들고 동시에 최근 활동 피드와 게시 사용자의 활동 피드로 업데이트하려고 할 수 있습니다. 이를 위해 블로깅 애플리케이션은 다음과 같은 코드를 사용합니다.

빠른

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
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)

오브젝티브-C

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
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 는 모두 쓰기가 데이터베이스에 커밋될 때 호출되는 선택적 완료 블록을 사용합니다. 이 리스너는 저장된 데이터와 아직 동기화 중인 데이터를 추적하는 데 유용할 수 있습니다. 호출이 실패하면 실패가 발생한 이유를 나타내는 오류 개체가 리스너에 전달됩니다.

빠른

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
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!")
  }
}

오브젝티브-C

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
[[[_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 를 호출해도 자식 노드에 등록된 리스너가 자동으로 제거되지 않습니다. 또한 제거하려면 해당 참조 또는 핸들을 추적해야 합니다.

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

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

예를 들어 소셜 블로깅 앱의 예에서 다음과 같이 사용자가 게시물에 별표를 표시하거나 별표 표시를 해제하도록 허용하고 게시물이 얼마나 많은 별을 받았는지 추적할 수 있습니다.

빠른

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
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)
  }
}

오브젝티브-C

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
[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와 증가된 별 수입니다. 사용자가 게시물에 별표를 표시하고 있다는 것을 이미 알고 있다면 트랜잭션 대신 원자적 증가 연산을 사용할 수 있습니다.

빠른

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
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);

오브젝티브-C

참고: 이 Firebase 제품은 App Clip 대상에서 사용할 수 없습니다.
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 클라이언트는 해당 데이터를 "최선의" 방식으로 원격 데이터베이스 서버 및 다른 클라이언트와 동기화합니다.

결과적으로 데이터베이스에 대한 모든 쓰기는 데이터가 서버에 쓰기 전에 즉시 로컬 이벤트를 트리거합니다. 즉, 네트워크 대기 시간이나 연결에 관계없이 앱이 응답하는 상태를 유지합니다.

연결이 다시 설정되면 사용자 지정 코드를 작성할 필요 없이 클라이언트가 현재 서버 상태와 동기화할 수 있도록 앱이 적절한 이벤트 집합을 수신합니다.

온라인 및 오프라인 기능에 대해 자세히 알아보기 에서 오프라인 동작에 대해 자세히 설명합니다.

다음 단계