Cloud Functions로 실시간 데이터베이스 확장

Cloud Functions를 사용하면 클라이언트 코드를 업데이트하지 않고도 Firebase 실시간 데이터베이스의 이벤트를 처리할 수 있습니다. Cloud Functions를 사용하면 전체 관리자 권한으로 데이터베이스 작업을 실행할 수 있으며 데이터베이스의 각 변경사항이 개별적으로 처리됩니다. DataSnapshot 또는 Admin SDK를 통해 Firebase 실시간 데이터베이스를 변경할 수 있습니다.

일반적인 처리 과정에서 Firebase 실시간 데이터베이스 함수는 다음을 수행합니다.

  1. 특정 데이터베이스 위치가 변경되기를 기다립니다.
  2. 이벤트가 발생할 때 트리거되어 작업을 수행합니다(사용 사례의 예시를 더 보려면 Cloud Functions로 무엇을 할 수 있나요?를 참조하세요).
  3. 지정된 문서에 저장된 데이터의 스냅샷을 포함하는 데이터 객체를 수신합니다.

데이터베이스 함수 트리거

functions.database를 사용하여 실시간 데이터베이스 이벤트의 새 함수를 만듭니다. 함수가 트리거되는 시점을 제어하려면 이벤트 핸들러 중 하나를 지정하고 이벤트를 수신 대기할 데이터베이스 경로를 지정합니다.

이벤트 핸들러 설정

함수를 통해 데이터베이스 이벤트를 처리할 수 있으며, 얼마나 세부적으로 처리할 지에 따라 2가지 수준으로 나눠집니다. 즉, 생성, 업데이트 또는 삭제 이벤트만 수신 대기하거나 모든 유형의 경로 변경을 수신 대기할 수 있습니다. Cloud Functions는 실시간 데이터베이스에 대한 다음 이벤트 핸들러를 지원합니다.

  • onWrite() - 실시간 데이터베이스에서 데이터가 생성, 업데이트 또는 삭제될 때 트리거됩니다.
  • onCreate() - 실시간 데이터베이스에서 새 데이터가 생성될 때 트리거됩니다.
  • onUpdate() - 실시간 데이터베이스에서 데이터가 업데이트될 때 트리거됩니다.
  • onDelete() - 실시간 데이터베이스에서 데이터가 삭제될 때 트리거됩니다.

데이터베이스 인스턴스 및 경로 지정

함수가 트리거되는 시점과 위치를 제어하려면 ref(path)를 호출하여 경로를 지정하고 필요한 경우 instance('INSTANCE_NAME')로 데이터베이스 인스턴스를 지정합니다. 인스턴스를 지정하지 않으면 Firebase 프로젝트의 기본 데이터베이스 인스턴스에 함수가 배포됩니다. 예를 들면 다음과 같습니다.

  • 기본 데이터베이스 인스턴스: functions.database.ref('/foo/bar')
  • 이름이 'my-app-db-2'인 인스턴스: functions.database.instance('my-app-db-2').ref('/foo/bar')

이러한 메서드는 함수가 데이터베이스 인스턴스 내의 특정 경로에서 쓰기를 처리하도록 지시합니다. 경로를 지정하면 해당 경로와 모든 하위 경로에 영향을 주는 모든 쓰기 이벤트와 일치됩니다. 함수의 경로를 /foo/bar로 설정하면 다음 두 위치 모두의 이벤트와 일치합니다.

 /foo/bar
 /foo/bar/baz/really/deep/path

두 경우 모두 /foo/bar에서 이벤트가 발생한 것으로 해석되고, 이벤트 데이터에 의 이전 데이터와 새 데이터가 포함됩니다. 이벤트 데이터가 큰 경우에는 데이터베이스 루트 근처에 단일 함수를 사용하는 대신 더 깊은 경로에 여러 함수를 사용하는 것이 좋습니다. 성능을 최적화하려면 최대한 깊은 수준의 데이터만 요청합니다.

경로 구성요소를 중괄호로 묶어 와일드 카드로 지정할 수 있습니다. ref('foo/{bar}')/foo의 모든 하위 경로와 일치합니다. 함수의 EventContext.params 객체에서 이 와일드 카드 경로 구성요소의 값을 확인할 수 있습니다. 이 예시에서는 값이 event.params.bar로 제공됩니다.

