앱에서 함수 호출


Cloud Functions for Firebase 클라이언트 SDK를 사용하면 Firebase 앱에서 직접 함수를 호출할 수 있습니다. 이 방식으로 앱에서 함수를 호출하려면 Cloud Functions에서 HTTP 호출 가능 함수를 작성 및 배포한 후 앱에서 함수를 호출하는 클라이언트 로직을 추가합니다.

HTTP 호출 가능 함수는 HTTP 함수와 비슷하지만 동일하지 않다는 점에 주의해야 합니다. HTTP 호출 가능 함수를 사용하려면 플랫폼의 클라이언트 SDK를 백엔드 API와 함께 사용하거나 프로토콜을 구현해야 합니다. 호출 가능 함수와 HTTP 함수의 주요 차이점은 다음과 같습니다.

  • 호출 가능 함수를 사용하면 Firebase Authentication 토큰, FCM 토큰, App Check 토큰(사용 가능한 경우)이 자동으로 요청에 포함됩니다.
  • 트리거가 자동으로 요청 본문을 역직렬화하고 인증 토큰의 유효성을 검사합니다.

Cloud Functions(2세대) 이상용 Firebase SDK는 다음과 같은 Firebase 클라이언트 SDK 최소 버전과 연동되어 HTTPS 호출 가능 함수를 지원합니다.

  • Apple 플랫폼용 Firebase SDK 11.5.0
  • Android 21.1.0용 Firebase SDK
  • Firebase Modular Web SDK v. 9.7.0

지원되지 않는 플랫폼에서 빌드한 앱에 비슷한 기능을 추가하려면 https.onCall의 프로토콜 사양을 참조하세요. 이 가이드의 나머지 부분에서는 Apple 플랫폼, Android, 웹, C++, Unity용 HTTP 호출 가능 함수를 작성, 배포, 호출하는 방법을 안내합니다.

호출 가능 함수 작성 및 배포

이 섹션의 코드 예시는 서버 측 함수로 요청을 보내고 클라이언트 SDK 중 하나를 사용하여 응답을 가져오는 방법을 보여주는 완전한 빠른 시작 샘플을 기반으로 합니다. 시작하려면 필요한 모듈을 가져옵니다.

Node.js

// Dependencies for callable functions.
const {onCall, HttpsError} = require("firebase-functions/v2/https");
const {logger} = require("firebase-functions/v2");

// Dependencies for the addMessage function.
const {getDatabase} = require("firebase-admin/database");
const sanitizer = require("./sanitizer");

Python

# Dependencies for callable functions.
from firebase_functions import https_fn, options

# Dependencies for writing to Realtime Database.
from firebase_admin import db, initialize_app

플랫폼의 요청 핸들러(functions.https.onCall 또는 on_call)를 사용하여 HTTPS 호출 가능 함수를 만듭니다. 이 메서드는 요청 매개변수를 사용합니다.

Node.js

// Saves a message to the Firebase Realtime Database but sanitizes the
// text by removing swearwords.
exports.addmessage = onCall((request) => {
  // ...
});

Python

@https_fn.on_call()
def addmessage(req: https_fn.CallableRequest) -> Any:
    """Saves a message to the Firebase Realtime Database but sanitizes the text
    by removing swear words."""

request 매개변수에는 클라이언트 앱에서 전달된 데이터뿐 아니라 인증 상태 같은 추가 컨텍스트가 포함됩니다. Realtime Database에 문자 메시지를 저장하는 호출 가능 함수의 경우 예를 들어 dataauth의 인증 정보와 함께 메시지 텍스트가 포함될 수 있습니다.

Node.js

// Message text passed from the client.
const text = request.data.text;
// Authentication / user information is automatically added to the request.
const uid = request.auth.uid;
const name = request.auth.token.name || null;
const picture = request.auth.token.picture || null;
const email = request.auth.token.email || null;

Python

# Message text passed from the client.
text = req.data["text"]
# Authentication / user information is automatically added to the request.
uid = req.auth.uid
name = req.auth.token.get("name", "")
picture = req.auth.token.get("picture", "")
email = req.auth.token.get("email", "")

호출 가능 함수의 위치와 호출 중인 클라이언트의 위치 간의 거리로 인해 네트워크 지연 시간이 발생할 수 있습니다. 성능을 최적화하려면 함수 위치를 지정하고(해당하는 경우) 호출 가능 함수의 위치를 클라이언트 측에서 SDK를 초기화할 때 설정한 위치와 맞추세요.

