콜백을 사용한 인간 참여형(Human-In-The-Loop) 워크플로 만들기


이 튜토리얼에서는 사용자 입력(인간 참여형(Human In The Loop))을 기다렸다가 Firestore 데이터베이스, Cloud 함수 2개, Cloud Translation API, Firebase SDK를 사용해 실시간으로 업데이트하는 웹페이지를 연결하는 번역 워크플로를 만드는 방법을 보여줍니다.

Workflows를 사용하면 HTTP 요청이 해당 엔드포인트에 도달할 때까지 기다리는 콜백 엔드포인트(또는 웹훅)를 지원할 수 있으며 나중에 워크플로 실행을 재개할 수 있습니다. 이 경우 워크플로는 일부 텍스트 번역을 거부하거나 검증할 때까지 기다리지만 외부 프로세스를 기다릴 수도 있습니다. 자세한 내용은 콜백을 사용하여 대기를 참조하세요.

아키텍처

이 튜토리얼에서는 다음 작업을 수행할 수 있는 웹 앱을 만듭니다.

  1. 번역 웹페이지에서 프랑스어로 번역하려는 영어 텍스트를 입력합니다. 번역을 클릭합니다.
  2. 웹페이지에서 워크플로 실행을 시작하는 Cloud 함수가 호출됩니다. 함수와 워크플로 모두에 번역할 텍스트가 매개변수로 전달됩니다.
  3. 텍스트는 Cloud Firestore 데이터베이스에 저장됩니다. Cloud Translation API를 호출할 수 있습니다. 반환된 번역은 데이터베이스에 저장됩니다. 웹 앱은 Firebase 호스팅을 사용하여 배포되며 번역된 텍스트를 표시합니다.
  4. 워크플로의 create_callback 단계에서는 역시 Firestore 데이터베이스에 저장되는 콜백 엔드포인트 URL을 만듭니다. 이제 웹페이지에 유효성 검사거부 버튼이 모두 표시됩니다.
  5. 워크플로가 일시중지되며 콜백 엔드포인트 URL에 대한 명시적인 HTTP POST 요청을 기다립니다.
  6. 사용자가 번역을 검증할지 아니면 거부할지 결정할 수 있습니다. 버튼을 클릭하면 두 번째 Cloud 함수가 호출되어 워크플로에서 생성된 콜백 엔드포인트를 호출하고 승인 상태를 전달합니다. 워크플로가 실행을 재개하고 Firestore 데이터베이스에 true 또는 false 승인 상태를 저장합니다.

다음 다이어그램은 프로세스의 개요를 보여줍니다.

콜백을 사용한 워크플로

목표

  • 웹 앱을 배포합니다.
  • 번역 요청을 저장할 Firestore 데이터베이스를 만듭니다.
  • Cloud 함수를 배포하여 워크플로를 실행합니다.
  • 워크플로를 배포하고 실행하여 전체 프로세스를 조정합니다.

비용

이 문서에서는 비용이 청구될 수 있는 다음과 같은 Google Cloud 구성요소를 사용합니다.

프로젝트 사용량을 기준으로 예상 비용을 산출하려면 가격 계산기를 사용하세요. Google Cloud를 처음 사용하는 사용자는 무료 체험판을 사용할 수 있습니다.

시작하기 전에

