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

데이터 저장

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

이 문서에서는 Firebase 실시간 데이터베이스에 데이터를 쓰는 네 가지 방법인 설정, 업데이트, 푸시 및 트랜잭션 지원을 다룹니다.

데이터를 저장하는 방법

세트 messages/users/<username> 과 같이 정의된 경로 에 데이터를 쓰거나 바꿉니다.
업데이트 모든 데이터를 바꾸지 않고 정의된 경로에 대한 일부 키 업데이트
푸시 데이터베이스 의 데이터 목록에 추가 합니다. 새 노드를 목록에 푸시할 때마다 데이터베이스는 messages/users/<unique-user-id>/<username> 과 같은 고유 키를 생성합니다.
거래 동시 업데이트로 인해 손상될 수 있는 복잡한 데이터로 작업할 때 트랜잭션 사용

데이터 저장

기본 데이터베이스 쓰기 작업은 지정된 데이터베이스 참조에 새 데이터를 저장하고 해당 경로의 기존 데이터를 교체하는 세트입니다. 집합을 이해하기 위해 간단한 블로깅 앱을 빌드합니다. 앱 데이터는 다음 데이터베이스 참조에 저장됩니다.

자바
final FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("server/saving-data/fireblog");
노드.js
// Import Admin SDK
const { getDatabase } = require('firebase-admin/database');

// Get a database reference to our blog
const db = getDatabase();
const ref = db.ref('server/saving-data/fireblog');
파이썬
# Import database module.
from firebase_admin import db

# Get a database reference to our blog.
ref = db.reference('server/saving-data/fireblog')
가다
// Create a database client from App.
client, err := app.Database(ctx)
if err != nil {
	log.Fatalln("Error initializing database client:", err)
}

// Get a database reference to our blog.
ref := client.NewRef("server/saving-data/fireblog")

일부 사용자 데이터를 저장하는 것으로 시작하겠습니다. 각 사용자를 고유한 사용자 이름으로 저장하고 전체 이름과 생년월일도 저장합니다. 각 사용자는 고유한 사용자 이름을 가지므로 키가 이미 있고 키를 만들 필요가 없으므로 여기에서 push 메서드 대신 set 메서드를 사용하는 것이 좋습니다.

먼저 사용자 데이터에 대한 데이터베이스 참조를 만듭니다. 그런 다음 set() / setValue() 를 사용하여 사용자 이름, 전체 이름 및 생일과 함께 사용자 개체를 데이터베이스에 저장합니다. 문자열, 숫자, 부울, null , 배열 또는 모든 JSON 개체를 전달할 수 있습니다. null 을 전달하면 지정된 위치의 데이터가 제거됩니다. 이 경우 객체를 전달합니다.

자바
public static class User {

  public String date_of_birth;
  public String full_name;
  public String nickname;

  public User(String dateOfBirth, String fullName) {
    // ...
  }

  public User(String dateOfBirth, String fullName, String nickname) {
    // ...
  }

}

DatabaseReference usersRef = ref.child("users");

Map<String, User> users = new HashMap<>();
users.put("alanisawesome", new User("June 23, 1912", "Alan Turing"));
users.put("gracehop", new User("December 9, 1906", "Grace Hopper"));

usersRef.setValueAsync(users);
노드.js
const usersRef = ref.child('users');
usersRef.set({
  alanisawesome: {
    date_of_birth: 'June 23, 1912',
    full_name: 'Alan Turing'
  },
  gracehop: {
    date_of_birth: 'December 9, 1906',
    full_name: 'Grace Hopper'
  }
});
파이썬
users_ref = ref.child('users')
users_ref.set({
    'alanisawesome': {
        'date_of_birth': 'June 23, 1912',
        'full_name': 'Alan Turing'
    },
    'gracehop': {
        'date_of_birth': 'December 9, 1906',
        'full_name': 'Grace Hopper'
    }
})
가다

// User is a json-serializable type.
type User struct {
	DateOfBirth string `json:"date_of_birth,omitempty"`
	FullName    string `json:"full_name,omitempty"`
	Nickname    string `json:"nickname,omitempty"`
}

usersRef := ref.Child("users")
err := usersRef.Set(ctx, map[string]*User{
	"alanisawesome": {
		DateOfBirth: "June 23, 1912",
		FullName:    "Alan Turing",
	},
	"gracehop": {
		DateOfBirth: "December 9, 1906",
		FullName:    "Grace Hopper",
	},
})
if err != nil {
	log.Fatalln("Error setting value:", err)
}

