Firebase Cloud Functions 사용해 보기

Firebase 세팅하기
Firebase Authentication 사용해보기
Firebase Storage 사용해보기
Firebase Firestore Database

이번 글에서는 Firebase의 기능인 Cloud Functions에 대해서 다뤄보도록 하겠다.

Cloud Functions는 Firebase의 기능과 HTTP 요청에 의해서 Trigger 되는 이벤트에 응답하여 백엔드 코드를 자동으로 실행시켜주는 서버리스 프레임워크라고 한다.

이게 무슨말일까 ?

Firebase는 모바일 앱 서비스 프레임워크이다. 즉, 모바일 앱 서비스를 하기 위한 다양한 기능들을 Firebase가 지원해주면서, 흔히 백엔드, 서버 없이도 모바일 앱을 개발하고 배포할 수 있다고 한다. 물론 운영도 한다.

그럼 정말 Firebase만 사용하면 모바일 앱 서비스를 개발하는게 가능할까 ? Firebase를 자주 사용해보신 분들은 일부는 맞는 말이고, 일부는 틀렸다는 것을 잘 알고 있을 것이다.

전부 백엔드 없이 서비스를 운영할 수는 없다.

만약에 SNS 서비스를 개발한다고 가정할 때에, 나를 팔로워한 사용자들에게 내가 피드를 게시하면 푸시를 보내주고 싶다고 해보자. 어떻게 보낼것인가 ? 백엔드가 보내줘야 한다. 프론트엔드에서 보내줄 수도 있지만 거의 불가능에 가깝다.
또, Firebase Authentication은 간편 로그인을 전부 지원하지 않는다. 국내 서비스에서 절대 다수를 차지하는 카카오만 추가하려고 해도 백엔드 없이 불가능하다. JWT 토큰을 발급받아야 하는데, 프론트는 발급이 안되다. 백엔드를 구성해서 JWT 서버에서 토큰을 인증받아 커스텀 토큰을 발행해야 된다.
카카오를 꼭 Authentication에 연결하지 않아도 되지만, Firebase에 사용하는 보안규칙에 필수로 사용되는 값이 바로 UID이기 때문에 연결을 안할 수가 없다.

이런 기능들을 구성하기 위해 백엔드를 배우고, 배포하고 해야하는데 Cloud Functions를 사용하면 간단하게 구성할 수가 있다.

Cloud Functions을 프로젝트에 연결하고 배포하면서 어떻게 Flutter에서 사용할 수 있는지 하나씩 살펴보자.

여기서는 node.js를 사용해 볼것이기 때문에, node.js 설치와 Firebase 프로젝트 생성 및 연결까지는 사전에 진행해보자.

Cloud Functions

프로젝트에서 터미널을 열고 firebase login을 해주자. firebase CLI 환경이 설치되어 있지않다면, 설치를 하고 진행하면 된다.

Firebae CLI에 로그인이 되었다면, 아래 명령어를 사용하여 Cloud Functions를 설치해주자.

firebase init functions

명령어를 입력하고나면, 옵션을 선택하라고 되어있는데 저는 기존 프로젝트에 연결할 것이기 때문에 "Use an existing project"를 선택하였다. 옵션을 변경하고 싶으면 화살표로 움직이시면 되고, 선택은 엔터를 입력하면 된다.

이번에는 어떤 프로젝트와 연결할 것인지를 선택하라고 하는데, 본인이 연결하고자 하는 프로젝트와 연결하면 된다.

Functions에 사용하는 언어를 선택하면 되는데, 저는 javaScript를 선택하였다.

린트 설정을 물어보는데, 이 부분도 설정을 진행하면 된다.

종속성 설치를 진행할꺼냐고 선택하는 거라 "y"를 넣어주고 진행하자.

자 이제 기다려보면 프로젝트에 functions 폴더가 추가된 것을 확인할 수 있다.

Clound Functions는 꼭 Flutter 프로젝트에 추가할 필요는 없고, 따로 프로젝트를 생성해서 진행해도 무방하다.

본격적인 개발전 린트 규칙을 조금 추가하도록 하겠다. 린트 때문에 배포시 에러가 많아서 미리 자주 사용하는 규칙만 추가하였다.