와일드 카드가 포함된 경로는 단일 쓰기 작업의 여러 이벤트와 일치할 수 있습니다. 다음을 삽입합니다.

{
  "foo": {
    "hello": "world",
    "firebase": "functions"
  }
}

"hello": "world", "firebase": "functions"에 대해 한 번씩 "/foo/{bar}" 경로와 두 번 일치하게 됩니다.

이벤트 데이터 처리

실시간 데이터베이스 이벤트를 처리할 때 반환되는 데이터 객체는 DataSnapshot입니다. onWrite 또는 onUpdate 이벤트의 경우 첫 번째 매개변수는 각각 트리거 이벤트 전후의 데이터 상태를 나타내는 스냅샷 2개가 포함된 Change 객체입니다. onCreateonDelete 이벤트의 경우 반환되는 데이터 객체는 생성하거나 삭제한 데이터의 스냅샷입니다.

이 예시에서 함수는 지정된 경로의 스냅샷을 snap으로 가져와 해당 위치의 문자열을 대문자로 변환하고 수정된 문자열을 데이터베이스에 작성합니다.

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snapshot, context) => {
      // Grab the current value of what was written to the Realtime Database.
      const original = snapshot.val();
      console.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      return snapshot.ref.parent.child('uppercase').set(uppercase);
    });

사용자 인증 정보 액세스

EventContext.authEventContext.authType에서 함수를 트리거한 사용자의 권한을 비롯하여 사용자 정보에 액세스할 수 있습니다. 이 방법은 보안 규칙을 적용하는 데 유용하며 함수는 사용자의 권한 수준에 따라 다른 작업을 완료할 수 있습니다.

const functions = require('firebase-functions');
const admin = require('firebase-admin');

exports.simpleDbFunction = functions.database.ref('/path')
    .onCreate((snap, context) => {
      if (context.authType === 'ADMIN') {
        // do something
      } else if (context.authType === 'USER') {
        console.log(snap.val(), 'written by', context.auth.uid);
      }
    });

또한 사용자 인증 정보를 활용하여 사용자로 '가장'하고 사용자 대신 쓰기 작업을 수행할 수 있습니다. 동시 실행 문제를 방지하기 위해 아래에 나와 있는 것처럼 앱 인스턴스를 삭제해야 합니다.

exports.impersonateMakeUpperCase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snap, context) => {
      const appOptions = JSON.parse(process.env.FIREBASE_CONFIG);
      appOptions.databaseAuthVariableOverride = context.auth;
      const app = admin.initializeApp(appOptions, 'app');
      const uppercase = snap.val().toUpperCase();
      const ref = snap.ref.parent.child('uppercase');

      const deleteApp = () => app.delete().catch(() => null);

      return app.database().ref(ref).set(uppercase).then(res => {
        // Deleting the app is necessary for preventing concurrency leaks
        return deleteApp().then(() => res);
      }).catch(err => {
        return deleteApp().then(() => Promise.reject(err));
      });
    });

이전 값 읽기

Change 객체의 before 속성을 사용하면 이벤트가 발생하기 전에 데이터베이스에 저장된 데이터를 검사할 수 있습니다. before 속성은 DataSnapshot을 반환합니다. 여기서 모든 메서드(예: val()exists())는 이전 값을 참조합니다. 다시 새 값을 읽으려면 원본 DataSnapshot을 사용하거나 after 속성을 읽으면 됩니다. Change에 있는 이 속성은 이벤트가 발생한 데이터 상태를 나타내는 또 다른 DataSnapshot입니다.

예를 들어 데이터를 처음 만들 때만 함수가 텍스트를 대문자로 변환하도록 before 속성을 사용할 수 있습니다.

exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onWrite((change, context) => {
      // Only edit data when it is first created.
      if (change.before.exists()) {
        return null;
      }
      // Exit when the data is deleted.
      if (!change.after.exists()) {
        return null;
      }
      // Grab the current value of what was written to the Realtime Database.
      const original = change.after.val();
      console.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      return change.after.ref.parent.child('uppercase').set(uppercase);
    });