조직에서 정의한 보안 제약조건으로 인해 다음 단계를 완료하지 못할 수 있습니다. 문제 해결 정보는 제한된 Google Cloud 환경에서 애플리케이션 개발을 참조하세요.

  1. Google Cloud 계정에 로그인합니다. Google Cloud를 처음 사용하는 경우 계정을 만들고 Google 제품의 실제 성능을 평가해 보세요. 신규 고객에게는 워크로드를 실행, 테스트, 배포하는 데 사용할 수 있는 $300의 무료 크레딧이 제공됩니다.
  2. Google Cloud CLI를 설치합니다.
  3. gcloud CLI를 초기화하려면 다음 명령어를 실행합니다.

    gcloud init
  4. Google Cloud 프로젝트를 만들거나 선택합니다.

    • Google Cloud 프로젝트를 만듭니다.

      gcloud projects create PROJECT_ID

      PROJECT_ID를 만들려는 Google Cloud 프로젝트의 이름으로 바꿉니다.

    • 만든 Google Cloud 프로젝트를 선택합니다.

      gcloud config set project PROJECT_ID

      PROJECT_ID를 Google Cloud 프로젝트 이름으로 바꿉니다.

  5. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  6. App Engine, Cloud Build, Cloud Functions, Firestore, Translation, and Workflows API를 사용 설정합니다.

    gcloud services enable appengine.googleapis.com cloudbuild.googleapis.com cloudfunctions.googleapis.com firestore.googleapis.com translate.googleapis.com workflows.googleapis.com
  7. Google Cloud CLI를 설치합니다.
  8. gcloud CLI를 초기화하려면 다음 명령어를 실행합니다.

    gcloud init
  9. Google Cloud 프로젝트를 만들거나 선택합니다.

    • Google Cloud 프로젝트를 만듭니다.

      gcloud projects create PROJECT_ID

      PROJECT_ID를 만들려는 Google Cloud 프로젝트의 이름으로 바꿉니다.

    • 만든 Google Cloud 프로젝트를 선택합니다.

      gcloud config set project PROJECT_ID

      PROJECT_ID를 Google Cloud 프로젝트 이름으로 바꿉니다.

  10. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  11. App Engine, Cloud Build, Cloud Functions, Firestore, Translation, and Workflows API를 사용 설정합니다.

    gcloud services enable appengine.googleapis.com cloudbuild.googleapis.com cloudfunctions.googleapis.com firestore.googleapis.com translate.googleapis.com workflows.googleapis.com
  12. Google Cloud CLI 구성요소를 업데이트합니다.
    gcloud components update
  13. 계정을 사용하여 로그인합니다.
    gcloud auth login
  14. 이 튜토리얼에서 사용한 프로젝트 ID와 기본 위치를 설정합니다.
    export GOOGLE_CLOUD_PROJECT=PROJECT_ID
    export REGION=REGION
    gcloud config set workflows/location ${REGION}
    

    다음을 바꿉니다.

    • PROJECT_ID: Google Cloud 프로젝트 ID입니다. 프로젝트 ID는 Google Cloud 콘솔의 시작 페이지에서 찾을 수 있습니다.
    • REGION: 지원되는 Workflows 위치 중 원하는 위치입니다.

첫 번째 Cloud 함수 배포