fuctions > .eslintrc.js 파일로 이동해서 아래 린트 규칙을 추가하자.

    "no-restricted-globals": ["error", "name", "length"],
    "prefer-arrow-callback": "error",
    "quotes": ["error", "double", {
      "allowTemplateLiterals": true,
    }],
    "indent": "off",
    "semi": "off",
    "max-len": "off",

여기까지 진행했으면, Fucntions 연결과 설치는 끝이났다. 간단한 코드를 작성해서 배포를 해보자.

index.js 파일의 코드를 아래의 코드로 변경해주자.

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

exports.helloWorld = functions.region("asia-northeast3").https.onRequest((request, response) => {
    response.send("Hello from Firebase!");
});

배포를 진행해야 하는데, 배포 명령어는 2가지가 있다. 먼저 package.json에 script에 있는 deploy 명령어를 입력해도 되고, 노드 배포 명령어를 입력해도 된다.

npm run deploy

배포해보자. 먼저 터미널이 functions 폴더에 있어야 하기 때문에 functions로 이동 후 배포 명령어를 실행해보자.

아래와 같은 에러가 발생했을 것이다. 만약에 lint 관련 에러가 발생했다면, 에러 코드를 구글에서 찾아 린트 규칙을 추가하거나, 규칙에 맞는 코드를 작성하면 된다.

아래 에러를 보면 Blaze 요금제만 배포가 가능하다고 한다. Cloud Functions는 Blaze 요금제에서만 기능을 사용할 수 있다.

Firebase Spark -> Blaze

Cloud Functions를 사용하기 위해 요금제를 올려주도록 하자. 요금제를 올린다고 바로 결제되는 것이 아니다. 사용량을 초과할 때만 요금이 발생하는 것이니, 테스트 중에는 안심하고 사용해도 된다.

자신의 프로젝트 대시보드로 이동해보면, 하단부에 Spark라고 현재 요금제가 나오고 업그레이드 표시가 나와있다.

업그레이드를 누르면 Blaze 요금제에 대한 안내가 나온다.

결제 계정을 등록해야 하는데, 저는 기존에 구글 개발자 계정에 결제 정보를 등록한 적이 있어서 계정이 바로 표시가 된다.

계정이 없는 경우는 결제 카드를 등록하고 진행하면 된다.

결제 금액에 따른 안내를 받을 수 있는데, 테스트 용이니 그냥 100원으로 진행해 주자. 언제든 변경할 수 있다.

최종적으로 구매를 클릭해주면 요금제가 변경이 된다.

정상적으로 Blaze 요금제가 등록이 되었다.

이제 다시 배포를 해보자. 명령어를 실행하면 시간이 조금 걸리니 기다리면 배포가 정상적으로 된다.

Firebase 대시보드에서 Functions 탭으로 이동해보자.

Functions에 배포된 트리거가 추가 되어있는 것을 확인할 수 있다. 해당 HTTP 요청 주소가 보이는데, 해당 주소를 호출하면 우리가 배포한 코드되로 문구가 리턴이 될 것이다.

각자 배포된 HTTP Url을 주소창에 넣어보자. 정상적으로 실행이 된다.

이렇게 API를 생성하여 작업을 할 수도있고, 푸시를 보내거나 커스텀 토큰 발행, DB 트리거 등을 사용하여 백엔드 코드를 실행시킬 수 있는 것이다.

이번엔 Git에 올라와있는 예제를 사용해서 다른 기능도 한 번 살펴보도록 하겠다.

Convert Image

이번엔 Firebase Storage에 게시한 이미지에 대해서 확장자를 jpg로 변경하는 기능을 만들어보자.

Storage를 사용할 수 있도록 대시보드에서 설정을 먼저 진행해주자. Storage에 대해서 간단하게 설명하자면 이미지, 동영상, 파일 등을 저장하는 저장소라고 생각하면 된다. 자세한 설명은 아래 글을 참고하자.

Firebase Storage 사용해보기

Storage 탭으로 이동하여 시작하기를 눌러주자.

시작하기를 눌러 테스트 모드로 시작을 하자.

여기서는 리전을 선택해야 한다. 리전은 저장소가 위치한 장소를 말하는데, "asia-northeast3" 리전이 서울에 위치해있다.