필요한 경우 App Check 증명을 연결하여 백엔드 리소스 악용(예: 결제 사기 또는 피싱)을 방지할 수 있습니다. Cloud FunctionsApp Check 적용 사용 설정을 참고하세요.

결과 반환

데이터를 클라이언트에 다시 전송하려면 JSON으로 인코딩될 수 있는 데이터를 반환합니다. 예를 들어 추가 작업의 결과를 반환하는 방법은 다음과 같습니다.

Node.js

// returning result.
return {
  firstNumber: firstNumber,
  secondNumber: secondNumber,
  operator: "+",
  operationResult: firstNumber + secondNumber,
};

Python

return {
    "firstNumber": first_number,
    "secondNumber": second_number,
    "operator": "+",
    "operationResult": first_number + second_number
}

메시지 텍스트 예시의 정리된 텍스트는 클라이언트와 Realtime Database 모두에 반환됩니다. Node.js에서는 JavaScript 프로미스를 사용하여 비동기식으로 이 작업을 수행할 수 있습니다.

Node.js

// Saving the new message to the Realtime Database.
const sanitizedMessage = sanitizer.sanitizeText(text); // Sanitize message.

return getDatabase().ref("/messages").push({
  text: sanitizedMessage,
  author: {uid, name, picture, email},
}).then(() => {
  logger.info("New Message written");
  // Returning the sanitized message to the client.
  return {text: sanitizedMessage};
})

Python

# Saving the new message to the Realtime Database.
sanitized_message = sanitize_text(text)  # Sanitize message.
db.reference("/messages").push({  # type: ignore
    "text": sanitized_message,
    "author": {
        "uid": uid,
        "name": name,
        "picture": picture,
        "email": email
    }
})
print("New message written")

# Returning the sanitized message to the client.
return {"text": sanitized_message}

교차 출처 리소스 공유(CORS) 구성

cors 옵션을 사용하여 함수에 액세스할 수 있는 출처를 관리하세요.

기본적으로 호출 가능 함수에는 모든 출처의 요청을 허용하도록 CORS가 구성되어 있습니다. 일부 교차 출처 요청만 허용하려면 허용하는 특정 도메인 또는 정규 표현식의 목록을 전달하면 됩니다. 예를 들면 다음과 같습니다.

Node.js

const { onCall } = require("firebase-functions/v2/https");

exports.getGreeting = onCall(
  { cors: [/firebase\.com$/, "https://flutter.com"] },
  (request) => {
    return "Hello, world!";
  }
);

교차 출처 요청을 금지하려면 cors 정책을 false로 설정합니다.

오류 처리

클라이언트에서 유용한 오류 세부정보를 가져오려면 functions.https.HttpsError 또는 https_fn.HttpsError 인스턴스를 발생시키거나 Node.js의 경우 거부된 프로미스를 반환하여 호출 가능 함수에서 오류를 반환합니다. 오류에는 code 속성이 있으며 이 속성은 gRPC 상태 코드에 나열된 값 중 하나일 수 있습니다. 또한 오류에 message 문자열도 있으며 기본값은 비어 있는 문자열입니다. 임의의 값이 있는 details 필드(선택사항)도 있을 수 있습니다. 함수에서 HTTPS 오류 외의 오류가 발생하면 클라이언트에서 INTERNAL 메시지와 internal 코드가 포함된 오류를 수신합니다.

예를 들어 함수에서 오류 메시지와 함께 데이터 유효성 검사 및 인증 오류를 발생시켜 호출 중인 클라이언트에 반환할 수 있습니다.

Node.js

// Checking attribute.
if (!(typeof text === "string") || text.length === 0) {
  // Throwing an HttpsError so that the client gets the error details.
  throw new HttpsError("invalid-argument", "The function must be called " +
          "with one arguments \"text\" containing the message text to add.");
}
// Checking that the user is authenticated.
if (!request.auth) {
  // Throwing an HttpsError so that the client gets the error details.
  throw new HttpsError("failed-precondition", "The function must be " +
          "called while authenticated.");
}

Python

# Checking attribute.
if not isinstance(text, str) or len(text) < 1:
    # Throwing an HttpsError so that the client gets the error details.
    raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
                              message=('The function must be called with one argument, "text",'
                                       " containing the message text to add."))

