Node.js를 사용한 백그라운드 처리


많은 앱은 웹 요청의 컨텍스트 외부에서 백그라운드 처리를 해야 합니다. 이 가이드에서는 사용자가 번역할 텍스트를 입력하고 이전 번역 목록을 표시하는 웹 앱을 만듭니다. 사용자 요청이 차단되지 않도록 번역이 백그라운드 프로세스로 수행됩니다.

다음 다이어그램은 번역 요청 프로세스를 보여줍니다.

아키텍처 다이어그램입니다.

다음은 가이드 앱 작동 방법을 보여주는 이벤트 시퀀스입니다.

  1. 웹 페이지를 방문하여 Firestore에 저장된 이전 번역 목록을 확인합니다.
  2. HTML 양식을 입력하여 텍스트 번역을 요청합니다.
  3. 번역 요청이 Pub/Sub에 게시됩니다.
  4. 해당 Pub/Sub 주제에 구독된 Cloud 함수가 트리거됩니다.
  5. Cloud 함수가 Cloud Translation을 사용하여 텍스트를 번역합니다.
  6. Cloud 함수가 결과를 Firestore에 저장합니다.

이 가이드는 Google Cloud를 사용한 백그라운드 처리에 관심이 있는 모든 사용자를 대상으로 합니다. Pub/Sub, Firestore, App Engine, Cloud Functions와 관련된 사전 경험은 필요하지 않습니다. 하지만 모든 코드를 이해하기 위해서는 Node.js, 자바스크립트, HTML 관련 경험이 도움이 됩니다.

목표

  • Cloud 함수를 이해하고 배포합니다.
  • App Engine 앱을 이해하고 배포합니다.
  • 앱을 사용해봅니다.

비용

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

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

이 문서에 설명된 태스크를 완료했으면 만든 리소스를 삭제하여 청구가 계속되는 것을 방지할 수 있습니다. 자세한 내용은 삭제를 참조하세요.

시작하기 전에

  1. Google Cloud 계정에 로그인합니다. Google Cloud를 처음 사용하는 경우 계정을 만들고 Google 제품의 실제 성능을 평가해 보세요. 신규 고객에게는 워크로드를 실행, 테스트, 배포하는 데 사용할 수 있는 $300의 무료 크레딧이 제공됩니다.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

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

  4. Enable the Firestore, Cloud Functions, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

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

  7. Enable the Firestore, Cloud Functions, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  8. Google Cloud 콘솔의 Cloud Shell에서 앱을 엽니다.

    Cloud Shell로 이동

    Cloud Shell을 사용하면 브라우저에서 직접 명령줄을 통해 클라우드 리소스에 액세스할 수 있습니다. 브라우저에서 Cloud Shell을 열고 계속을 클릭하여 샘플 코드를 다운로드하고 앱 디렉터리로 변경합니다.

  9. Google Cloud 프로젝트를 사용하도록 Cloud Shell에서 gcloud 도구를 구성합니다.
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

Cloud 함수 이해

  • 이 함수는 Firestore 및 Translation과 같은 일부 종속 항목 가져오기를 시작합니다. 함수를 호출할 때 재사용할 수 있도록 전역 Firestore 및 Translation 클라이언트가 초기화됩니다. 이렇게 하면 함수를 호출할 때마다 새 클라이언트를 초기화할 필요가 없으므로, 실행 시간이 빨라집니다.

    const Firestore = require('@google-cloud/firestore');
    const {Translate} = require('@google-cloud/translate').v2;
    
    const firestore = new Firestore();
    const translate = new Translate();
  • Translation API는 문자열을 선택된 언어로 번역합니다.

    const [
      translated,
      {
        data: {translations},
      },
    ] = await translate.translate(original, language);
    const originalLanguage = translations[0].detectedSourceLanguage;
    console.log(
      `Translated ${original} in ${originalLanguage} to ${translated} in ${language}.`
    );
  • Cloud 함수는 Pub/Sub 메시지를 파싱하여 번역할 텍스트와 원하는 도착어를 가져옵니다.

    그런 후 앱이 번역을 요청하고 결과를 Firestore에 저장합니다.

    // translate translates the given message and stores the result in Firestore.
    // Triggered by Pub/Sub message.
    exports.translate = async (pubSubEvent) => {
      const {language, original} = JSON.parse(
        Buffer.from(pubSubEvent.data, 'base64').toString()
      );
    
      const [
        translated,
        {
          data: {translations},
        },
      ] = await translate.translate(original, language);
      const originalLanguage = translations[0].detectedSourceLanguage;
      console.log(
        `Translated ${original} in ${originalLanguage} to ${translated} in ${language}.`
      );
    
      // Store translation in firestore.
      await firestore.collection('translations').doc().set({
        language,
        original,
        translated,
        originalLanguage,
      });
    };