각 서비스에 맞는 리전은 최초 설정시 신중히 정해야한다. 나중에 변경 못한다고 알고 있다(이 부분은 정확하진 않음).

리전을 선택하고 완료를 해주면 저장소가 생성됬다.

package.json에 dependencies를 추가해주자.

  "dependencies": {
    "firebase-admin": "^11.5.0",
    "firebase-functions": "^4.2.0",
    "child-process-promise": "^2.2.1",
    "mkdirp": "^1.0.3",
    "mkdirp-promise": "^5.0.1"
  },

추가해주고 아래 명령어를 입력하도록 하자.

npm install

위에서 진행한 index.js 파일에 아래 코드를 추가해주자. javascript 코드에 대해서는 따로 설명하지 않도록 하겠다.

중요한 부분 하나만 보자면, region("asia-northeast3")을 추가해주는 것인데, 해당 코드를 추가하지 않으면 디폴트 리전으로 설정되서 미국 리전이 선택되기 때문에, 리전은 반드시 설정해주자.

만약 저장소 리전은 아시아인데, 트리거 리전이 미국이라면 비용도 비용인데, 시간도 오래 소요될 것이다. 보통 트리거는 해당 기능의 DB와 동일한 리전을 사용한다.

const admin = require("firebase-admin");
const mkdirp = require("mkdirp");
const spawn = require("child-process-promise").spawn;
const path = require("path");
const os = require("os");
const fs = require("fs");
admin.initializeApp();

// File extension for the created JPEG files.
const JPEG_EXTENSION = ".jpg";

exports.imageToJPG = functions.region("asia-northeast3").storage.object().onFinalize(async (object) => {
    const filePath = object.name;
    const baseFileName = path.basename(filePath, path.extname(filePath));
    const fileDir = path.dirname(filePath);
    const JPEGFilePath = path.normalize(path.format({
        dir: fileDir, name: baseFileName, ext: JPEG_EXTENSION,
    }));
    const tempLocalFile = path.join(os.tmpdir(), filePath);
    const tempLocalDir = path.dirname(tempLocalFile);
    const tempLocalJPEGFile = path.join(os.tmpdir(), JPEGFilePath);

    // Exit if this is triggered on a file that is not an image.
    if (!object.contentType.startsWith("image/")) {
        functions.logger.log("This is not an image.");
        return null;
    }

    // Exit if the image is already a JPEG.
    if (object.contentType.startsWith("image/jpeg")) {
        functions.logger.log("Already a JPEG.");
        return null;
    }

    const bucket = admin.storage().bucket(object.bucket);
    // Create the temp directory where the storage file will be downloaded.
    await mkdirp(tempLocalDir);
    // Download file from bucket.
    await bucket.file(filePath).download({
        destination: tempLocalFile,
    });
    functions.logger.log("The file has been downloaded to", tempLocalFile);
    // Convert the image to JPEG using ImageMagick.
    await spawn("convert", [tempLocalFile, tempLocalJPEGFile]);
    functions.logger.log("JPEG image created at", tempLocalJPEGFile);
    // Uploading the JPEG image.
    await bucket.upload(tempLocalJPEGFile, {
        destination: JPEGFilePath,
    });
    functions.logger.log("JPEG image uploaded to Storage at", JPEGFilePath);
    // Once the image has been converted delete the local files to free up disk space.
    fs.unlinkSync(tempLocalJPEGFile);
    fs.unlinkSync(tempLocalFile);
    return null;
});

다시 배포를 진행하자.

npm run deploy

imageToJPG라는 트리거가 추가 되었다. 한 번 기능을 테스트 해보자.

Jpg/Jpeg가 아닌 이미지 아무거나 준비해서 Storage 대시보드로 이동하자.

파일 업로드를 클릭하여 이미지를 업로드 해주자.

Png 파일을 준비했다.

정상적으로 업로드가 된 것을 확인할 수 있다. 이제 대시보드 페이지를 새로고침 해보자.

아래 jpg로 변경된 이미지가 하나 더 생성된 것을 확인할 수 있다.

이런 기능이 별거 아닌 것 처럼 보일 수 있지만, 매우 편한 기능이다. 만일 Flutter에서 이미지 확장자를 변경해서 Storage에 업로드 한다면 엄청 오래 걸린다...

