꿈꾸는 시스템 디자이너

Flutter 강좌 - [Firebase] Cloud Functions 사용법 #2 본문

Development/Flutter

Flutter 강좌 - [Firebase] Cloud Functions 사용법 #2

독행소년 2019. 11. 13. 15:34

Flutter Code Examples 강좌를 추천합니다.

  • 제 블로그에서 Flutter Code Examples 프로젝트를 시작합니다.
  • Flutter의 다양한 예제를 소스코드와 실행화면으로 제공합니다.
  • 또한 모든 예제는 Flutter Code Examples 앱을 통해 테스트 가능합니다.

Flutter Code Examples 강좌로 메뉴로 이동

Flutter Code Examples 강좌 목록 페이지로 이동

Flutter Code Examples 앱 설치 | Google Play Store로 이동

 

Flutter Code Examples - Google Play 앱

Are you a beginner at Flutter? Check out the various features of Flutter through the demo. Source code for all demos is also provided.

play.google.com


 

Flutter 강좌 시즌2 목록 : https://here4you.tistory.com/149

 

지난 강좌에서는 Firebase Cloud Function의 개념에 대해 알아보고 실제 helloWorld 함수를 만들어서 Cloud Function에 배포한 후 웹브라우저와 앱을 이용해서 함수를 호출해봤다.

2019/11/12 - [Development/Flutter] - Flutter 강좌 - [Firebase] Firebase Cloud Functions 사용법 #1

지난 강좌에서 함수를 호출하기 위해 해당 함수의 URL을 이용했다. 웹브라우저의 경우에는 URL을 이용해서 함수에 접근할 수 밖에 없겠지만 앱에서 URL을 통해 함수에 접근하는 것을 실제 함수 호출이라고 할 수 있을까?

이번 강좌에서는 cloud_funtions 플러그인을 이용하여 Cloud Functions을 함수처럼 호출하는 방법에 대해서 알아본다.

플러그인의 정보는 다음의 사이트를 참고한다.

https://pub.dev/packages/cloud_functions

 

pubspec.yaml 파일에 플러그인을 추가한 후 Packages get 명령을 실행한다.

dependencies:
  cloud_functions: ^0.4.1+4

 

지난 강좌에서 사용한 소스코드를 다음과 같이 수정한다.

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:cloud_functions/cloud_functions.dart';

CloudFunctionsHelloWorldState pageState;

class CloudFunctionsHelloWorld extends StatefulWidget {
  @override
  CloudFunctionsHelloWorldState createState() {
    pageState = CloudFunctionsHelloWorldState();
    return pageState;
  }
}

class CloudFunctionsHelloWorldState extends State<CloudFunctionsHelloWorld> {
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

  final HttpsCallable helloWorld = CloudFunctions.instance
      .getHttpsCallable(functionName: 'helloWorld') // 호출할 Cloud Functions 의 함수명
        ..timeout = const Duration(seconds: 30);  // 타임아웃 설정(옵션)

  String resp = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(title: Text("Cloud Functions HelloWorld")),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ListView(
          children: <Widget>[
            Container(
              alignment: Alignment.center,
              margin: const EdgeInsets.symmetric(vertical: 20),
              padding: const EdgeInsets.all(10),
              color: Colors.deepOrangeAccent,
              child: Text(
                resp,
                style: TextStyle(color: Colors.white, fontSize: 16),
              ),
            ),
            // URL로 helloWorld 에 접근
            RaisedButton(
              child: Text("http.get helloWorld"),
              onPressed: () async {
                clearResponse();
                String url =
                    "https://us-central1-flutter-firebase-a2994.cloudfunctions.net/helloWorld";
                showProgressSnackBar();
                var response = await http.get(url);
                hideProgressSnackBar();
                setState(() {
                  resp = response.body;
                });
              },
            ),

            // Cloud Functions 으로 호출
            RaisedButton(
              child: Text("Call Cloud Function helloWorld"),
              onPressed: () async {
                try {
                  clearResponse();
                  showProgressSnackBar();
                  final HttpsCallableResult result = await helloWorld.call();
                  setState(() {
                    resp = result.data;
                  });
                } on CloudFunctionsException catch (e) {
                  print('caught firebase functions exception');
                  print('code: ${e.code}');
                  print('message: ${e.message}');
                  print('details: ${e.details}');
                } catch (e) {
                  print('caught generic exception');
                  print(e);
                }
                hideProgressSnackBar();
              },
            )
          ],
        ),
      ),
    );
  }

  clearResponse() {
    setState(() {
      resp = "";
    });
  }

  showProgressSnackBar() {
    _scaffoldKey.currentState
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          duration: Duration(seconds: 10),
          content: Row(
            children: <Widget>[
              CircularProgressIndicator(),
              Text("   Calling Firebase Cloud Functions...")
            ],
          ),
        ),
      );
  }

  hideProgressSnackBar() {
    _scaffoldKey.currentState..hideCurrentSnackBar();
  }

  showErrorSnackBar(String msg) {
    _scaffoldKey.currentState
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          backgroundColor: Colors.red[400],
          duration: Duration(seconds: 10),
          content: Text(msg),
          action: SnackBarAction(
            label: "Done",
            textColor: Colors.white,
            onPressed: () {},
          ),
        ),
      );
  }
}