이 Cloud 함수는 워크플로 실행을 시작합니다. 함수와 워크플로 모두에 번역할 텍스트가 매개변수로 전달됩니다.

  1. invokeTranslationWorkflow,translationCallbackCall, public이라는 하위 디렉터리가 있는 callback-translation 디렉터리를 만듭니다.

    mkdir -p ~/callback-translation/{invokeTranslationWorkflow,translationCallbackCall,public}
    
  2. invokeTranslationWorkflow 디렉터리로 변경합니다.

    cd ~/callback-translation/invokeTranslationWorkflow
    
  3. 다음 Node.js 코드가 포함된 파일 이름이 index.js인 텍스트 파일을 만듭니다.

    const cors = require('cors')({origin: true});
    const {ExecutionsClient} = require('@google-cloud/workflows');
    const client = new ExecutionsClient();
    
    exports.invokeTranslationWorkflow = async (req, res) => {
      cors(req, res, async () => {
        const text = req.body.text;
        console.log(`Translation request for "${text}"`);
    
        const PROJECT_ID = process.env.PROJECT_ID;
        const CLOUD_REGION = process.env.CLOUD_REGION;
        const WORKFLOW_NAME = process.env.WORKFLOW_NAME;
    
        const execResponse = await client.createExecution({
          parent: client.workflowPath(PROJECT_ID, CLOUD_REGION, WORKFLOW_NAME),
          execution: {
            argument: JSON.stringify({text})
          }
        });
        console.log(`Translation workflow execution request: ${JSON.stringify(execResponse)}`);
    
        const execName = execResponse[0].name;
        console.log(`Created translation workflow execution: ${execName}`);
    
        res.set('Access-Control-Allow-Origin', '*');
        res.status(200).json({executionId: execName});
      });
    };
  4. 다음 npm 메타데이터를 포함하는, 파일 이름이 package.json인 텍스트 파일을 만듭니다.

    {
      "name": "launch-translation-workflow",
      "version": "0.0.1",
      "dependencies": {
        "@google-cloud/workflows": "^1.2.5",
        "cors": "^2.8.5"
      }
    }
    
  5. HTTP 트리거를 사용하여 함수를 배포하고 인증되지 않은 액세스를 허용합니다.

    gcloud functions deploy invokeTranslationWorkflow \
    --region=${REGION} \
    --runtime nodejs14 \
    --entry-point=invokeTranslationWorkflow \
    --set-env-vars PROJECT_ID=${GOOGLE_CLOUD_PROJECT},CLOUD_REGION=${REGION},WORKFLOW_NAME=translation_validation \
    --trigger-http \
    --allow-unauthenticated
    

    함수를 배포하는 데 몇 분 정도 걸릴 수 있습니다. Google Cloud Console에서 Cloud Functions 인터페이스를 사용하여 함수를 배포할 수도 있습니다.

  6. 함수가 배포되면 httpsTrigger.url 속성을 확인할 수 있습니다.

    gcloud functions describe invokeTranslationWorkflow
    

    이후 단계에서 사용할 수 있도록 반환된 URL을 기록해 둡니다.

두 번째 Cloud 함수 배포

이 Cloud 함수는 워크플로에서 생성된 콜백 엔드포인트에 HTTP POST 요청을 보내면서 번역을 검증할지 아니면 거부할 지를 반영하는 승인 상태를 전달합니다.

  1. translationCallbackCall 디렉터리로 변경합니다.

    cd ../translationCallbackCall
    
  2. 다음 Node.js 코드가 포함된 파일 이름이 index.js인 텍스트 파일을 만듭니다.

    const cors = require('cors')({origin: true});
    const fetch = require('node-fetch');
    
    exports.translationCallbackCall = async (req, res) => {
      cors(req, res, async () => {
        res.set('Access-Control-Allow-Origin', '*');
    
        const {url, approved} = req.body;
        console.log("Approved? ", approved);
        console.log("URL = ", url);
        const {GoogleAuth} = require('google-auth-library');
        const auth = new GoogleAuth();
        const token = await auth.getAccessToken();
        console.log("Token", token);
    
        try {
          const resp = await fetch(url, {
              method: 'POST',
              headers: {
                  'accept': 'application/json',
                  'content-type': 'application/json',
                  'authorization': `Bearer ${token}`
              },
              body: JSON.stringify({ approved })
          });
          console.log("Response = ", JSON.stringify(resp));
    
          const result = await resp.json();
          console.log("Outcome = ", JSON.stringify(result));
    
          res.status(200).json({status: 'OK'});
        } catch(e) {
          console.error(e);
    
          res.status(200).json({status: 'error'});
        }
      });
    };
  3. 다음 npm 메타데이터를 포함하는, 파일 이름이 package.json인 텍스트 파일을 만듭니다.

    {
      "name": "approve-translation-workflow",
      "version": "0.0.1",
      "dependencies": {
        "cors": "^2.8.5",
        "node-fetch": "^2.6.1",
        "google-auth-library": "^7.1.1"
      }
    }
    
  4. HTTP 트리거를 사용하여 함수를 배포하고 인증되지 않은 액세스를 허용합니다.

    gcloud functions deploy translationCallbackCall \
    --region=${REGION} \
    --runtime nodejs14 \
    --entry-point=translationCallbackCall \
    --trigger-http \
    --allow-unauthenticated
    

    함수를 배포하는 데 몇 분 정도 걸릴 수 있습니다. Google Cloud Console에서 Cloud Functions 인터페이스를 사용하여 함수를 배포할 수도 있습니다.

  5. 함수가 배포되면 httpsTrigger.url 속성을 확인할 수 있습니다.

    gcloud functions describe translationCallbackCall
    

    이후 단계에서 사용할 수 있도록 반환된 URL을 기록해 둡니다.