Cloud 함수 배포

  • translate.js 파일과 같은 디렉터리에서 Pub/Sub 트리거로 Cloud 함수를 배포합니다.

    gcloud functions deploy translate --runtime nodejs10 --trigger-topic translate

앱 이해

웹 앱에는 두 가지 주요 구성요소가 있습니다.

  • 하나는 웹 요청 처리를 위한 Node.js HTTP 서버입니다. 이 서버에는 다음 두 가지 엔드포인트가 있습니다.
    • /: 기존 번역을 모두 나열하고 새 번역을 요청하기 위해 사용자가 제출할 수 있는 양식을 표시합니다.
    • /request-translation: 양식 제출은 이 엔드포인트로 전송됩니다. 이 엔드포인트는 비동기적으로 번역되도록 요청을 Pub/Sub에 게시합니다.
  • 다른 하나는 Node.js 서버가 기존 번역으로 채우는 HTML 템플릿입니다.

HTTP 서버

  • server 디렉터리에서 앱을 설정하고 HTTP 핸들러를 등록하여 app.js가 시작됩니다.

    
    // This app is an HTTP app that displays all previous translations
    // (stored in Firestore) and has a form to request new translations. On form
    // submission, the request is sent to Pub/Sub to be processed in the background.
    
    // TOPIC_NAME is the Pub/Sub topic to publish requests to. The Cloud Function to
    // process translation requests should be subscribed to this topic.
    const TOPIC_NAME = 'translate';
    
    const express = require('express');
    const bodyParser = require('body-parser');
    const {PubSub} = require('@google-cloud/pubsub');
    const {Firestore} = require('@google-cloud/firestore');
    
    const app = express();
    const port = process.env.PORT || 8080;
    
    const firestore = new Firestore();
    
    const pubsub = new PubSub();
    const topic = pubsub.topic(TOPIC_NAME);
    
    // Use handlebars.js for templating.
    app.set('views', __dirname);
    app.set('view engine', 'html');
    app.engine('html', require('hbs').__express);
    
    app.use(bodyParser.urlencoded({extended: true}));
    
    app.get('/', index);
    app.post('/request-translation', requestTranslation);
    app.listen(port, () => console.log(`Listening on port ${port}!`));
    
  • 색인 핸들러 /는 Firestore에서 모든 기존 번역을 가져오고 HTML 템플릿을 목록으로 채웁니다.

    
    // index lists the current translations.
    async function index(req, res) {
      const translations = [];
      const querySnapshot = await firestore.collection('translations').get();
      querySnapshot.forEach((doc) => {
        console.log(doc.id, ' => ', doc.data());
        translations.push(doc.data());
      });
    
      res.render('index', {translations});
    }
    
  • 새 번역은 HTML 양식을 제출하여 요청됩니다. /request-translation에 등록된 번역 요청 핸들러는 양식 제출을 파싱하고, 요청을 검증하고, 메시지를 Pub/Sub에 게시합니다.

    
    // requestTranslation parses the request, validates it, and sends it to Pub/Sub.
    function requestTranslation(req, res) {
      const language = req.body.lang;
      const original = req.body.v;
    
      const acceptableLanguages = ['de', 'en', 'es', 'fr', 'ja', 'sw'];
      if (!acceptableLanguages.includes(language)) {
        throw new Error(`Invalid language ${language}`);
      }
    
      console.log(`Translation requested: ${original} -> ${language}`);
    
      const buffer = Buffer.from(JSON.stringify({language, original}));
      topic.publish(buffer);
      res.sendStatus(200);
    }

HTML 템플릿