# Checking that the user is authenticated.
if req.auth is None:
    # Throwing an HttpsError so that the client gets the error details.
    raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.FAILED_PRECONDITION,
                              message="The function must be called while authenticated.")

호출 가능 함수 배포

완성된 호출 가능 함수를 index.js에 저장한 후 firebase deploy를 실행하면 다른 모든 함수와 함께 배포할 수 있습니다. 호출 가능 함수만 배포하려면 다음과 같이 --only 인수를 사용하여 부분 배포를 수행합니다.

firebase deploy --only functions:addMessage

함수를 배포할 때 권한 오류가 발생하면 배포 명령어를 실행하는 사용자에게 적절한 IAM 역할이 할당되었는지 확인합니다.

클라이언트 개발 환경 설정

기본 요건을 충족하는지 확인하고 필요한 종속 항목 및 클라이언트 라이브러리를 앱에 추가합니다.

iOS+

안내에 따라 Firebase를 Apple 앱에 추가합니다.

Swift Package Manager를 사용해 Firebase 종속 항목을 설치하고 관리하세요.

  1. 앱 프로젝트를 연 상태로 Xcode에서 File(파일) > Add Packages(패키지 추가)로 이동합니다.
  2. 메시지가 표시되면 Firebase Apple 플랫폼 SDK 저장소를 추가합니다.
  3.   https://github.com/firebase/firebase-ios-sdk.git
  4. Cloud Functions 라이브러리를 선택합니다.
  5. 타겟 빌드 설정의 Other Linker Flags(기타 링커 플래그) 섹션에 -ObjC 플래그를 추가합니다.
  6. 완료되면 Xcode가 백그라운드에서 자동으로 종속 항목을 확인하고 다운로드하기 시작합니다.

Web

  1. 안내에 따라 Firebase를 웹 앱에 추가합니다. 터미널에서 다음 명령어를 실행해야 합니다.
    npm install firebase@11.0.2 --save
    
  2. Firebase core 및 Cloud Functions를 모두 수동으로 요청합니다.

     import { initializeApp } from 'firebase/app';
     import { getFunctions } from 'firebase/functions';
    
     const app = initializeApp({
         projectId: '### CLOUD FUNCTIONS PROJECT ID ###',
         apiKey: '### FIREBASE API KEY ###',
         authDomain: '### FIREBASE AUTH DOMAIN ###',
       });
     const functions = getFunctions(app);
    

Android

  1. 안내에 따라 Firebase를 Android 앱에 추가합니다.

  2. 모듈(앱 수준) Gradle 파일(일반적으로 <project>/<app-module>/build.gradle.kts 또는 <project>/<app-module>/build.gradle)에서 Android용 Cloud Functions 라이브러리의 종속 항목을 추가합니다. 라이브러리 버전 관리 제어에는 Firebase Android BoM을 사용하는 것이 좋습니다.

    dependencies {
        // Import the BoM for the Firebase platform
        implementation(platform("com.google.firebase:firebase-bom:33.6.0"))
    
        // Add the dependency for the Cloud Functions library
        // When using the BoM, you don't specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-functions")
    }
    

    Firebase Android BoM을 사용하면 앱에서 항상 호환되는 Firebase Android 라이브러리 버전만 사용합니다.

    (대안) BoM을 사용하지 않고 Firebase 라이브러리 종속 항목을 추가합니다.

    Firebase BoM을 사용하지 않도록 선택한 경우에는 종속 항목 줄에 각 Firebase 라이브러리 버전을 지정해야 합니다.

    앱에서 여러 Firebase 라이브러리를 사용하는 경우 모든 버전이 호환되도록 BoM을 사용하여 라이브러리 버전을 관리하는 것이 좋습니다.

    dependencies {
        // Add the dependency for the Cloud Functions library
        // When NOT using the BoM, you must specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-functions:21.1.0")
    }
    
    Kotlin 전용 라이브러리 모듈을 찾고 계신가요? 2023년 10월(Firebase BoM 32.5.0)부터 Kotlin 및 Java 개발자 모두 기본 라이브러리 모듈을 사용할 수 있습니다. 자세한 내용은 이 이니셔티브에 관한 FAQ를 참조하세요.

클라이언트 SDK 초기화

Cloud Functions의 인스턴스를 초기화합니다.

Swift

lazy var functions = Functions.functions()