이미지를 자주 다뤄 보신 분들은 공감할꺼다. 백엔드 없이 개발하면 제일 불편한 부분이 이미지 처리하는 부분이기도 하다.

이번에는 jpg 이미지를 한 번 올려보자. 이번엔 새로운 파일이 추가되지 않았다.

로그를 출력해보면 이유를 알 수 있다.

firebase functions:log

로그를 확인해보면, "Already a JPEG"라는 로그가 출력된 것을 확인할 수 있다. 해당 로그는 이미 jpg 이미지인 경우 출력하는 로그로 코드 부분을 확인하면 알 수 있다.

Welcome Email

이번에는 신규 사용자에게 Welcome 이메일을 보내는 기능을 만들어 보자.

해당 기능은 node.js의 nodemailer 기능을 사용할 것이고, Gmail 사용자는 앱 비밀번호를 발급받아 사용해야 한다.

보안 수준으로 인해 메일이 전송이 되지 않는다.

Google App Password

아래 구글 계정관리로 진입해보자.

Google Account

보안 탭에서 아래를 내려보면 2단계 인증이라고 보인다. 2단계 인증을 눌러주자.

스크롤을 내려보면 앱 비밀번호라고 하는 부분이 보이는데, 저는 이미 발급 받은 상태라 1개가 보이고, 아직 발급된 비밀번호가 없으면 비밀번호 없음이라고 나올 것이다.

아래와 같이 선택해주자.

앱 비밀번호 이름을 자유롭게 정해서 생성을 눌러주도록 하자.

아래와 같이 비밀번호가 생성됬을 것이다. 12자리 숫자를 사용해서 구글 인증을 처리하면 메일을 정상적으로 보낼 수 있다.

Authentication Trigger

functions package.json 파일의 dependencies에 nodemailer를 추가해주자.

  "dependencies": {
    ...
    "nodemailer": "^6.9.4"
  },

index.js 파일에 아래 코드를 추가해주도록 하자. 코드를 살펴보면 auth.user().onCreate()에서 Authentication에 새로운 사용자가 추가되면 해당 트리거가 작동이 되기 때문에, 해당 함수에서 신규 사용자에게 메일을 전송해주면 된다. 간단하다.

배포를 해주자.

const nodemailer = require("nodemailer")
const gmailEmail = functions.config().gmail.email;
const gmailPassword = functions.config().gmail.password;

exports.createUserWithWelcomeEmail = functions.region("asia-northeast3").auth.user().onCreate(async (user) => {
    const transporter = nodemailer.createTransport({
        service: "gmail",
        auth: {
            user: gmailEmail,
            pass: gmailPassword,
        },
    });

    const mailOptions = {
        from: gmailEmail,
        to: user.email,
        subject: "Welcome To My App!",
        text: `Hey ${user.displayName || ""}! Welcome to My App. I hope you will enjoy our service.`,
    };

    try {
        await transporter.sendMail(mailOptions);
        functions.logger.log(
            `New Welcom email sent to: ${user.email}`,
            user.email,
        );
    } catch (error) {
        functions.logger.error(
            `There was an error while sending the email: ${user.email}`,
            error,
        );
    }
});

터미널에서 Config를 설정해주어야 하는데, 아래와 같이 명령어를 입력하면 된다.

이메일은 gmail.com 까지 전부다 작성해야하고, 패스워드 부분에는 위에서 발급한 앱 비밀번호를 넣어주면 된다.

firebase functions:config:set gmail.email="user_email_address" gmail.password="user_app_password"

아래 명령어로 제대로 세팅되었는지 확인해보자.

firebase functions:config:get

Create User

Authentication 대시보드로 이동해보자.

로그인 방법에 이메일/비밀번호를 추가하도록 하자.

사용 설정을 해주고 저장을 클릭해 주자.

이제 이메일/비밀번호를 사용할 수 있다.

pubspec.yaml 파일에 dependencies 부분을 추가하자.

dependencies:
	firebase_auth: ^4.7.2

전송받을 이메일 주소를 넣어주고 아래 기능을 아무 버튼을 만들어서 실행해주자.

onTap: () async {
	await FirebaseAuth.instance.createUserWithEmailAndPassword(
    email: "user_email", password: "123123");
},