워크플로 배포

워크플로 정의는 YAML 또는 JSON 형식으로 작성할 수 있는 Workflows 구문을 사용하여 기술되는 일련의 단계들로 구성됩니다. 이것이 워크플로의 정의입니다. 워크플로를 만든 후 실행에 사용할 수 있도록 워크플로를 배포합니다.

  1. callback-translation 디렉터리로 변경합니다.

    cd ..
    
  2. 다음 콘텐츠가 포함된 파일 이름이 translation-validation.yaml인 텍스트 파일을 만듭니다.

    main:
        params: [translation_request]
        steps:
            - log_request:
                call: sys.log
                args:
                    text: ${translation_request}
            - vars:
                assign:
                    - exec_id: ${sys.get_env("GOOGLE_CLOUD_WORKFLOW_EXECUTION_ID")}
                    - text_to_translate: ${translation_request.text}
                    - database_root: ${"projects/" + sys.get_env("GOOGLE_CLOUD_PROJECT_ID") + "/databases/(default)/documents/translations/"}
            - log_translation_request:
                call: sys.log
                args:
                    text: ${text_to_translate}
    
            - store_translation_request:
                call: googleapis.firestore.v1.projects.databases.documents.patch
                args:
                    name: ${database_root + exec_id}
                    updateMask:
                        fieldPaths: ['text']
                    body:
                        fields:
                            text:
                                stringValue: ${text_to_translate}
                result: store_translation_request_result
    
            - translate:
                call: googleapis.translate.v2.translations.translate
                args:
                    query:
                        q: ${text_to_translate}
                        target: "FR"
                        format: "text"
                        source: "EN"
                result: translation_result
            - assign_translation:
                assign:
                    - translation: ${translation_result.data.translations[0].translatedText}
            - log_translation_result:
                call: sys.log
                args:
                    text: ${translation}
    
            - store_translated_text:
                call: googleapis.firestore.v1.projects.databases.documents.patch
                args:
                    name: ${database_root + exec_id}
                    updateMask:
                        fieldPaths: ['translation']
                    body:
                        fields:
                            translation:
                                stringValue: ${translation}
                result: store_translation_request_result
    
            - create_callback:
                call: events.create_callback_endpoint
                args:
                    http_callback_method: "POST"
                result: callback_details
            - log_callback_details:
                call: sys.log
                args:
                    text: ${callback_details}
    
            - store_callback_details:
                call: googleapis.firestore.v1.projects.databases.documents.patch
                args:
                    name: ${database_root + exec_id}
                    updateMask:
                        fieldPaths: ['callback']
                    body:
                        fields:
                            callback:
                                stringValue: ${callback_details.url}
                result: store_callback_details_result
    
            - await_callback:
                call: events.await_callback
                args:
                    callback: ${callback_details}
                    timeout: 3600
                result: callback_request
            - assign_approval:
                assign:
                    - approved: ${callback_request.http_request.body.approved}
    
            - store_approval:
                call: googleapis.firestore.v1.projects.databases.documents.patch
                args:
                    name: ${database_root + exec_id}
                    updateMask:
                        fieldPaths: ['approved']
                    body:
                        fields:
                            approved:
                                booleanValue: ${approved}
                result: store_approval_result
    
            - return_outcome:
                return:
                    text: ${text_to_translate}
                    translation: ${translation}
                    approved: ${approved}
  3. 워크플로를 만든 후 배포해도 되지만 워크플로를 실행하지는 마세요.

    gcloud workflows deploy translation_validation --source=translation-validation.yaml
    

웹 앱 만들기