Objective-C

@property(strong, nonatomic) FIRFunctions *functions;
// ...
self.functions = [FIRFunctions functions];

Web

const app = initializeApp({
  projectId: '### CLOUD FUNCTIONS PROJECT ID ###',
  apiKey: '### FIREBASE API KEY ###',
  authDomain: '### FIREBASE AUTH DOMAIN ###',
});
const functions = getFunctions(app);

Kotlin+KTX

private lateinit var functions: FirebaseFunctions
// ...
functions = Firebase.functions

Java

private FirebaseFunctions mFunctions;
// ...
mFunctions = FirebaseFunctions.getInstance();

함수 호출

Swift

functions.httpsCallable("addMessage").call(["text": inputField.text]) { result, error in
  if let error = error as NSError? {
    if error.domain == FunctionsErrorDomain {
      let code = FunctionsErrorCode(rawValue: error.code)
      let message = error.localizedDescription
      let details = error.userInfo[FunctionsErrorDetailsKey]
    }
    // ...
  }
  if let data = result?.data as? [String: Any], let text = data["text"] as? String {
    self.resultField.text = text
  }
}

Objective-C

[[_functions HTTPSCallableWithName:@"addMessage"] callWithObject:@{@"text": _inputField.text}
                                                      completion:^(FIRHTTPSCallableResult * _Nullable result, NSError * _Nullable error) {
  if (error) {
    if ([error.domain isEqual:@"com.firebase.functions"]) {
      FIRFunctionsErrorCode code = error.code;
      NSString *message = error.localizedDescription;
      NSObject *details = error.userInfo[@"details"];
    }
    // ...
  }
  self->_resultField.text = result.data[@"text"];
}];

Web

var addMessage = firebase.functions().httpsCallable('addMessage');
addMessage({ text: messageText })
  .then((result) => {
    // Read result of the Cloud Function.
    var sanitizedMessage = result.data.text;
  });

Web

import { getFunctions, httpsCallable } from "firebase/functions";

const functions = getFunctions();
const addMessage = httpsCallable(functions, 'addMessage');
addMessage({ text: messageText })
  .then((result) => {
    // Read result of the Cloud Function.
    /** @type {any} */
    const data = result.data;
    const sanitizedMessage = data.text;
  });

Kotlin+KTX

private fun addMessage(text: String): Task<String> {
    // Create the arguments to the callable function.
    val data = hashMapOf(
        "text" to text,
        "push" to true,
    )

    return functions
        .getHttpsCallable("addMessage")
        .call(data)
        .continueWith { task ->
            // This continuation runs on either success or failure, but if the task
            // has failed then result will throw an Exception which will be
            // propagated down.
            val result = task.result?.data as String
            result
        }
}

Java

private Task<String> addMessage(String text) {
    // Create the arguments to the callable function.
    Map<String, Object> data = new HashMap<>();
    data.put("text", text);
    data.put("push", true);

    return mFunctions
            .getHttpsCallable("addMessage")
            .call(data)
            .continueWith(new Continuation<HttpsCallableResult, String>() {
                @Override
                public String then(@NonNull Task<HttpsCallableResult> task) throws Exception {
                    // This continuation runs on either success or failure, but if the task
                    // has failed then getResult() will throw an Exception which will be
                    // propagated down.
                    String result = (String) task.getResult().getData();
                    return result;
                }
            });
}

Dart

    final result = await FirebaseFunctions.instance.httpsCallable('addMessage').call(
      {
        "text": text,
        "push": true,
      },
    );
    _response = result.data as String;

C++

firebase::Future<firebase::functions::HttpsCallableResult> AddMessage(
    const std::string& text) {
  // Create the arguments to the callable function.
  firebase::Variant data = firebase::Variant::EmptyMap();
  data.map()["text"] = firebase::Variant(text);
  data.map()["push"] = true;

  // Call the function and add a callback for the result.
  firebase::functions::HttpsCallableReference doSomething =
      functions->GetHttpsCallable("addMessage");
  return doSomething.Call(data);
}

Unity

private Task<string> addMessage(string text) {
  // Create the arguments to the callable function.
  var data = new Dictionary<string, object>();
  data["text"] = text;
  data["push"] = true;

  // Call the function and extract the operation from the result.
  var function = functions.GetHttpsCallable("addMessage");
  return function.CallAsync(data).ContinueWith((task) => {
    return (string) task.Result.Data;
  });
}