Authentication 대시보드에 정상적으로 사용자가 추가되었고, 이제 전송 받은 이메일로 들어가서 이메일로 보내졌는지 확인해 보도록 하자.

기능이 정상적으로 작동된다.

Flutter에서도 API를 통해 Gmail 전송이 가능하지만, 앱은 계속 딜레이가 발생할 수 밖에 없다. 이런 기능은 백엔드에서 보내주는 것이 좋다.

Create Profile

이번에는 Firestore에 트리거를 추가해보도록 하겠다.

새로운 유저가 추가되면, "users" 컬렉션에 데이터를 추가하고, functions에서는 users 컬렉션에 새로운 데이터가 추가되면 profiles 라는 컬렉션에 추가된 사용자의 profile을 구성하는 기능을 만들어 보도록 하자.

먼저 Firestore를 사용하도록 해보자.

Firebase 다른 기능들 처럼 생성하는 방식은 비슷하다.

리전 세팅시 원하는 위치로 생성을 해주자.

pubspec.yaml에 cloud_firestore를 추가하자.

dependencies:
	cloud_firestore: ^4.8.4

Flutter에서 버튼 하나를 생성해서 사용자를 추가하고, 추가된 사용자의 정보로 users 컬렉션에 데이터를 추가하도록 하자.

UserCredential credential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
		email: "abc@gmail.com", password: "123123");
	if (credential.user != null) {
		await FirebaseFirestore.instance.collection("users").doc(credential.user!.uid).set({
                  "uid": credential.user!.uid,
                  "email": credential.user!.email,
                  "name": credential.user!.displayName,
                  "createdAt": Timestamp.now(),
                });
              }

index.js에 아래 코드를 추가해서 users 컬렉션의 데이터가 추가되는 시점에 해당 데이터로 profiles 컬렉션에 추가해주도록 하자.

배포를 다시 해주자.

exports.createProfile = functions.region("asia-northeast3").firestore.document("users/{uid}").onCreate(async (snapshot) => {
    const {
        email, uid, name, createdAt,
    } = snapshot.data();
    await admin.firestore().doc(`profiles/${uid}`).set({
        "uid": uid,
        "email": email,
        "name": name,
        "createdAt": createdAt,
        "age": null,
        "address": null,
        "followers": [],
        "followings": [],
    });
    return null;
});

테스트를 해보면 users 컬렉션에 데이터가 생성이 되었고, 바로 profiles 컬렉션에 데이터가 추가된 것을 확인할 수 있다.

Update Profile

이번에는 users 컬렉션에 name이 변경될 때, profile 컬렉션 문서의 name을 수정하는 방법이다.

사용 예제를 보여주기 위해서 작성한 코드이고, 실제로는 이렇게는 사용하지 않습니다.

exports.updateUser = functions.region("asia-northeast3").firestore.document("users/{uid}").onUpdate(async (snapshot, context) => {
    const name = snapshot.after.data();
    await admin.firestore().doc(`profiles/${context.params.uid}`).update({
        "name": name.name,
    });
});

실행해보면, 정상적으로 작동하는 것을 확인할 수 있다.

이외에도 생성, 변경, 삭제 등을 한 번에 수신하는 기능도 지원하고 있다.


마무리

간단하게 Firebase functions 기능을 다뤄봤다. node.js에 익숙하면 어렵지 않는 기능인데, 익숙하지 않다면 어려웠을 수도 있다.
node.js 외에도 python으로도 사용이 가능하기에, 저한테 좀 더 익숙한 python을 사용하는 방법도 게시하도록 하겠다.

원래 Push Notifications 기능과 Custom Token 발행을 다뤄보려고 작성한 글인데, 기본적인 사용법을 작성하다 보니 내용이 길어져서 다음 글에서 이어서 다뤄보도록 하겠다.

Functions는 트리거를 발생시켜 이벤트를 처리하는 기능이기에, 필요한 영역에서만 사용하시면 되고, 여기서 다룬 예제는 최대한 기능을 사용해보려고 다룬 내용이니 참고만 하시길 바란다.

profile
Flutter Developer

2개의 댓글

comment-user-thumbnail
2023년 8월 22일

Firebase functions 파이썬 버전 기대하고 있겠습니다

답글 달기
comment-user-thumbnail
2024년 9월 20일

감사합니다 잘보고 가요 와우!

답글 달기