워크플로 실행을 시작하는 Cloud 함수를 호출하는 웹 앱을 만듭니다. 웹페이지가 실시간으로 업데이트되어 번역 요청 결과를 표시합니다.

  1. public 디렉터리로 변경합니다.

    cd public
    
  2. 다음 HTML 마크업이 포함된 파일 이름 index.html인 텍스트 파일을 만듭니다. (이후 단계에서 index.html 파일을 수정하고 Firebase SDK 스크립트를 추가합니다.)

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
    
        <title>Frenglish translation — Feature Workflows callbacks</title>
    
        <link rel="stylesheet"
            href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.42/dist/themes/base.css">
        <script type="module"
            src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.42/dist/shoelace.js"></script>
        <link rel="stylesheet" href="./style.css">
    </head>
    
    <body>
        <h1>Translate from English to French</h1>
    
        <sl-form class="form-overview">
            <sl-textarea id="text" placeholder="The quick brown fox jumps over the lazy dog."
                label="English text to translate"></sl-textarea>
            <p></p>
            <sl-button id="translateBtn" type="primary">Translate</sl-button>
            <p></p>
            <sl-alert id="translation" type="primary">
                Le rapide renard brun saute au dessus du chien paresseux.
            </sl-alert>
            <p></p>
            <div id="buttonRow" style="display: none;">
                <sl-button id="validateBtn" type="success">Validate</sl-button>
                <sl-button id="rejectBtn" type="danger">Reject</sl-button>
            </div>
            <p></p>
            <sl-alert id="validationAlert" type="success">
                <sl-icon slot="icon" name="check2-circle"></sl-icon>
                <strong>The translation has been validated</strong><br>
                Glad that you liked our translation! We'll save it in our database.
            </sl-alert>
            <sl-alert id="rejectionAlert" type="danger">
                <sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
                <strong>The translation has been rejected</strong><br>
                A pity the translation isn't good! We'll do better next time!
            </sl-alert>
            <p></p>
            <sl-button id="newBtn" style="display: none;" type="primary">New translation</sl-button>
        </sl-form>
    
        <script src="https://www.gstatic.com/firebasejs/8.6.3/firebase-app.js"></script>
        <script src="https://www.gstatic.com/firebasejs/8.6.3/firebase-firestore.js"></script>
    
        <script>
            var firebaseConfig = {
                apiKey: "XXXX",
                authDomain: "XXXX",
                projectId: "XXXX",
                storageBucket: "XXXX",
                messagingSenderId: "XXXX",
                appId: "XXXX",
                measurementId: "XXXX"
            };
            // Initialize Firebase
            firebase.initializeApp(firebaseConfig);
        </script>
        <script src="./script.js" type="module"></script>
    </body>
    
    </html>
    
  3. 다음 JavaScript 코드가 포함된 파일 이름이 script.js인 텍스트 파일을 만듭니다.

    document.addEventListener("DOMContentLoaded", async function (event) {
        const textArea = document.getElementById("text");
        textArea.focus();
    
        const newBtn = document.getElementById("newBtn");
        newBtn.addEventListener("sl-focus", event => {
            event.target.blur();
            window.location.reload();
        });
    
        const translationAlert = document.getElementById("translation");
        const buttonRow = document.getElementById("buttonRow");
    
        var callbackUrl = "";
    
        const validationAlert = document.getElementById("validationAlert");
        const rejectionAlert = document.getElementById("rejectionAlert");
        const validateBtn = document.getElementById("validateBtn");
        const rejectBtn = document.getElementById("rejectBtn");
    
        const translateBtn = document.getElementById("translateBtn");
        translateBtn.addEventListener("sl-focus", async event => {
            event.target.disabled = true;
            event.target.loading = true;
            textArea.disabled = true;
    
            console.log("Text to translate = ", textArea.value);
    
            const fnUrl = UPDATE_ME;
    
            try {
                console.log("Calling workflow executor function...");
                const resp = await fetch(fnUrl, {
                    method: "POST",
                    headers: {
                        "accept": "application/json",
                        "content-type": "application/json"
                    },
                    body: JSON.stringify({ text: textArea.value })
                });
                const executionResp = await resp.json();
                const executionId = executionResp.executionId.slice(-36);
                console.log("Execution ID = ", executionId);
    
                const db = firebase.firestore();
                const translationDoc = db.collection("translations").doc(executionId);
    
                var translationReceived = false;
                var callbackReceived =  false;
                var approvalReceived = false;
                translationDoc.onSnapshot((doc) => {
                    console.log("Firestore update", doc.data());
                    if (doc.data()) {
                        if ("translation" in doc.data()) {
                            if (!translationReceived) {
                                console.log("Translation = ", doc.data().translation);
                                translationReceived = true;
                                translationAlert.innerText = doc.data().translation;
                                translationAlert.open = true;
                            }
                        }
                        if ("callback" in doc.data()) {
                            if (!callbackReceived) {
                                console.log("Callback URL = ", doc.data().callback);
                                callbackReceived = true;
                                callbackUrl = doc.data().callback;
                                buttonRow.style.display = "block";
                            }
                        }
                        if ("approved" in doc.data()) {
                            if (!approvalReceived) {
                                const approved = doc.data().approved;
                                console.log("Approval received = ", approved);
                                if (approved) {
                                    validationAlert.open = true;
                                    buttonRow.style.display = "none";
                                    newBtn.style.display = "inline-block";
                                } else {
                                    rejectionAlert.open = true;
                                    buttonRow.style.display = "none";
                                    newBtn.style.display = "inline-block";
                                }
                                approvalReceived = true;
                            }
                        }
                    }
                });
            } catch (e) {
                console.log(e);
            }
            event.target.loading = false;
        });
    
        validateBtn.addEventListener("sl-focus", async event => {
            validateBtn.disabled = true;
            rejectBtn.disabled = true;
            validateBtn.loading = true;
            validateBtn.blur();
    
            // call callback
            await callCallbackUrl(callbackUrl, true);
        });
    
        rejectBtn.addEventListener("sl-focus", async event => {
            rejectBtn.disabled = true;
            validateBtn.disabled = true;
            rejectBtn.loading = true;
            rejectBtn.blur();
    
            // call callback
            await callCallbackUrl(callbackUrl, false);
        });
    
    });
    
    async function callCallbackUrl(url, approved) {
        console.log("Calling callback URL with status = ", approved);
    
        const fnUrl = UPDATE_ME;
        try {
            const resp = await fetch(fnUrl, {
                method: "POST",
                headers: {
                    "accept": "application/json",
                    "content-type": "application/json"
                },
                body: JSON.stringify({ url, approved })
            });
            const result = await resp.json();
            console.log("Callback answer = ", result);
        } catch(e) {
            console.log(e);
        }
    }
  4. script.js 파일을 수정하여 UPDATE_ME 자리표시자를 이전에 기록해 둔 Cloud 함수 URL로 바꿉니다.

    1. translateBtn.addEventListener 메서드에서 const fnUrl = UPDATE_ME;를 다음으로 바꿉니다.

      const fnUrl = "https://REGION-PROJECT_ID.cloudfunctions.net/invokeTranslationWorkflow";

    2. callCallbackUrl 함수에서 const fnUrl = UPDATE_ME;를 다음으로 바꿉니다.

      const fnUrl = "https://REGION-PROJECT_ID.cloudfunctions.net/translationCallbackCall";

  5. 다음 캐스케이딩 스타일이 포함된 파일 이름이 style.css인 텍스트 파일을 만듭니다.

    * {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    }
    
    body {
        margin: 20px;
    }
    
    h1, h2, h3, h4 {
        color: #0ea5e9;
    }
    