HTML 템플릿은 이전 번역을 보고 새 번역을 요청할 수 있도록 사용자에게 표시되는 HTML 페이지의 기본 페이지입니다. HTTP 서버가 이 템플릿에 기존 번역 목록을 채웁니다.

  • HTML 템플릿의 <head> 요소에는 해당 페이지의 메타데이터, 스타일 시트, 자바스크립트가 포함됩니다.
    <html>
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Translations</title>
    
        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
        <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
        <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script>
            $(document).ready(function() {
                $("#translate-form").submit(function(e) {
                    e.preventDefault();
                    // Get value, make sure it's not empty.
                    if ($("#v").val() == "") {
                        return;
                    }
                    $.ajax({
                        type: "POST",
                        url: "/request-translation",
                        data: $(this).serialize(),
                        success: function(data) {
                            // Show snackbar.
                            console.log(data);
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--red-100");
                            $("#snackbar").addClass("mdl-color--green-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation requested'
                            });
                        },
                        error: function(data) {
                            // Show snackbar.
                            console.log("Error requesting translation");
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--green-100");
                            $("#snackbar").addClass("mdl-color--red-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation request failed'
                            });
                        }
                    });
                });
            });
        </script>
        <style>
            .lang {
                width: 50px;
            }
            .translate-form {
                display: inline;
            }
        </style>
    </head>

    이 페이지는 Material Design Lite(MDL) CSS 및 자바스크립트 애셋을 확인합니다. MDL을 사용하면 머터리얼 디자인 스타일과 느낌을 웹 사이트에 추가할 수 있습니다.

    이 페이지는 JQuery를 사용하여 문서 로드가 완료될 때까지 기다리고 양식 제출 핸들러를 설정합니다. 번역 요청 양식이 제출될 때마다 페이지가 최소 양식 검증을 수행하여 값이 비어 있지 않은지 확인한 후 비동기 요청을 /request-translation 엔드포인트로 전송합니다.

    마지막으로 요청이 성공했는지 또는 오류가 발생했는지를 나타내는 MDL 스낵바가 표시됩니다.

  • 페이지의 HTML 본문은 MDL 레이아웃 및 여러 MDL 구성요소를 사용하여 번역 목록과 추가 번역 요청 양식을 표시합니다.
    <body>
        <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
            <header class="mdl-layout__header">
                <div class="mdl-layout__header-row">
                    <!-- Title -->
                    <span class="mdl-layout-title">Translate with Background Processing</span>
                </div>
            </header>
            <main class="mdl-layout__content">
                <div class="page-content">
                    <div class="mdl-grid">
                    <div class="mdl-cell mdl-cell--1-col"></div>
                        <div class="mdl-cell mdl-cell--3-col">
                            <form id="translate-form" class="translate-form">
                                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                                    <input class="mdl-textfield__input" type="text" id="v" name="v">
                                    <label class="mdl-textfield__label" for="v">Text to translate...</label>
                                </div>
                                <select class="mdl-textfield__input lang" name="lang">
                                    <option value="de">de</option>
                                    <option value="en">en</option>
                                    <option value="es">es</option>
                                    <option value="fr">fr</option>
                                    <option value="ja">ja</option>
                                    <option value="sw">sw</option>
                                </select>
                                <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent" type="submit"
                                    name="submit">Submit</button>
                            </form>
                        </div>
                        <div class="mdl-cell mdl-cell--8-col">
                            <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
                                <thead>
                                    <tr>
                                        <th class="mdl-data-table__cell--non-numeric"><strong>Original</strong></th>
                                        <th class="mdl-data-table__cell--non-numeric"><strong>Translation</strong></th>
                                    </tr>
                                </thead>
                                <tbody>
                                    {{#each translations}}
                                    <tr>
                                        <td class="mdl-data-table__cell--non-numeric">
                                            <span class="mdl-chip mdl-color--primary">
                                                <span class="mdl-chip__text mdl-color-text--white">{{ originalLanguage }} </span>
                                            </span>
                                        {{ original }}
                                        </td>
                                        <td class="mdl-data-table__cell--non-numeric">
                                            <span class="mdl-chip mdl-color--accent">
                                                <span class="mdl-chip__text mdl-color-text--white">{{ language }} </span>
                                            </span>
                                            {{ translated }}
                                        </td>
                                    </tr>
                                    {{/each}}
                                </tbody>
                            </table>
                            <br/>
                            <button class="mdl-button mdl-js-button mdl-button--raised" type="button" onClick="window.location.reload();">
                                Refresh
                            </button>
                        </div>
                    </div>
                </div>
                <div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-snackbar mdl-js-snackbar" id="snackbar">
                    <div class="mdl-snackbar__text mdl-color-text--black"></div>
                    <button type="button" class="mdl-snackbar__action"></button>
                </div>
            </main>
        </div>
    </body>
    
    </html>

웹 앱 배포

App Engine 표준 환경을 사용하면 많은 양의 데이터를 사용해 작업하고 과도한 부하가 발생하는 애플리케이션을 쉽게 빌드하고 배포하며 안정적으로 실행할 수 있습니다.

이 가이드에서는 App Engine 표준 환경을 사용하여 HTTP 프런트엔드를 배포합니다.

app.yaml은 App Engine 앱을 구성합니다.

runtime: nodejs10
  • app.yaml 파일과 동일한 디렉터리에서 App Engine 표준 환경에 앱을 배포합니다.
    gcloud app deploy

앱 테스트

Cloud 함수 및 App Engine 앱을 배포한 후 번역 요청을 시도합니다.

  1. 브라우저에서 앱을 보려면 다음 URL을 입력합니다.

    https://PROJECT_ID.REGION_ID.r.appspot.com

    다음을 바꿉니다.

    빈 번역 목록과 새 번역 요청 양식이 포함된 페이지가 있습니다.

  2. 번역 할 텍스트 필드에 번역할 텍스트를 입력합니다(예: Hello, World).
  3. 드롭다운 목록에서 텍스트를 번역할 언어를 선택합니다.
  4. 제출을 클릭합니다.
  5. 페이지를 새로 고침하려면 새로 고침을 클릭합니다. 번역 목록에 새 행이 있습니다. 번역이 표시되지 않으면 몇 초 후 다시 시도합니다. 그래도 번역이 표시되지 않으면 앱 디버깅에 대한 다음 섹션을 참조하세요.

앱 디버깅

App Engine 앱에 연결할 수 없거나 새 번역이 표시되지 않으면 다음을 확인합니다.

  1. gcloud 배포 명령어가 성공적으로 완료되었고 오류를 출력하지 않았는지 확인합니다. 오류가 있으면 수정하고, Cloud 함수App Engine 앱 배포를 다시 시도합니다.
  2. Google Cloud 콘솔에서 로그 뷰어 페이지로 이동합니다.

    로그 뷰어 페이지로 이동
    1. 최근에 선택한 리소스 드롭다운 목록에서 GAE 애플리케이션을 클릭하고 전체 module_id를 클릭합니다. 앱을 방문했을 때의 요청 목록이 표시됩니다. 요청 목록이 표시되지 않는 경우 드롭다운 목록에서 전체 module_id를 선택했는지 확인합니다. Google Cloud 콘솔에 오류 메시지가 출력되는 것이 보이면 앱 이해 관련 섹션에 있는 코드와 앱 코드가 일치하는지 확인합니다.
    2. 최근에 선택한 리소스 드롭다운 목록에서 Cloud 함수를 클릭하고 전체 함수 이름을 클릭합니다. 요청된 각 번역마다 나열된 함수가 보입니다. 그렇지 않으면 Cloud 함수 및 App Engine 앱이 동일한 Pub/Sub 주제를 사용 중인지 확인합니다.
      • background/server/app.js 파일에서 TOPIC_NAME 상수가 "translate"인지 확인합니다.
      • Cloud 함수를 배포할 때, --trigger-topic=translate 플래그를 포함합니다.

삭제

이 튜토리얼에서 사용된 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 리소스가 포함된 프로젝트를 삭제하거나 프로젝트를 유지하고 개별 리소스를 삭제하세요.

Cloud 프로젝트 삭제

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

App Engine 인스턴스 삭제

  1. In the Google Cloud console, go to the Versions page for App Engine.

    Go to Versions

  2. Select the checkbox for the non-default app version that you want to delete.
  3. 앱 버전을 삭제하려면 삭제를 클릭합니다.

Cloud 함수 삭제

  • 이 가이드에서 만든 Cloud 함수를 삭제합니다.
    gcloud functions delete Translate

다음 단계