JSON 개체가 데이터베이스에 저장되면 개체 속성이 중첩 방식으로 데이터베이스 하위 위치에 자동으로 매핑됩니다. 이제 URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name 으로 이동하면 "Alan Turing" 값이 표시됩니다. 데이터를 하위 위치에 직접 저장할 수도 있습니다.

자바
usersRef.child("alanisawesome").setValueAsync(new User("June 23, 1912", "Alan Turing"));
usersRef.child("gracehop").setValueAsync(new User("December 9, 1906", "Grace Hopper"));
노드.js
const usersRef = ref.child('users');
usersRef.child('alanisawesome').set({
  date_of_birth: 'June 23, 1912',
  full_name: 'Alan Turing'
});
usersRef.child('gracehop').set({
  date_of_birth: 'December 9, 1906',
  full_name: 'Grace Hopper'
});
파이썬
users_ref.child('alanisawesome').set({
    'date_of_birth': 'June 23, 1912',
    'full_name': 'Alan Turing'
})
users_ref.child('gracehop').set({
    'date_of_birth': 'December 9, 1906',
    'full_name': 'Grace Hopper'
})
가다
if err := usersRef.Child("alanisawesome").Set(ctx, &User{
	DateOfBirth: "June 23, 1912",
	FullName:    "Alan Turing",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

if err := usersRef.Child("gracehop").Set(ctx, &User{
	DateOfBirth: "December 9, 1906",
	FullName:    "Grace Hopper",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

위의 두 가지 예(두 값을 동시에 객체로 작성하고 이를 하위 위치에 별도로 작성)는 동일한 데이터가 데이터베이스에 저장되는 결과를 낳습니다.

{
  "users": {
    "alanisawesome": {
      "date_of_birth": "June 23, 1912",
      "full_name": "Alan Turing"
    },
    "gracehop": {
      "date_of_birth": "December 9, 1906",
      "full_name": "Grace Hopper"
    }
  }
}

첫 번째 예는 데이터를 보고 있는 클라이언트에서 하나의 이벤트만 트리거하는 반면, 두 번째 예는 두 개의 이벤트를 트리거합니다. 데이터가 usersRef 에 이미 존재하는 경우 첫 번째 방법은 덮어쓰지만 두 번째 방법은 각 개별 자식 노드의 값만 수정하고 usersRef 의 다른 자식은 변경하지 않는다는 점에 유의하는 것이 중요합니다.

저장된 데이터 업데이트

다른 자식 노드를 덮어쓰지 않고 동시에 데이터베이스 위치의 여러 자식에 쓰려면 다음과 같이 업데이트 방법을 사용할 수 있습니다.

자바
DatabaseReference hopperRef = usersRef.child("gracehop");
Map<String, Object> hopperUpdates = new HashMap<>();
hopperUpdates.put("nickname", "Amazing Grace");

hopperRef.updateChildrenAsync(hopperUpdates);
노드.js
const usersRef = ref.child('users');
const hopperRef = usersRef.child('gracehop');
hopperRef.update({
  'nickname': 'Amazing Grace'
});
파이썬
hopper_ref = users_ref.child('gracehop')
hopper_ref.update({
    'nickname': 'Amazing Grace'
})
가다
hopperRef := usersRef.Child("gracehop")
if err := hopperRef.Update(ctx, map[string]interface{}{
	"nickname": "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating child:", err)
}

그러면 닉네임을 포함하도록 Grace의 데이터가 업데이트됩니다. update 대신 set here을 사용했다면 hopperRef 에서 full_namedate_of_birth 를 모두 삭제했을 것입니다.

Firebase 실시간 데이터베이스는 다중 경로 업데이트도 지원합니다. 이는 업데이트가 이제 데이터베이스의 여러 위치에서 동시에 값을 업데이트할 수 있음을 의미합니다. 이는 데이터를 비정규화 하는 데 도움이 되는 강력한 기능입니다. 다중 경로 업데이트를 사용하여 Grace와 Alan에게 동시에 닉네임을 추가할 수 있습니다.

자바
Map<String, Object> userUpdates = new HashMap<>();
userUpdates.put("alanisawesome/nickname", "Alan The Machine");
userUpdates.put("gracehop/nickname", "Amazing Grace");

usersRef.updateChildrenAsync(userUpdates);
노드.js
const usersRef = ref.child('users');
usersRef.update({
  'alanisawesome/nickname': 'Alan The Machine',
  'gracehop/nickname': 'Amazing Grace'
});
파이썬
users_ref.update({
    'alanisawesome/nickname': 'Alan The Machine',
    'gracehop/nickname': 'Amazing Grace'
})
가다
if err := usersRef.Update(ctx, map[string]interface{}{
	"alanisawesome/nickname": "Alan The Machine",
	"gracehop/nickname":      "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating children:", err)
}

이 업데이트 이후 Alan과 Grace 모두 닉네임이 추가되었습니다.

{
  "users": {
    "alanisawesome": {
      "date_of_birth": "June 23, 1912",
      "full_name": "Alan Turing",
      "nickname": "Alan The Machine"
    },
    "gracehop": {
      "date_of_birth": "December 9, 1906",
      "full_name": "Grace Hopper",
      "nickname": "Amazing Grace"
    }
  }
}

경로가 포함된 개체를 작성하여 개체를 업데이트하려고 하면 다른 동작이 발생합니다. 대신 Grace와 Alan을 다음과 같이 업데이트하려고 하면 어떻게 되는지 살펴보겠습니다.

자바
Map<String, Object> userNicknameUpdates = new HashMap<>();
userNicknameUpdates.put("alanisawesome", new User(null, null, "Alan The Machine"));
userNicknameUpdates.put("gracehop", new User(null, null, "Amazing Grace"));

usersRef.updateChildrenAsync(userNicknameUpdates);
노드.js
const usersRef = ref.child('users');
usersRef.update({
  'alanisawesome': {
    'nickname': 'Alan The Machine'
  },
  'gracehop': {
    'nickname': 'Amazing Grace'
  }
});
파이썬
users_ref.update({
    'alanisawesome': {
        'nickname': 'Alan The Machine'
    },
    'gracehop': {
        'nickname': 'Amazing Grace'
    }
})
가다
if err := usersRef.Update(ctx, map[string]interface{}{
	"alanisawesome": &User{Nickname: "Alan The Machine"},
	"gracehop":      &User{Nickname: "Amazing Grace"},
}); err != nil {
	log.Fatalln("Error updating children:", err)
}

이로 인해 전체 /users 노드를 덮어쓰는 것과 같이 다른 동작이 발생합니다.

{
  "users": {
    "alanisawesome": {
      "nickname": "Alan The Machine"
    },
    "gracehop": {
      "nickname": "Amazing Grace"
    }
  }
}

완료 콜백 추가

Node.js 및 Java Admin SDK에서 데이터가 커밋된 시기를 알고 싶다면 완료 콜백을 추가할 수 있습니다. 이러한 SDK의 set 및 update 메서드는 모두 쓰기가 데이터베이스에 커밋될 때 호출되는 선택적 완료 콜백을 사용합니다. 어떤 이유로 호출이 실패하면 실패가 발생한 이유를 나타내는 오류 개체가 콜백에 전달됩니다. Python 및 Go Admin SDK에서는 모든 쓰기 메서드가 차단됩니다. 즉, 쓰기 메서드는 쓰기가 데이터베이스에 커밋될 때까지 반환되지 않습니다.

자바
DatabaseReference dataRef = ref.child("data");
dataRef.setValue("I'm writing data", new DatabaseReference.CompletionListener() {
  @Override
  public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
    if (databaseError != null) {
      System.out.println("Data could not be saved " + databaseError.getMessage());
    } else {
      System.out.println("Data saved successfully.");
    }
  }
});
노드.js
dataRef.set('I\'m writing data', (error) => {
  if (error) {
    console.log('Data could not be saved.' + error);
  } else {
    console.log('Data saved successfully.');
  }
});

데이터 목록 저장

데이터 목록을 만들 때 대부분의 응용 프로그램의 다중 사용자 특성을 염두에 두고 그에 따라 목록 구조를 조정하는 것이 중요합니다. 위의 예를 확장하여 앱에 블로그 게시물을 추가해 보겠습니다. 첫 번째 본능은 다음과 같이 자동 증가 정수 인덱스를 사용하여 자식을 저장하는 데 set을 사용하는 것입니다.

// NOT RECOMMENDED - use push() instead!
{
  "posts": {
    "0": {
      "author": "gracehop",
      "title": "Announcing COBOL, a New Programming Language"
    },
    "1": {
      "author": "alanisawesome",
      "title": "The Turing Machine"
    }
  }
}

사용자가 새 게시물을 추가하면 /posts/2 로 저장됩니다. 이는 한 명의 작성자만 게시물을 추가하는 경우 작동하지만 협업 블로깅 애플리케이션에서는 많은 사용자가 동시에 게시물을 추가할 수 있습니다. 두 명의 작성자가 /posts/2 에 동시에 글을 쓰는 경우 해당 게시물 중 하나가 다른 작성자에 의해 삭제됩니다.

이 문제를 해결하기 위해 Firebase 클라이언트는 각각의 새 자식에 대해 고유한 를 생성하는 push() 함수를 제공합니다 . 고유한 자식 키를 사용하여 여러 클라이언트가 쓰기 충돌에 대한 걱정 없이 동시에 같은 위치에 자식을 추가할 수 있습니다.

자바
public static class Post {

  public String author;
  public String title;

  public Post(String author, String title) {
    // ...
  }

}

DatabaseReference postsRef = ref.child("posts");

DatabaseReference newPostRef = postsRef.push();
newPostRef.setValueAsync(new Post("gracehop", "Announcing COBOL, a New Programming Language"));

// We can also chain the two calls together
postsRef.push().setValueAsync(new Post("alanisawesome", "The Turing Machine"));
노드.js
const newPostRef = postsRef.push();
newPostRef.set({
  author: 'gracehop',
  title: 'Announcing COBOL, a New Programming Language'
});

// we can also chain the two calls together
postsRef.push().set({
  author: 'alanisawesome',
  title: 'The Turing Machine'
});
파이썬
posts_ref = ref.child('posts')

new_post_ref = posts_ref.push()
new_post_ref.set({
    'author': 'gracehop',
    'title': 'Announcing COBOL, a New Programming Language'
})

# We can also chain the two calls together
posts_ref.push().set({
    'author': 'alanisawesome',
    'title': 'The Turing Machine'
})
가다

// Post is a json-serializable type.
type Post struct {
	Author string `json:"author,omitempty"`
	Title  string `json:"title,omitempty"`
}

postsRef := ref.Child("posts")

newPostRef, err := postsRef.Push(ctx, nil)
if err != nil {
	log.Fatalln("Error pushing child node:", err)
}

if err := newPostRef.Set(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

// We can also chain the two calls together
if _, err := postsRef.Push(ctx, &Post{
	Author: "alanisawesome",
	Title:  "The Turing Machine",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

고유 키는 타임스탬프를 기반으로 하므로 목록 항목은 자동으로 시간순으로 정렬됩니다. Firebase는 각 블로그 게시물에 대해 고유한 키를 생성하므로 여러 사용자가 동시에 게시물을 추가해도 쓰기 충돌이 발생하지 않습니다. 이제 데이터베이스 데이터는 다음과 같습니다.

{
  "posts": {
    "-JRHTHaIs-jNPLXOQivY": {
      "author": "gracehop",
      "title": "Announcing COBOL, a New Programming Language"
    },
    "-JRHTHaKuITFIhnj02kE": {
      "author": "alanisawesome",
      "title": "The Turing Machine"
    }
  }
}

JavaScript, Python 및 Go에서는 push() 를 호출한 다음 즉시 set() 을 호출하는 패턴이 매우 일반적이므로 Firebase SDK에서는 다음과 같이 설정할 데이터를 push() 에 직접 전달하여 이들을 결합할 수 있습니다.

자바
// No Java equivalent
노드.js
// This is equivalent to the calls to push().set(...) above
postsRef.push({
  author: 'gracehop',
  title: 'Announcing COBOL, a New Programming Language'
});;
파이썬
# This is equivalent to the calls to push().set(...) above
posts_ref.push({
    'author': 'gracehop',
    'title': 'Announcing COBOL, a New Programming Language'
})
가다
if _, err := postsRef.Push(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

push()에 의해 생성된 고유 키 가져오기

push() 를 호출하면 키를 가져오거나 데이터를 설정하는 데 사용할 수 있는 새 데이터 경로에 대한 참조가 반환됩니다. 다음 코드는 위의 예와 동일한 데이터를 생성하지만 이제 생성된 고유 키에 액세스할 수 있습니다.

자바
// Generate a reference to a new location and add some data using push()
DatabaseReference pushedPostRef = postsRef.push();

// Get the unique ID generated by a push()
String postId = pushedPostRef.getKey();
노드.js
// Generate a reference to a new location and add some data using push()
const newPostRef = postsRef.push();

// Get the unique key generated by push()
const postId = newPostRef.key;
파이썬
# Generate a reference to a new location and add some data using push()
new_post_ref = posts_ref.push()

# Get the unique key generated by push()
post_id = new_post_ref.key
가다
// Generate a reference to a new location and add some data using Push()
newPostRef, err := postsRef.Push(ctx, nil)
if err != nil {
	log.Fatalln("Error pushing child node:", err)
}

// Get the unique key generated by Push()
postID := newPostRef.Key

보시다시피 push() 참조에서 고유 키 값을 가져올 수 있습니다.

데이터 검색 에 대한 다음 섹션에서는 Firebase 데이터베이스에서 이 데이터를 읽는 방법을 배웁니다.

트랜잭션 데이터 저장

증분 카운터와 같이 동시 수정으로 인해 손상될 수 있는 복잡한 데이터로 작업할 때 SDK는 트랜잭션 작업을 제공합니다.

Java 및 Node.js에서는 트랜잭션 작업에 두 개의 콜백(업데이트 기능 및 선택적 완료 콜백)을 제공합니다. Python 및 Go에서는 트랜잭션 작업이 차단되므로 업데이트 기능만 허용합니다.

업데이트 함수는 데이터의 현재 상태를 인수로 취하고 작성하려는 새로운 원하는 상태를 반환해야 합니다. 예를 들어 특정 블로그 게시물에 대한 찬성 수를 늘리려면 다음과 같은 트랜잭션을 작성합니다.

자바
DatabaseReference upvotesRef = ref.child("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes");
upvotesRef.runTransaction(new Transaction.Handler() {
  @Override
  public Transaction.Result doTransaction(MutableData mutableData) {
    Integer currentValue = mutableData.getValue(Integer.class);
    if (currentValue == null) {
      mutableData.setValue(1);
    } else {
      mutableData.setValue(currentValue + 1);
    }

    return Transaction.success(mutableData);
  }

  @Override
  public void onComplete(
      DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) {
    System.out.println("Transaction completed");
  }
});
노드.js
const upvotesRef = db.ref('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes');
upvotesRef.transaction((current_value) => {
  return (current_value || 0) + 1;
});
파이썬
def increment_votes(current_value):
    return current_value + 1 if current_value else 1

upvotes_ref = db.reference('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes')
try:
    new_vote_count = upvotes_ref.transaction(increment_votes)
    print('Transaction completed')
except db.TransactionAbortedError:
    print('Transaction failed to commit')
가다
fn := func(t db.TransactionNode) (interface{}, error) {
	var currentValue int
	if err := t.Unmarshal(&currentValue); err != nil {
		return nil, err
	}
	return currentValue + 1, nil
}

ref := client.NewRef("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes")
if err := ref.Transaction(ctx, fn); err != nil {
	log.Fatalln("Transaction failed to commit:", err)
}

위의 예는 카운터가 null 인지 또는 아직 증가되지 않았는지 확인합니다. 기본값이 작성되지 않은 경우 null 로 트랜잭션을 호출할 수 있기 때문입니다.

위의 코드가 트랜잭션 기능 없이 실행되었고 두 클라이언트가 동시에 이를 증가시키려고 시도했다면 둘 다 새 값으로 1 을 쓰게 되어 결과적으로 2가 아닌 1개의 증가가 발생합니다.

네트워크 연결 및 오프라인 쓰기

Firebase Node.js 및 Java 클라이언트는 활성 데이터의 자체 내부 버전을 유지 관리합니다. 데이터가 기록되면 이 로컬 버전에 먼저 기록됩니다. 그런 다음 클라이언트는 '최선의 노력'을 기준으로 해당 데이터를 데이터베이스 및 다른 클라이언트와 동기화합니다.

결과적으로 데이터베이스에 대한 모든 쓰기는 데이터가 데이터베이스에 쓰기도 전에 즉시 로컬 이벤트를 트리거합니다. 즉, Firebase를 사용하여 애플리케이션을 작성할 때 네트워크 대기 시간이나 인터넷 연결에 관계없이 앱이 응답하는 상태를 유지합니다.

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

데이터 보안

Firebase 실시간 데이터베이스에는 데이터의 다른 노드에 대한 읽기 및 쓰기 액세스 권한이 있는 사용자를 정의할 수 있는 보안 언어가 있습니다. 자세한 내용은 데이터 보안 에서 읽을 수 있습니다.