웹 앱에 Firebase 추가

이 튜토리얼에서는 Firebase 호스팅을 사용해 HTML 페이지, JavaScript 스크립트, CSS 스타일 시트를 정적 애셋으로 배포하지만 이를 어디서나 호스팅할 수 있으며 테스트 목적으로 자체 머신에서 로컬로 제공할 수 있습니다.

Firebase 프로젝트 만들기

Firebase를 앱에 추가하려면 먼저 앱에 연결할 Firebase 프로젝트를 만드세요.

  1. Firebase Console에서 프로젝트 만들기를 클릭한 후 드롭다운 메뉴에서 기존 Google Cloud 프로젝트를 선택하여 프로젝트에 Firebase 리소스를 추가합니다.

  2. Firebase 추가 옵션이 표시될 때까지 계속을 클릭합니다.

  3. 프로젝트의 Google 애널리틱스 설정은 건너뛰세요.

  4. Firebase 추가를 클릭합니다.

Firebase에서 Firebase 프로젝트용 리소스를 자동으로 프로비저닝합니다. 프로세스가 완료되면 Firebase Console에서 프로젝트의 개요 페이지로 이동하게 됩니다.

Firebase에 앱 등록

Firebase 프로젝트가 준비되었으면 웹 앱을 추가할 수 있습니다.

  1. 프로젝트 개요 페이지 중앙에 있는 아이콘(</>)을 클릭하여 설정 워크플로를 시작합니다.

  2. 앱의 닉네임을 입력합니다.

    닉네임은 Firebase Console에서 본인만 볼 수 있습니다.

  3. 지금은 Firebase 호스팅 설정을 건너뜁니다.

  4. 앱 등록을 클릭하고 Console로 이동합니다.