소스코드를 살펴보면, Http Get 메소드를 이용하여 helloWorld 함수에 접근하는 기존 코드에 cloud_functions 플러그인을 이용하여 helloWorld 함수를 호출하는 코드가 추가되었다.

앱을 실행시켜서 helloWorld 함수를 호출해 보자. 그런데 다음과 같은 Exception이 발생한다.

Performing hot reload...
Syncing files to device AOSP on IA Emulator...
Reloaded 2 of 605 libraries in 276ms.
I/flutter ( 4420): caught firebase functions exception
I/flutter ( 4420): code: INTERNAL
I/flutter ( 4420): message: Response is not valid JSON object.
I/flutter ( 4420): details: null

내용을 살펴보면 응답 메시지가 유효한 JSON 오브젝트가 아니라는 의미이다. 지난 강좌에서 구현한 helloWorld 함수의 응답 메시지가 JSON 포맷이 아니기 때문에 발생한 것으로 보인다.

 

helloWorld 함수가 JSON 포멧으로 응답 메시지를 반환하도록 수정한 후 다시 배포한다. 배포 방법은 직전 강좌를 참고한다.

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

exports.helloWorld = functions.https.onRequest((request, response) => {
  response.send({ data: "Hello from Firebase" });
});

 

 

우선 첫번째 버튼을 눌러 URL을 통해 함수를 호출해 보자. JSON 포멧의 데이터로 수신된 응답 메시지가 출력된다.

 

두번째 버튼을 눌러 함수 호출 방식으로 helloWorld 함수를 호출해보자.

정상적으로 메시지가 출력되는 것을 확인할 수 있다.

 

다음으로는 아규먼트를 가지는 Cloud Functions을 만들어보자.

index.js 파일을 다음과 같이 수정한다.

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

exports.helloWorld = functions.https.onRequest((request, response) => {
  response.send({ data: "Hello from Firebase" });
});

exports.addCount = functions.https.onCall((data, context) => {
  var count = parseInt(data["count"], 10);
  return ++count;
});

exports.removeCount = functions.https.onCall((data, context) => {
  var count = data["count"];
  return --count;
});

숫자를 입력 받아 증가시키는 addCount와 감소시키는 removeCount 함수를 두개 추가 했다. 두 함수를 보면 onRequest가 아닌 onCall을 이용해서 구현했다. 

onCall을 이용하면 좀 더 함수처럼 동작한다. helloWorld 함수에서 응답 메시지를 JSON 형태로 반환한 것과 달리 count 값을 직접 return하는 형태로 구현이 가능하다.

 

앱은 다음과 같이 수정한다.

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:cloud_functions/cloud_functions.dart';

CloudFunctionsHelloWorldState pageState;

class CloudFunctionsHelloWorld extends StatefulWidget {
  @override
  CloudFunctionsHelloWorldState createState() {
    pageState = CloudFunctionsHelloWorldState();
    return pageState;
  }
}

class CloudFunctionsHelloWorldState extends State<CloudFunctionsHelloWorld> {
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