클라이언트의 오류 처리

서버에서 오류가 발생하거나 결과 프로미스가 거부되면 클라이언트가 오류를 수신합니다.

함수가 반환한 오류의 유형이 function.https.HttpsError이면 클라이언트가 서버 오류로부터 오류 code, message, details를 수신합니다. 그 외의 경우는 오류에 INTERNAL 메시지와 INTERNAL 코드가 포함됩니다. 호출 가능 함수의 오류 처리 방법에 대한 안내를 참조하세요.

Swift

if let error = error as NSError? {
  if error.domain == FunctionsErrorDomain {
    let code = FunctionsErrorCode(rawValue: error.code)
    let message = error.localizedDescription
    let details = error.userInfo[FunctionsErrorDetailsKey]
  }
  // ...
}

Objective-C

if (error) {
  if ([error.domain isEqual:@"com.firebase.functions"]) {
    FIRFunctionsErrorCode code = error.code;
    NSString *message = error.localizedDescription;
    NSObject *details = error.userInfo[@"details"];
  }
  // ...
}

Web

var addMessage = firebase.functions().httpsCallable('addMessage');
addMessage({ text: messageText })
  .then((result) => {
    // Read result of the Cloud Function.
    var sanitizedMessage = result.data.text;
  })
  .catch((error) => {
    // Getting the Error details.
    var code = error.code;
    var message = error.message;
    var details = error.details;
    // ...
  });

Web

import { getFunctions, httpsCallable } from "firebase/functions";

const functions = getFunctions();
const addMessage = httpsCallable(functions, 'addMessage');
addMessage({ text: messageText })
  .then((result) => {
    // Read result of the Cloud Function.
    /** @type {any} */
    const data = result.data;
    const sanitizedMessage = data.text;
  })
  .catch((error) => {
    // Getting the Error details.
    const code = error.code;
    const message = error.message;
    const details = error.details;
    // ...
  });

Kotlin+KTX

addMessage(inputMessage)
    .addOnCompleteListener { task ->
        if (!task.isSuccessful) {
            val e = task.exception
            if (e is FirebaseFunctionsException) {
                val code = e.code
                val details = e.details
            }
        }
    }

Java

addMessage(inputMessage)
        .addOnCompleteListener(new OnCompleteListener<String>() {
            @Override
            public void onComplete(@NonNull Task<String> task) {
                if (!task.isSuccessful()) {
                    Exception e = task.getException();
                    if (e instanceof FirebaseFunctionsException) {
                        FirebaseFunctionsException ffe = (FirebaseFunctionsException) e;
                        FirebaseFunctionsException.Code code = ffe.getCode();
                        Object details = ffe.getDetails();
                    }
                }
            }
        });

Dart

try {
  final result =
      await FirebaseFunctions.instance.httpsCallable('addMessage').call();
} on FirebaseFunctionsException catch (error) {
  print(error.code);
  print(error.details);
  print(error.message);
}

C++

void OnAddMessageCallback(
    const firebase::Future<firebase::functions::HttpsCallableResult>& future) {
  if (future.error() != firebase::functions::kErrorNone) {
    // Function error code, will be kErrorInternal if the failure was not
    // handled properly in the function call.
    auto code = static_cast<firebase::functions::Error>(future.error());

    // Display the error in the UI.
    DisplayError(code, future.error_message());
    return;
  }

  const firebase::functions::HttpsCallableResult* result = future.result();
  firebase::Variant data = result->data();
  // This will assert if the result returned from the function wasn't a string.
  std::string message = data.string_value();
  // Display the result in the UI.
  DisplayResult(message);
}

// ...

// ...
  auto future = AddMessage(message);
  future.OnCompletion(OnAddMessageCallback);
  // ...

Unity

 addMessage(text).ContinueWith((task) => {
  if (task.IsFaulted) {
    foreach (var inner in task.Exception.InnerExceptions) {
      if (inner is FunctionsException) {
        var e = (FunctionsException) inner;
        // Function error code, will be INTERNAL if the failure
        // was not handled properly in the function call.
        var code = e.ErrorCode;
        var message = e.ErrorMessage;
      }
    }
  } else {
    string result = task.Result;
  }
});

앱을 출시하기 전에 내 앱만 호출 가능 함수 엔드포인트에 액세스할 수 있도록 App Check를 사용 설정해야 합니다.