Cloud Firestore 사용 설정

웹 앱은 Cloud Firestore를 사용하여 데이터를 수신하고 저장합니다. Cloud Firestore를 사용 설정해야 합니다.

  1. Firebase Console의 빌드 섹션에서 Firestore 데이터베이스를 클릭합니다.

    (먼저 왼쪽 탐색창을 펼쳐야 빌드 섹션이 표시될 수 있습니다.)

  2. Cloud Firestore 창에서 데이터베이스 만들기를 클릭합니다.

  3. 다음과 같은 보안 규칙을 사용하여 테스트 모드에서 시작을 선택합니다.

    rules_version = '2';
    service cloud.firestore {
    match /databases/{database}/documents {
      match /{document=**} {
        allow read, write;
      }
    }
    }
    
  4. 보안 규칙에 대한 면책조항을 읽은 후 다음을 클릭합니다.

  5. Cloud Firestore 데이터가 저장되는 위치를 설정합니다. 기본값을 수락하거나 가까운 리전을 선택할 수 있습니다.

  6. 사용 설정을 클릭하여 Firestore를 프로비저닝합니다.

Firebase SDK 추가 및 Firebase 초기화

Firebase는 대부분의 Firebase 제품에 사용할 수 있는 자바스크립트 라이브러리를 제공합니다. Firebase 호스팅을 사용하기 전에 웹 앱에 Firebase SDK를 추가해야 합니다.

  1. 앱에서 Firebase를 초기화하려면 앱의 Firebase 프로젝트 구성을 제공해야 합니다.
    1. Firebase Console에서 프로젝트 설정 으로 이동합니다.
    2. 내 앱 창에서 앱을 선택합니다.
    3. CDN에서 Firebase SDK 라이브러리를 로드하려면 SDK 설정 및 구성 창에서 CDN을 선택합니다.
    4. index.html 파일의 <body> 태그 하단에 스니펫을 복사하고 XXXX 자리표시자 값을 바꿉니다.
  2. Firebase 자바스크립트 SDK를 설치합니다.

    1. 아직 package.json 파일이 없으면 callback-translation 디렉터리에서 다음 명령어를 실행하여 파일을 만듭니다.

      npm init
    2. 다음 명령어를 실행하여 firebase npm 패키지를 설치하고 package.json 파일에 저장합니다.

      npm install --save firebase

프로젝트 초기화 및 배포