  final HttpsCallable helloWorld = CloudFunctions.instance
      .getHttpsCallable(functionName: 'helloWorld') // 호출할 Cloud Functions 의 함수명
        ..timeout = const Duration(seconds: 30); // 타임아웃 설정(옵션)

  final HttpsCallable addCount = CloudFunctions.instance
      .getHttpsCallable(functionName: 'addCount') // 호출할 Cloud Functions 의 함수명
        ..timeout = const Duration(seconds: 30); // 타임아웃 설정(옵션)

  final HttpsCallable removeCount = CloudFunctions.instance.getHttpsCallable(
      functionName: 'removeCount') // 호출할 Cloud Functions 의 함수명
    ..timeout = const Duration(seconds: 30); // 타임아웃 설정(옵션)

  String resp = "";
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(title: Text("Cloud Functions HelloWorld")),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ListView(
          children: <Widget>[
            Container(
              alignment: Alignment.center,
              margin: const EdgeInsets.symmetric(vertical: 20),
              padding: const EdgeInsets.all(10),
              color: Colors.deepOrangeAccent,
              child: Text(
                resp,
                style: TextStyle(color: Colors.white, fontSize: 16),
              ),
            ),
            // URL로 helloWorld 에 접근
            RaisedButton(
              child: Text("http.get helloWorld"),
              onPressed: () async {
                clearResponse();
                String url =
                    "https://us-central1-flutter-firebase-a2994.cloudfunctions.net/helloWorld";
                showProgressSnackBar();
                var response = await http.get(url);
                hideProgressSnackBar();
                setState(() {
                  resp = response.body;
                });
              },
            ),

            // Cloud Functions 으로 호출
            RaisedButton(
              child: Text("Call Cloud Function helloWorld"),
              onPressed: () async {
                try {
                  clearResponse();
                  showProgressSnackBar();
                  final HttpsCallableResult result = await helloWorld.call();
                  setState(() {
                    resp = result.data;
                  });
                } on CloudFunctionsException catch (e) {
                  print('caught firebase functions exception');
                  print('code: ${e.code}');
                  print('message: ${e.message}');
                  print('details: ${e.details}');
                } catch (e) {
                  print('caught generic exception');
                  print(e);
                }
                hideProgressSnackBar();
              },
            ),

            Container(
              padding: const EdgeInsets.symmetric(vertical: 20),
              alignment: Alignment.center,
              child: Text(
                count.toString(),
                style: TextStyle(
                  color: Colors.blueGrey,
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
            Container(
              height: 40,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  FloatingActionButton(
                    heroTag: null,
                    child: Icon(Icons.remove),
                    onPressed: () async {
                      final HttpsCallableResult result = await removeCount.call(<String, dynamic>{'count': count});
                      print(result.data);
                      setState(() {
                        count = result.data;
                      });
                    },
                  ),
                  FloatingActionButton(
                    heroTag: null,
                    child: Icon(Icons.add),
                    onPressed: () async {
                      final HttpsCallableResult result = await addCount.call(<String, dynamic>{'count': count});
                      print(result.data);
                      setState(() {
                        count = result.data;
                      });
                    },
                  )
                ],
              ),
            )
          ],
        ),
      ),
    );
  }

  clearResponse() {
    setState(() {
      resp = "";
    });
  }

  showProgressSnackBar() {
    _scaffoldKey.currentState
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          duration: Duration(seconds: 10),
          content: Row(
            children: <Widget>[
              CircularProgressIndicator(),
              Text("   Calling Firebase Cloud Functions...")
            ],
          ),
        ),
      );
  }

  hideProgressSnackBar() {
    _scaffoldKey.currentState..hideCurrentSnackBar();
  }

  showErrorSnackBar(String msg) {
    _scaffoldKey.currentState
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          backgroundColor: Colors.red[400],
          duration: Duration(seconds: 10),
          content: Text(msg),
          action: SnackBarAction(
            label: "Done",
            textColor: Colors.white,
            onPressed: () {},
          ),
        ),
      );
  }
}

 

필드 count의 값을 Cloud Function인 addCount와 removeCount 함수를 이용하여 값을 가감하는 기능이 추가되었다.

 

실제 실행화면은 다음과 같다. 버튼을 누를 때 마다 해당 Cloud Function이 호출되어 값이 가감되어 화면에 출력된다.

 

 

Comments