로컬 프로젝트 파일을 Firebase 프로젝트에 연결하려면 프로젝트를 초기화해야 합니다. 그런 다음 웹 앱을 배포할 수 있습니다.

  1. callback-translation 디렉터리에서 다음 명령어를 실행합니다.

    firebase init
  2. Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys 옵션을 선택합니다.

  3. 기존 프로젝트를 사용하도록 선택하고 프로젝트 ID를 입력합니다.

  4. public을 기본 공개 루트 디렉터리로 허용합니다.

  5. 단일 페이지 앱을 구성하도록 선택합니다.

  6. GitHub를 사용한 자동 빌드 및 배포 설정은 건너뜁니다.

  7. File public/index.html already exists. Overwrite? 프롬프트에서 No를 입력합니다.

  8. public 디렉터리로 변경합니다.

    cd public
  9. public 디렉터리에서 다음 명령어를 실행하여 프로젝트를 사이트에 배포합니다.

    firebase deploy --only hosting

로컬에서 웹 앱 테스트

Firebase 호스팅에서는 변경사항을 로컬로 확인 및 테스트하고 에뮬레이션된 백엔드 프로젝트 리소스와 상호작용할 수 있습니다. firebase serve를 사용하면 호스팅 콘텐츠 및 구성에 대해 앱이 에뮬레이션된 백엔드와 상호작용하지만 다른 모든 프로젝트 리소스에 대해서는 실제 백엔드와 상호작용합니다. 이 튜토리얼에서는 firebase serve를 사용해도 되지만 더 광범위한 테스트에는 사용이 권장되지 않습니다.

  1. public 디렉터리에서 다음 명령어를 실행합니다.

    firebase serve
  2. 반환된 로컬 URL(일반적으로 http://localhost:5000)에서 웹 앱을 엽니다.

  3. 영어로 텍스트를 입력한 후 번역을 클릭합니다.

    텍스트의 프랑스어 번역이 표시됩니다.

  4. 이제 유효성 검사 또는 거부를 클릭하면 됩니다.

    Firestore 데이터베이스에서 콘텐츠를 검증할 수 있습니다. 다음과 유사합니다.

    approved: true
    callback: "https://workflowexecutions.googleapis.com/v1/projects/26811016474/locations/us-central1/workflows/translation_validation/executions/68bfce75-5f62-445f-9cd5-eda23e6fa693/callbacks/72851c97-6bb2-45e3-9816-1e3dcc610662_1a16697f-6d90-478d-9736-33190bbe222b"
    text: "The quick brown fox jumps over the lazy dog."
    translation: "Le renard brun rapide saute par-dessus le chien paresseux."
    

    approved 상태는 번역 검증 또는 거부 여부에 따라 true 또는 false 값입니다.

수고하셨습니다 Workflows 콜백을 사용하는 인간 참여형(Human-In-The-Loop) 번역 워크플로가 생성되었습니다.

삭제

이 튜토리얼용으로 새 프로젝트를 만든 경우 이 프로젝트를 삭제합니다. 기존 프로젝트를 사용한 경우 이 튜토리얼에 추가된 변경사항은 제외하고 보존하려면 튜토리얼용으로 만든 리소스를 삭제합니다.

프로젝트 삭제

비용이 청구되지 않도록 하는 가장 쉬운 방법은 튜토리얼에서 만든 프로젝트를 삭제하는 것입니다.

프로젝트를 삭제하는 방법은 다음과 같습니다.

  1. Google Cloud 콘솔에서 리소스 관리 페이지로 이동합니다.

    리소스 관리로 이동

  2. 프로젝트 목록에서 삭제할 프로젝트를 선택하고 삭제를 클릭합니다.
  3. 대화상자에서 프로젝트 ID를 입력한 후 종료를 클릭하여 프로젝트를 삭제합니다.

튜토리얼 리소스 삭제

  1. 튜토리얼 설정 중에 추가한 gcloud CLI 기본 구성을 삭제합니다.

    gcloud config unset workflows/location
    
  2. 이 튜토리얼에서 만든 워크플로를 삭제합니다.

    gcloud workflows delete WORKFLOW_NAME
    
  3. 이 튜토리얼에서 만든 Cloud 함수를 삭제합니다.

    gcloud functions delete FUNCTION_NAME
    

    Google Cloud Console에서 Cloud Functions를 삭제할 수도 있습니다.

다음 단계