서버리스 웹 API 워크숍

1. 개요

이 Codelab의 목표는 Google Cloud Platform에서 제공하는 '서버리스' 경험을 얻는 것입니다.

  • Cloud Functions: 다양한 이벤트 (Pub/Sub 메시지, Cloud Storage의 새 파일, HTTP 요청 등)에 반응하는 함수 형태의 작은 비즈니스 로직 배포
  • App Engine - 빠른 확장 및 축소 기능을 갖춘 웹 앱, 웹 API, 모바일 백엔드, 정적 애셋을 배포하고 제공하는 서비스
  • Cloud Run — 모든 언어, 런타임 또는 라이브러리를 포함할 수 있는 컨테이너를 배포하고 확장하기 위한 용도입니다.

그리고 서버리스 서비스를 활용하여 웹 및 REST API를 배포하고 확장하는 동시에 훌륭한 RESTful 디자인 원칙을 확인하는 방법도 알아보세요.

이 워크숍에서는 다음과 같은 도구로 구성된 북마크 탐색기를 만들어 보겠습니다.

  • Cloud 함수: Cloud Firestore 문서 데이터베이스에서 라이브러리에 제공되는 도서의 초기 데이터 세트를 가져오는 방법
  • Cloud Run 컨테이너: 데이터베이스의 콘텐츠를 통해 REST API를 노출합니다.
  • App Engine 웹 프런트엔드: REST API를 호출하여 도서 목록을 탐색합니다.

이 Codelab을 마치면 웹 프런트엔드가 다음과 같이 표시됩니다.

b6964f26b9624565.png

과정 내용

  • Cloud Functions
  • Cloud Firestore
  • Cloud Run
  • App Engine

2 설정 및 요건

자습형 환경 설정

  1. Cloud Console에 로그인하고 새 프로젝트를 만들거나 기존 프로젝트를 다시 사용합니다. (아직 Gmail 또는 Google Workspace 계정이 없는 경우 계정을 만들어야 합니다.)

9,699c957bc475304.png

b9a10ebdf5b5a447.png

A1e3c01a38fa61c2.png

모든 Google Cloud 프로젝트에서 고유한 이름인 프로젝트 ID를 기억하세요(위의 이름은 이미 사용되었으므로 사용할 수 없습니다). 이 ID는 나중에 이 Codelab에서 PROJECT_ID라고 부릅니다.

  1. 그런 후 Google Cloud 리소스를 사용할 수 있도록 Cloud Console에서 결제를 사용 설정해야 합니다.

이 Codelab 실행에는 많은 비용이 들지 않습니다. 이 가이드를 마친 후 비용이 결제되지 않도록 리소스 종료 방법을 알려주는 '삭제' 섹션의 안내를 따르세요. Google Cloud 신규 사용자는 $300 USD 무료 체험 프로그램을 이용할 수 있습니다.

Cloud Shell 시작

Google Cloud를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 Cloud에서 실행되는 명령줄 환경인 Google Cloud Shell을 사용합니다.

GCP 콘솔에서 오른쪽 상단 툴바의 Cloud Shell 아이콘을 클릭합니다.

bce75f34b2c53987.png

환경을 프로비저닝하고 연결하는 데 몇 분 정도 소요됩니다. 완료되면 다음과 같이 표시됩니다.

f6ef2b5f13479f3a.png

가상 머신에는 필요한 개발 도구가 모두 들어있습니다. 영구적인 5GB 홈 디렉토리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 실습의 모든 작업은 브라우저만으로 수행할 수 있습니다.

3. 환경 준비 및 Cloud API 사용 설정

이 프로젝트 전체에서 필요한 다양한 서비스를 사용하기 위해 몇 가지 API를 사용 설정할 것입니다. Cloud Shell에서 다음 명령어를 실행하여 이 작업을 수행합니다.

$ gcloud services enable \
      appengine.googleapis.com \
      cloudbuild.googleapis.com \
      cloudfunctions.googleapis.com \
      compute.googleapis.com \
      firestore.googleapis.com \
      run.googleapis.com

잠시 후 작업이 성공적으로 완료됩니다.

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

이 과정에서 필요한 환경 변수, 즉 함수, 앱, 컨테이너를 배포할 클라우드 변수도 설정할 것입니다.

$ export REGION=europe-west3

Cloud Firestore 데이터베이스에 데이터를 저장하므로 데이터베이스를 만들어야 합니다.

$ gcloud app create --region=${REGION}
$ gcloud firestore databases create --region=${REGION}

이 Codelab의 뒷부분에서 REST API를 구현할 때 데이터를 정렬하고 필터링해야 합니다. 이를 위해 3개의 색인을 생성합니다.

$ gcloud firestore indexes composite create --collection=books \
    --field-config field-path=updated,order=descending \
    --field-config field-path=author,order=ascending \
    --field-config field-path=language,order=ascending

$ gcloud firestore indexes composite create --collection=books \
    --field-config field-path=updated,order=descending \
    --field-config field-path=language,order=ascending

$ gcloud firestore indexes composite create --collection=books \
    --field-config field-path=updated,order=descending \
    --field-config field-path=author,order=ascending

이 3가지 색인은 작성자 또는 언어별로 검색되는 동시에 검색 필드를 통해 업데이트된 컬렉션을 유지합니다.

4. 코드 가져오기

다음 GitHub 저장소에서 코드를 가져옵니다.

$ git clone https://github.com/glaforge/serverless-web-apis

애플리케이션 코드는 Node.JS를 사용하여 작성됩니다.

이 실습과 관련된 다음 폴더 구조가 생성됩니다.

serverless-web-apis
 |
 ├── data
 |   ├── books.json
 |
 ├── function-import
 |   ├── index.js
 |   ├── package.json
 |
 ├── run-crud
 |   ├── index.js
 |   ├── package.json
 |   ├── Dockerfile
 |
 ├── appengine-frontend
 |   ├── public
 |   |   ├── css/style.css
 |   |   ├── html/index.html
 |   |   ├── js/app.js
 |   ├── index.js
 |   ├── package.json
 |   ├── app.yaml

다음은 관련 폴더입니다.

  • data - 이 폴더에는 100권의 책 목록에 대한 샘플 데이터가 포함되어 있습니다.
  • function-import - 이 함수는 샘플 데이터를 가져올 엔드포인트를 제공합니다.
  • run-crud — 이 컨테이너는 Cloud Firestore에 저장된 책 데이터에 액세스하기 위해 웹 API를 노출합니다.
  • appengine-frontend - 이 App Engine 웹 애플리케이션은 도서 목록을 탐색할 수 있는 간단한 읽기 전용 프런트엔드를 표시합니다.

5 샘플 도서 라이브러리 데이터

데이터 폴더에는 100개의 책 목록이 포함된 books.json 파일이 있으며 이 책은 읽어 볼 만한 가치가 있습니다. 이 JSON 문서는 JSON 객체를 포함하는 배열입니다. Cloud 함수를 통해 수집할 데이터의 형태를 살펴보겠습니다.

[
  {
    "isbn": "9780435272463",
    "author": "Chinua Achebe",
    "language": "English",
    "pages": 209,
    "title": "Things Fall Apart",
    "year": 1958
  },
  {
    "isbn": "9781414251196",
    "author": "Hans Christian Andersen",
    "language": "Danish",
    "pages": 784,
    "title": "Fairy tales",
    "year": 1836
  },
  ...
]

이 배열의 모든 도서 항목에는 다음 정보가 포함됩니다.

  • isbn - 책을 식별하는 ISBN-13 코드입니다.
  • author — 도서 저자의 이름입니다.
  • language — 도서가 작성된 음성 언어입니다.
  • pages — 책의 페이지 수입니다.
  • title — 책의 제목입니다.
  • year - 도서가 게시된 연도입니다.

6. 샘플 도서 데이터를 가져오는 함수 엔드포인트

이 첫 번째 섹션에서는 샘플 도서 데이터를 가져오는 데 사용할 엔드포인트를 구현합니다. 여기서는 Cloud Functions를 사용합니다.

코드 살펴보기

먼저 package.json 파일을 살펴보겠습니다.

{
    "name": "function-import",
    "description": "Import sample book data",
    "license": "Apache-2.0",
    "dependencies": {
        "@google-cloud/firestore": "^4.9.9"
    },
    "devDependencies": {
        "@google-cloud/functions-framework": "^1.7.1"
    },
    "scripts": {
        "start": "npx @google-cloud/functions-framework --target=parseBooks"
    }
}

런타임 종속 항목에서는 @google-cloud/firestore NPM 모듈만 있으면 데이터베이스에 액세스하고 책 데이터를 저장할 수 있습니다. 내부적으로 Cloud Functions 런타임은 Express 웹 프레임워크도 제공하므로 종속 항목으로 선언할 필요가 없습니다.

개발 종속 항목에서는 함수를 호출하는 데 사용되는 런타임 프레임워크인 함수 프레임워크 (@google-cloud/functions-framework)를 선언합니다. 이 API는 머신에서 로컬로 사용할 수 있는 오픈소스 프레임워크로, 변경할 때마다 배포할 필요 없이 함수를 로컬에서 실행하여 개발 의견 루프를 개선합니다.

종속 항목을 설치하려면 install 명령어를 사용합니다.

$ npm install

start 스크립트는 Functions 프레임워크를 사용하여 다음 안내에 따라 로컬에서 함수를 실행하는 데 사용할 수 있는 명령어를 제공합니다.

$ npm start

curl 또는 HTTP GET 요청에 Cloud Shell 웹 미리보기를 사용하여 함수와 상호작용할 수 있습니다.

이제 도서 데이터 가져오기 함수의 로직이 포함된 index.js 파일을 살펴보겠습니다.

const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');

Firestore 모듈을 인스턴스화하고 도서 컬렉션을 가리킵니다 (관계형 데이터베이스의 표와 유사).

exports.parseBooks = async (req, resp) => {
    if (req.method !== "POST") {
        resp.status(405).send({error: "Only method POST allowed"});
        return;
    }
    if (req.headers['content-type'] !== "application/json") {
        resp.status(406).send({error: "Only application/json accepted"});
        return;
    }
    ...
}

parseBooks 자바스크립트 함수를 내보내는 중입니다. 이는 나중에 배포할 때 선언하는 함수입니다.

다음 안내에서는 다음 사항을 확인합니다.

  • Google은 HTTP POST 요청만 허용하며, 그렇지 않은 경우에는 405 상태 코드를 반환하여 다른 HTTP 메서드가 허용되지 않음을 나타냅니다.
  • Google에서는 application/json 페이로드만 허용하며, 허용되는 페이로드 형식이 아님을 나타내기 위해 406 상태 코드를 전송합니다.
    const books = req.body;

    const writeBatch = firestore.batch();

    for (const book of books) {
        const doc = bookStore.doc(book.isbn);
        writeBatch.set(doc, {
            title: book.title,
            author: book.author,
            language: book.language,
            pages: book.pages,
            year: book.year,
            updated: Firestore.Timestamp.now()
        });
    }

그러면 요청의 body을 통해 JSON 페이로드를 가져올 수 있습니다. 모든 도서를 일괄적으로 저장할 수 있도록 Firestore 일괄 작업을 준비 중입니다. Google에서는 도서 세부정보로 구성된 JSON 배열을 반복하여 isbn, title, author, language, pages, year 필드를 살펴봅니다. 도서의 ISBN 코드는 기본 키 또는 식별자로 사용됩니다.

    try {
        await writeBatch.commit();
        console.log("Saved books in Firestore");
    } catch (e) {
        console.error("Error saving books:", e);
        resp.status(400).send({error: "Error saving books"});
        return;
    };

    resp.status(202).send({status: "OK"});

이제 대량의 데이터가 준비되었으므로 작업을 커밋할 수 있습니다. 저장소 작업이 실패하면 400 상태 코드를 반환하여 실패했다고 알립니다. 그렇지 않으면 일괄 저장 요청이 수락되었음을 나타내는 202 상태 코드와 함께 OK 응답을 반환할 수 있습니다.

가져오기 함수 실행 및 테스트

코드를 실행하기 전에 다음을 사용하여 종속 항목을 설치합니다.

$ npm install

함수 프레임워크 덕분에 로컬에서 함수를 실행하기 위해 package.json에서 정의한 start 스크립트 명령어를 사용합니다.

$ npm start

> start
> npx @google-cloud/functions-framework --target=parseBooks

Serving function...
Function: parseBooks
URL: http://localhost:8080/

HTTP POST 요청을 로컬 함수에 전송하려면 다음을 실행합니다.

$ curl -d "@../data/books.json" \
       -H "Content-Type: application/json" \
       http://localhost:8080/

이 명령어를 실행하면 함수가 로컬로 실행되는지 확인하는 출력이 다음과 같이 표시됩니다.

{"status":"OK"}

또한 Cloud Console UI로 이동하여 데이터가 Firestore에 실제로 저장되었는지 확인할 수 있습니다.

d6a2b31bfa3443f2.png

위의 스크린샷에서 생성된 books 컬렉션, 도서 ISBN 코드로 식별된 책 문서 목록, 오른쪽의 특정 도서 항목에 대한 세부정보를 볼 수 있습니다.

클라우드에 함수 배포

Cloud Functions에서 함수를 배포하기 위해 function-import 디렉터리에 다음 명령어를 사용합니다.

$ gcloud functions deploy bulk-import \
         --trigger-http \
         --runtime=nodejs12 \
         --allow-unauthenticated \
         --region=${REGION} \
         --source=. \
         --entry-point=parseBooks

기호화된 이름 bulk-import를 사용하여 함수를 배포합니다. 이 함수는 HTTP 요청을 통해 트리거됩니다. Node.js 12 런타임을 사용합니다. 함수를 공개적으로 배포합니다 (이상적으로 엔드포인트에 보안을 적용해야 함). 함수를 배치할 리전을 지정합니다. 그리고 로컬 디렉터리의 소스를 가리키고 parseBooks (내보낸 자바스크립트 함수)를 진입점으로 사용합니다.

몇 분 이내에 함수가 클라우드에 배포됩니다. Cloud Console UI에서 함수가 표시됩니다.

C3156D50ba917ddd.png

배포 출력에서 특정 이름 지정 규칙 (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME})을 따르는 함수의 URL을 확인할 수 있습니다. 또한 Cloud Console UI의 HTTP 트리거 URL도 확인할 수 있습니다. 트리거 탭:

2d19539de3de98eb.png

명령줄을 통해 gcloud로 URL을 검색할 수도 있습니다.

$ export BULK_IMPORT_URL=$(gcloud functions describe bulk-import \
                                  --region=$REGION \
                                  --format 'value(httpsTrigger.url)')
$ echo $BULK_IMPORT_URL

배포된 함수를 테스트하는 데 재사용할 수 있도록 BULK_IMPORT_URL 환경 변수에 저장합니다.

배포된 함수 테스트

앞서 로컬에서 실행한 함수를 테스트하는 데 사용한 것과 유사한 curl 명령어를 사용하여 배포된 함수를 테스트합니다. 유일한 변경사항은 URL입니다.

$ curl -d "@../data/books.json" \
       -H "Content-Type: application/json" \
       $BULK_IMPORT_URL

이번에도 성공하면 다음과 같이 출력됩니다.

{"status":"OK"}

가져오기 함수가 배포되었고 샘플 데이터가 업로드되었으니 이제 이 데이터 세트를 노출하는 REST API를 개발할 차례입니다.

7 REST API 계약

Open API 사양 등을 사용하는 API 계약을 정의하지는 않지만 REST API의 다양한 엔드포인트를 살펴보겠습니다.

API 교환은 다음으로 구성된 JSON 객체를 예약합니다.

  • isbn(선택사항) - 유효한 ISBN 코드를 나타내는 13자(영문 기준) String입니다.
  • author — 도서 저자의 이름을 나타내는 비어 있지 않은 String
  • language — 도서가 작성된 언어를 포함하는 비어 있지 않은 String
  • pages — 책의 페이지 수에 양수 Integer
  • title — 책 제목이 비어 있지 않은 String
  • year - 도서 발행 연도의 Integer 값입니다.

도서 페이로드 예시:

{
    "isbn": "9780435272463",
    "author": "Chinua Achebe",
    "language": "English",
    "pages": 209,
    "title": "Things Fall Apart",
    "year": 1958
  }

/books 다운로드

저자 또는 언어로 필터링될 가능성이 있는 모든 도서의 목록을 가져오고 한 번에 10개의 결과 페이지로 나눌 수 있습니다.

본문 페이로드: 없음

쿼리 매개변수:

  • author (선택사항) - 저자별로 도서 목록을 필터링합니다.
  • language (선택사항) - 언어별로 도서 목록을 필터링합니다.
  • page (선택사항, 기본값 = 0) — 반환할 결과 페이지의 순위를 나타냅니다.

반환: 책 객체의 JSON 배열.

상태 코드:

  • 200 — 요청이 성공하면 도서 목록을 가져옵니다.
  • 400 — 오류가 발생한 경우

/books /books/POST /books/{isbn}

새 도서 페이로드를isbn path 매개변수(이 경우isbn 책 페이로드에 코드가 필요하지 않음)가 있거나 없는 경우isbn 도서 페이로드에 코드가 있어야 함)

본문 페이로드: 도서 객체입니다.

쿼리 매개변수: 없음.

반환: 없음.

상태 코드:

  • 201 — 도서가 성공적으로 저장되면
  • 406isbn 코드가 잘못된 경우
  • 400 — 오류가 발생한 경우

/books/{isbn} 받기

라이브러리에서 isbn 코드로 식별되며 경로 매개변수로 전달된 도서를 검색합니다.

본문 페이로드: 없음

쿼리 매개변수: 없음.

반환: 도서 JSON 객체 또는 책이 없는 경우 오류 객체

상태 코드:

  • 200 — 데이터베이스에서 책을 찾는 경우
  • 400 — 오류가 발생하면
  • 404 — 책을 찾을 수 없는 경우
  • 406isbn 코드가 잘못된 경우

PUT /books/{isbn}

경로 매개변수로 전달된 isbn로 식별된 기존 도서를 업데이트합니다.

본문 페이로드: 도서 객체입니다. 업데이트가 필요한 필드만 전달할 수 있으며 다른 필드는 선택사항입니다.

쿼리 매개변수: 없음.

반환: 업데이트된 책

상태 코드:

  • 200 — 도서가 성공적으로 업데이트되면
  • 400 — 오류가 발생하면
  • 406isbn 코드가 잘못된 경우

/books/{isbn} 삭제

경로 매개변수로 전달된 isbn로 식별되는 기존 책을 삭제합니다.

본문 페이로드: 없음

쿼리 매개변수: 없음.

반환: 없음.

상태 코드:

  • 204 — 도서가 성공적으로 삭제되면
  • 400 — 오류가 발생한 경우

8 컨테이너에서 REST API 배포 및 노출

코드 살펴보기

Dockerfile

먼저 애플리케이션 코드 컨테이너화를 담당하는 Dockerfile를 살펴보겠습니다.

FROM node:14-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . ./
CMD [ "node", "index.js" ]

Node.JS 14 'slim' 이미지를 사용하고 있습니다. /usr/src/app 디렉터리에서 작업 중입니다. Google에서는 종속 항목을 정의하는 package.json 파일 (아래 세부정보 참고)을 복사합니다. npm install를 사용하여 종속 항목을 설치하고 소스 코드를 복사합니다. 마지막으로 이 명령어를 node index.js 명령어로 실행합니다.

package.json

다음으로 package.json 파일을 살펴보겠습니다.

{
    "name": "run-crud",
    "description": "CRUD operations over book data",
    "license": "Apache-2.0",
    "engines": {
        "node": ">= 14.0.0"
    },
    "dependencies": {
        "@google-cloud/firestore": "^4.9.9",
        "cors": "^2.8.5",
        "express": "^4.17.1",
        "isbn3": "^1.1.10"
    },
    "scripts": {
        "start": "node index.js"
    }
}

Dockerfile의 경우와 마찬가지로 Node.JS 14를 사용하도록 지정합니다.

Google의 웹 API 애플리케이션은 다음 항목에 종속됩니다.

  • Firestore 도서 NPM 모듈이 데이터베이스의 도서 데이터에 액세스합니다.
  • cors 라이브러리는 CORS (교차 출처 리소스 공유) 요청을 처리합니다. REST API는 App Engine 웹 애플리케이션 프런트엔드의 클라이언트 코드에서 호출되기 때문입니다.
  • API 설계를 위한 웹 프레임워크가 될 Express 프레임워크인
  • 그런 다음 도서 ISBN 코드의 유효성을 검사하는 데 도움이 되는 isbn3 모듈을 사용합니다.

또한 개발 및 테스트 목적으로 로컬에서 애플리케이션을 시작하는 데 편리한 start 스크립트도 지정합니다.

index.js

이제 코드의 핵심으로 넘어가서 index.js를 자세히 살펴보겠습니다.

const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');

Firestore 모듈이 필요하며, 도서 데이터가 저장되는 books 컬렉션을 참조합니다.

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());

const querystring = require('querystring');

const cors = require('cors');
app.use(cors({
    exposedHeaders: ['Content-Length', 'Content-Type', 'Link'],
}));

Google은 웹 프레임워크인 Express를 사용하여 REST API를 구현합니다. body-parser 모듈을 사용하여 API와 교환된 JSON 페이로드를 파싱하고 있습니다.

querystring 모듈은 URL 조작에 유용합니다. 페이지로 나누기를 위해 Link 헤더를 만들 때도 마찬가지입니다 (자세한 내용은 뒷부분 참고).

그런 다음 cors 모듈을 구성합니다. CORS를 통해 전달하고자 하는 헤더는 대부분 제거되어 있기 때문에 여기서는 일반 콘텐츠 길이와 유형, 페이지로 나누기에 지정할 Link 헤더를 유지하고자 합니다.

const ISBN = require('isbn3');

function isbnOK(isbn, res) {
    const parsedIsbn = ISBN.parse(isbn);
    if (!parsedIsbn) {
        res.status(406)
            .send({error: `Invalid ISBN: ${isbn}`});
        return false;
    }
    return parsedIsbn;
}

isbn3 NPM 모듈을 사용하여 ISBN 코드를 파싱하고 검증합니다. ISBN 코드가 다음과 같은 경우 ISBN 코드를 파싱하고 응답에서 406 상태 코드로 응답하는 작은 유틸리티 함수를 개발합니다. 잘못되었습니다.

  • GET /books

GET /books 엔드포인트를 단계별로 살펴보겠습니다.

app.get('/books', async (req, res) => {
    try {
        var query = new Firestore().collection('books');

        if (!!req.query.author) {
            console.log(`Filtering by author: ${req.query.author}`);
            query = query.where("author", "==", req.query.author);
        }
        if (!!req.query.language) {
            console.log(`Filtering by language: ${req.query.language}`);
            query = query.where("language", "==", req.query.language);
        }

        const page = parseInt(req.query.page) || 0;

        // - - ✄ - - ✄ - - ✄ - - ✄ - - ✄ - -

    } catch (e) {
        console.error('Failed to fetch books', e);
        res.status(400)
            .send({error: `Impossible to fetch books: ${e.message}`});
    }
});

쿼리를 준비하여 데이터베이스를 쿼리할 준비를 하고 있습니다. 이 쿼리는 선택적 쿼리 매개변수를 사용하여 작성자 및/또는 언어별로 필터링합니다. 또한 10권으로 나누어 도서 목록을 반환합니다.

책을 가져오는 동안 오류가 발생하면 400 상태 코드와 함께 오류가 반환됩니다.

이 엔드포인트의 잘린 부분을 확대해 보겠습니다.

        const snapshot = await query
            .orderBy('updated', 'desc')
            .limit(PAGE_SIZE)
            .offset(PAGE_SIZE * page)
            .get();

        const books = [];

        if (snapshot.empty) {
            console.log('No book found');
        } else {
            snapshot.forEach(doc => {
                const {title, author, pages, year, language, ...otherFields} = doc.data();
                const book = {isbn: doc.id, title, author, pages, year, language};
                books.push(book);
            });
        }

이전 섹션에서는 authorlanguage로 필터링했습니다. 하지만 이 섹션에서는 마지막으로 업데이트된 날짜순으로 나열된 목록을 정렬합니다. 또한 제한 (반환할 요소 수)과 오프셋 (다음 도서 배치를 반환하는 시작점)을 정의하여 결과를 페이지로 나눕니다.

쿼리를 실행하고 데이터의 스냅샷을 가져와서 결과를 함수 끝부분에서 반환되는 자바스크립트 배열에 넣습니다.

이 엔드포인트에 관한 자세한 설명을 마치겠습니다. Link 헤더를 사용하여 데이터의 첫 번째, 이전, 다음 또는 마지막 페이지에 대한 URI 링크를 정의하겠습니다. 이 경우에는 이전 및 다음)을 탭합니다.

        var links = {};
        if (page > 0) {
            const prevQuery = querystring.stringify({...req.query, page: page - 1});
            links.prev = `${req.path}${prevQuery != '' ? `?${prevQuery}` : ''}`;
        }
        if (snapshot.docs.length === PAGE_SIZE) {
            const nextQuery = querystring.stringify({...req.query, page: page + 1});
            links.next = `${req.path}${nextQuery != '' ? `?${nextQuery}` : ''}`;
        }
        if (Object.keys(links).length > 0) {
            res.links(links);
        }

        res.status(200).send(books);

처음에는 이 로직이 약간 복잡하게 보일 수도 있지만 Google은 데이터의 첫 번째 페이지에 있지 않은 경우 이전 링크를 추가하는 작업을하고 있습니다. 또한 데이터가 더 있는 페이지가 있다면 (예: PAGE_SIZE 상수에 정의된 최대 도서 수가 포함되어 있고 더 많은 데이터가 나올 것으로 가정되는 경우) 다음 링크를 추가합니다. 그런 다음 Express의 resource#links() 함수를 사용하여 올바른 구문으로 올바른 헤더를 만듭니다.

정보 링크 링크는 다음과 같습니다.

link: </books?page=1>; rel="prev", </books?page=3>; rel="next"
  • POST /booksPOST /books/:isbn

새 엔드포인트를 만들기 위해 두 엔드포인트가 모두 여기에 있습니다. 하나는 도서 페이로드의 ISBN 코드를 전달하고 다른 하나는 경로 매개변수로 전달합니다. 어느 쪽이든 둘 다 createBook() 함수를 호출합니다.

async function createBook(isbn, req, res) {
    const parsedIsbn = isbnOK(isbn, res);
    if (!parsedIsbn) return;

    const {title, author, pages, year, language} = req.body;

    try {
        const docRef = bookStore.doc(parsedIsbn.isbn13);
        await docRef.set({
            title, author, pages, year, language,
            updated: Firestore.Timestamp.now()
        });
        console.log(`Saved book ${parsedIsbn.isbn13}`);

        res.status(201)
            .location(`/books/${parsedIsbn.isbn13}`)
            .send({status: `Book ${parsedIsbn.isbn13} created`});
    } catch (e) {
        console.error(`Failed to save book ${parsedIsbn.isbn13}`, e);
        res.status(400)
            .send({error: `Impossible to create book ${parsedIsbn.isbn13}: ${e.message}`});
    }
}

isbn 코드가 유효한지 확인하고 그렇지 않으면 함수에서 반환 (406 상태 코드 설정)합니다. 요청 본문에 전달된 페이로드에서 도서 필드를 검색합니다. 그런 다음 Firestore에 도서 세부정보를 저장합니다. 성공 시 201를, 실패 시 400를 반환합니다.

성공적으로 반환되면 새로 만든 리소스가 있는 API 클라이언트에 신호를 제공하기 위해 위치 헤더도 설정합니다. 헤더는 다음과 같이 표시됩니다.

Location: /books/9781234567898
  • GET /books/:isbn

ISBN에서 ISBN을 통해 식별된 책을 Firestore에서 가져옵니다.

app.get('/books/:isbn', async (req, res) => {
    const parsedIsbn = isbnOK(req.params.isbn, res);
    if (!parsedIsbn) return;

    try {
        const docRef = bookStore.doc(parsedIsbn.isbn13);
        const docSnapshot = await docRef.get();

        if (!docSnapshot.exists) {
            console.log(`Book not found ${parsedIsbn.isbn13}`)
            res.status(404)
                .send({error: `Could not find book ${parsedIsbn.isbn13}`});
            return;
        }

        console.log(`Fetched book ${parsedIsbn.isbn13}`, docSnapshot.data());

        const {title, author, pages, year, language, ...otherFields} = docSnapshot.data();
        const book = {isbn: parsedIsbn.isbn13, title, author, pages, year, language};

        res.status(200).send(book);
    } catch (e) {
        console.error(`Failed to fetch book ${parsedIsbn.isbn13}`, e);
        res.status(400)
            .send({error: `Impossible to fetch book ${parsedIsbn.isbn13}: ${e.message}`});
    }
});

언제나 그렇듯이 ISBN이 올바른지 확인합니다. Firestore를 통해 책을 검색하는 쿼리는 다음과 같습니다. snapshot.exists 속성을 사용하면 실제로 책을 찾을 수 있습니다. 그러지 않으면 Google에서 오류 및 404을(를) 찾을 수 없음 상태 코드를 다시 전송합니다. 도서 데이터를 검색하고 도서를 표시할 JSON 객체를 생성합니다.

  • PUT /books/:isbn

PUT 메서드를 사용하여 기존 책을 업데이트합니다.

app.put('/books/:isbn', async (req, res) => {
    const parsedIsbn = isbnOK(req.params.isbn, res);
    if (!parsedIsbn) return;

    try {
        const docRef = bookStore.doc(parsedIsbn.isbn13);
        await docRef.set({
            ...req.body,
            updated: Firestore.Timestamp.now()
        }, {merge: true});
        console.log(`Updated book ${parsedIsbn.isbn13}`);

        res.status(201)
            .location(`/books/${parsedIsbn.isbn13}`)
            .send({status: `Book ${parsedIsbn.isbn13} updated`});
    } catch (e) {
        console.error(`Failed to update book ${parsedIsbn.isbn13}`, e);
        res.status(400)
            .send({error: `Impossible to update book ${parsedIsbn.isbn13}: ${e.message}`});
    }
});

updated 날짜/시간 필드를 업데이트하여 이 레코드가 마지막으로 업데이트된 시기를 기억합니다. Google은 기존 필드를 새 값으로 대체하는 {merge:true} 전략을 사용합니다. 그렇지 않으면 모든 필드가 삭제되고 페이로드의 새 필드만 저장되므로 이전 업데이트나 초기 생성 시 기존 필드가 삭제됩니다.

Location 헤더가 책의 URI를 가리키도록 설정합니다.

  • DELETE /books/:isbn

책 삭제는 매우 간단합니다. 문서 참조에 대해 delete() 메서드를 호출하기만 하면 됩니다. Google은 콘텐츠를 반환하지 않으므로 204 상태 코드를 반환합니다.

app.delete('/books/:isbn', async (req, res) => {
    const parsedIsbn = isbnOK(req.params.isbn, res);
    if (!parsedIsbn) return;

    try {
        const docRef = bookStore.doc(parsedIsbn.isbn13);
        await docRef.delete();
        console.log(`Book ${parsedIsbn.isbn13} was deleted`);

        res.status(204).end();
    } catch (e) {
        console.error(`Failed to delete book ${parsedIsbn.isbn13}`, e);
        res.status(400)
            .send({error: `Impossible to delete book ${parsedIsbn.isbn13}: ${e.message}`});
    }
});

Express / Node 서버 시작

마지막으로, 포트 8080에서 기본적으로 수신 대기하는 서버를 시작합니다.

const port = process.env.PORT || 8080;
app.listen(port, () => {
    console.log(`Books Web API service: listening on port ${port}`);
    console.log(`Node ${process.version}`);
});

애플리케이션을 로컬로 실행

애플리케이션을 로컬에서 실행하려면 먼저 다음을 사용하여 종속 항목을 설치합니다.

$ npm install

그런 다음

$ npm start

서버는 localhost에 시작하여 기본적으로 포트 8080에서 수신 대기합니다.

다음 명령어를 사용하여 Docker 컨테이너를 빌드하고 컨테이너 이미지도 실행할 수도 있습니다.

$ docker build -t crud-web-api .

$ docker run --rm -p 8080:8080 -it crud-web-api

Docker 내에서 실행하는 것도 Cloud Build로 클라우드에서 빌드할 때 애플리케이션 컨테이너화가 원활하게 실행되는지 다시 확인하는 좋은 방법입니다.

API 테스트

{0}Node를 통해 직접 또는 Docker 컨테이너 이미지를 통해 직접 REST API 코드를 실행하는 것과 상관없이 이제 이 API에 대해 몇 가지 쿼리를 실행할 수 있습니다.

  • 새 책 (본문 페이로드의 ISBN)을 만듭니다.
$ curl -XPOST -d '{"isbn":"9782070368228","title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books
  • 새 도서 생성 (경로 매개변수의 ISBN):
$ curl -XPOST -d '{"title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books/9782070368228
  • 책 삭제 (생성한 책):
$ curl -XDELETE http://localhost:8080/books/9782070368228
  • ISBN으로 책 가져오기:
$ curl http://localhost:8080/books/9780140449136
$ curl http://localhost:8080/books/9782070360536
  • 제목만 변경하여 기존 도서를 업데이트합니다.
$ curl -XPUT \
       -d '{"title":"Book"}' \
       -H "Content-Type: application/json" \
       http://localhost:8080/books/9780003701203
  • 도서 목록을 가져옵니다 (첫 10장까지).
$ curl http://localhost:8080/books
  • 특정 저자가 집필한 도서를 찾습니다.
$ curl http://localhost:8080/books?author=Virginia+Woolf
  • 영어로 작성된 도서를 나열합니다.
$ curl http://localhost:8080/books?language=English
  • 네 번째 도서 페이지 로드:
$ curl http://localhost:8080/books?page=3

author, language, books 쿼리 매개변수를 결합하여 상세검색을 수행할 수도 있습니다.

컨테이너화된 REST API 빌드 및 배포

REST API가 계획에 따라 작동하게 되어 기쁘게 생각하며, Cloud Run에서 클라우드에 API를 배포하기에 좋습니다.

이 작업은 두 단계로 이루어집니다.

  • 먼저 Cloud Build로 다음 명령어를 사용하여 컨테이너 이미지를 빌드합니다.
$ gcloud builds submit \
         --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
  • 그런 다음 이 두 번째 명령어를 사용하여 서비스를 배포합니다.
$ gcloud run deploy run-crud \
         --image gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api \
         --allow-unauthenticated \
         --region=${REGION} \
         --platform=managed

첫 번째 명령어로 Cloud Build는 컨테이너 이미지를 빌드하고 Container Registry에 호스팅합니다. 다음 명령어는 레지스트리에서 컨테이너 이미지를 배포하고 클라우드 리전에 배포합니다.

이제 Cloud Console UI에서 Cloud Run 서비스가 목록에 표시되는지 다시 한번 확인할 수 있습니다.

4ca13b0a703b2126.png

마지막 단계로 다음 명령어를 사용하여 새로 배포된 Cloud Run 서비스의 URL을 검색합니다.

$ export RUN_CRUD_SERVICE_URL=$(gcloud run services describe run-crud \
                                       --region=${REGION} \
                                       --platform=managed \
                                       --format='value(status.url)')

App Engine 프런트엔드 코드가 API와 상호작용하므로 다음 섹션에서 Cloud Run REST API의 URL이 필요합니다.

9. 웹 앱을 호스팅하여 라이브러리 둘러보기

이 프로젝트에 반짝이는 퍼즐의 마지막 부분은 REST API와 상호작용하는 웹 프런트엔드를 제공하는 것입니다. 이 목적으로 AJAX 요청을 통해 API를 호출하는 일부 클라이언트 자바스크립트 코드와 함께 Google App Engine을 사용합니다 (클라이언트 측 Fetch API 사용).

Node.js App Engine 런타임에 배포되더라도 대부분의 애플리케이션은 정적 리소스로 구성됩니다. 대부분의 사용자 상호작용은 클라이언트 측 자바스크립트를 통해 브라우저에서 이루어지므로 백엔드 코드는 많지 않습니다. 여기서는 정교한 프런트엔드 자바스크립트 프레임워크를 사용하지 않으며, Shoelace 웹 구성요소 라이브러리를 사용하는 UI용 몇 가지 웹 구성요소와 함께 "vanilla" 자바스크립트를 사용합니다.

  • 도서 언어를 선택하는 선택 상자:

1b7bf64bd327b1ee.png

  • 특정 도서 관련 세부정보를 표시하는 카드 구성요소 (JsBarcode 라이브러리를 사용하여 책의 ISBN을 나타내는 바코드 포함)

4dd54e4d5ee53367.png

  • 데이터베이스에서 더 많은 책을 로드하는 버튼:

4766c796a9d87475.png

이러한 모든 시각적 구성요소를 함께 결합할 경우, 결과물 웹페이지는 다음과 같이 표시됩니다.

fb6eae65811c8ac2.png

app.yaml 구성 파일

먼저 이 App Engine 애플리케이션의 코드베이스를 살펴보고 app.yaml 구성 파일을 살펴보겠습니다. 이 파일은 App Engine에만 해당하는 파일로, 환경 변수, 애플리케이션의 다양한 '핸들러' 등을 구성하거나 일부 리소스가 정적 애셋임을 지정할 수 있습니다. App Engine의 기본 CDN에서 제공됩니다.

runtime: nodejs14

env_variables:
  RUN_CRUD_SERVICE_URL: CHANGE_ME

handlers:

- url: /js
  static_dir: public/js

- url: /css
  static_dir: public/css

- url: /img
  static_dir: public/img

- url: /(.+\.html)
  static_files: public/html/\1
  upload: public/(.+\.html)

- url: /
  static_files: public/html/index.html
  upload: public/html/index\.html

- url: /.*
  secure: always
  script: auto

애플리케이션이 Node.js 애플리케이션이며 버전 14를 사용하도록 지정한다고 가정해 보겠습니다.

그런 다음 Cloud Run 서비스 URL을 가리키는 환경 변수를 정의합니다. CHANGE_ME 자리표시자를 올바른 URL로 업데이트해야 합니다 (변경 방법은 아래 참조).

그런 다음 다양한 핸들러를 정의합니다. 처음 3개는 public/ 폴더와 그 하위 폴더 아래의 HTML, CSS, 자바스크립트 클라이언트 측 코드 위치를 가리킵니다. 네 번째는 App Engine 애플리케이션의 루트 URL이 index.html 페이지를 가리켜야 함을 나타냅니다. 이렇게 하면 웹사이트의 루트에 액세스할 때 URL에 index.html 접미사가 표시되지 않습니다. 마지막 URL은 다른 모든 URL (/.*)을 Node.JS 애플리케이션 (즉, 정적 URL과 대조적으로 애플리케이션의 'dynamic' 부분)으로 라우팅하는 기본 URL입니다. 설명된 애셋만 지원합니다.

이제 Cloud Run 서비스의 웹 API URL을 업데이트해 보겠습니다.

appengine-frontend/ 디렉터리에서 다음 명령어를 실행하여 Cloud Run 기반 REST API의 URL을 가리키는 환경 변수를 업데이트합니다.

$ sed -i -e "s|CHANGE_ME|${RUN_CRUD_SERVICE_URL}|" app.yaml

또는 app.yamlCHANGE_ME 문자열을 정확한 URL로 직접 변경합니다.

env_variables:
    RUN_CRUD_SERVICE_URL: CHANGE_ME

Node.JS package.json 파일

{
    "name": "appengine-frontend",
    "description": "Web frontend",
    "license": "Apache-2.0",
    "main": "index.js",
    "engines": {
        "node": "^14.0.0"
    },
    "dependencies": {
        "express": "^4.17.1",
        "isbn3": "^1.1.10"
    },
    "devDependencies": {
        "nodemon": "^2.0.7"
    },
    "scripts": {
        "start": "node index.js",
        "dev": "nodemon --watch server --inspect index.js"
    }
}

Node.js 14를 사용하여 이 애플리케이션을 실행하고자 합니다. Express 프레임워크 및 isbn3 NPM 모듈을 사용하여 책의 ISBN 코드를 확인합니다.

개발 종속 항목에서는 nodemon 모듈을 사용하여 파일 변경사항을 모니터링합니다. npm start를 사용하여 로컬에서 애플리케이션을 실행하고 코드를 변경하고 ^C로 앱을 중지했다가 다시 실행할 수는 있지만 다소 지루합니다. 대신 다음 명령어를 사용하여 변경 시 애플리케이션이 자동으로 새로고침 / 다시 시작되도록 할 수 있습니다.

$ npm run dev

index.js Node.JS 코드

const express = require('express');
const app = express();

app.use(express.static('public'));

const bodyParser = require('body-parser');
app.use(bodyParser.json());

Express 웹 프레임워크가 필요합니다. 공개 디렉터리는 적어도 static 미들웨어에서 제공할 수 있는 (정적 모드에서 로컬로 실행되는 경우) 정적 애셋을 포함하도록 지정합니다. 마지막으로 body-parser에서 JSON 페이로드를 파싱해야 합니다.

정의한 몇 가지 경로를 살펴보겠습니다.

app.get('/', async (req, res) => {
    res.redirect('/html/index.html');
});

app.get('/webapi', async (req, res) => {
    res.send(process.env.RUN_CRUD_SERVICE_URL);
});

일치하는 첫 번째 /public/html 디렉터리의 index.html로 리디렉션됩니다. 개발 모드에서 App Engine 런타임 내에서 실행되지 않기 때문에 App Engine의 URL 라우팅을 가져오지 않습니다. 대신 루트 URL을 HTML 파일로 리디렉션하면 됩니다.

/webapi를 정의하는 두 번째 엔드포인트는 Cloud RUN REST API의 URL을 반환합니다. 이렇게 하면 클라이언트 측 자바스크립트 코드가 도서 목록을 가져오기 위해 호출할 위치를 알 수 있습니다.

const port = process.env.PORT || 8080;
app.listen(port, () => {
    console.log(`Book library web frontend: listening on port ${port}`);
    console.log(`Node ${process.version}`);
    console.log(`Web API endpoint ${process.env.RUN_CRUD_SERVICE_URL}`);
});

완료하려면 Express 웹 앱을 실행하고 기본적으로 포트 8080에서 수신 대기합니다.

index.html 페이지

긴 HTML 페이지의 모든 줄을 살펴보지는 않습니다. 대신 몇 가지 주요 줄을 강조표시하겠습니다.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/themes/base.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/shoelace.js"></script>

<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/barcodes/JsBarcode.ean-upc.min.js"></script>

<script src="/js/app.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css">

처음 두 줄은 Shoelace 웹 구성요소 라이브러리 (스크립트 및 스타일시트)를 가져옵니다.

다음 줄은 JsBarcode 라이브러리를 가져와 책 ISBN 코드의 바코드를 만듭니다.

마지막 줄은 public/ 하위 디렉터리에 있는 자체 자바스크립트 코드 및 CSS 스타일시트를 가져옵니다.

HTML 페이지의 body에서는 다음과 같이 맞춤 요소 태그와 함께 Shoelace 구성요소를 사용합니다.

<sl-icon name="book-half"></sl-icon>
...

<sl-select id="language-select" placeholder="Select a language..." clearable>
    <sl-menu-item value="English">English</sl-menu-item>
    <sl-menu-item value="French">French</sl-menu-item>
    ...
</sl-select>
...

<sl-button id="more-button" type="primary" size="large">
    More books...
</sl-button>
...

또한 HTML 템플릿과 슬롯 채우기 기능을 사용하여 책을 나타냅니다. 이 템플릿의 사본을 만들어 도서 목록을 채우고 슬롯의 값을 도서 세부정보로 대체합니다.

    <template id="book-card">
        <sl-card class="card-overview">
        ...
            <slot name="author">Author</slot>
            ...
        </sl-card>
    </template>

HTML이 충분하므로 코드 검토가 거의 완료되었습니다. 마지막으로 중요한 부분은 REST API와 상호작용하는 app.js 클라이언트 측 자바스크립트 코드입니다.

app.js 클라이언트 측 자바스크립트 코드

먼저 DOM 콘텐츠가 로드되기를 기다리는 최상위 이벤트 리스너로 시작합니다.

document.addEventListener("DOMContentLoaded", async function(event) {
    ...
}

준비되면 주요 상수 및 변수를 설정할 수 있습니다.

    const serverUrlResponse = await fetch('/webapi');
    const serverUrl = await serverUrlResponse.text();
    console.log('Web API endpoint:', serverUrl);

    const server = serverUrl + '/books';
    var page = 0;
    var language = '';

먼저 app.yaml에서 처음에 설정한 환경 변수를 반환하는 App Engine 노드 코드 덕분에 REST API의 URL을 가져옵니다. 자바스크립트 클라이언트 측 코드에서 호출된 환경 변수인 /webapi 엔드포인트 덕분에 프런트엔드 코드에 REST API URL을 하드코딩할 필요가 없었습니다.

또한 페이지로 나누기와 언어 필터링을 추적하는 데 사용할 page 변수와 language 변수도 정의합니다.

    const moreButton = document.getElementById('more-button');
    moreButton.addEventListener('sl-focus', event => {
        console.log('Button clicked');
        moreButton.blur();

        appendMoreBooks(server, page++, language);
    });

버튼에 이벤트 핸들러를 추가하여 도서를 로드합니다. 클릭하면 appendMoreBooks() 함수를 호출합니다.

    const langSelect = document.getElementById('language-select');
    langSelect.addEventListener('sl-change', event => {
        page = 0;
        language = event.srcElement.value;
        document.getElementById('library').replaceChildren();
        console.log(`Language selected: "${language}"`);

        appendMoreBooks(server, page++, language);
    });

선택사항과 마찬가지로 언어 선택의 변경사항에 대한 알림을 받도록 이벤트 핸들러를 추가합니다. 또한 버튼과 마찬가지로 REST API URL, 현재 페이지, 언어 선택을 전달하는 appendMoreBooks() 함수를 호출합니다.

이제 책을 가져오고 추가하는 함수를 살펴보겠습니다.

async function appendMoreBooks(server, page, language) {
    const searchUrl = new URL(server);
    if (!!page) searchUrl.searchParams.append('page', page);
    if (!!language) searchUrl.searchParams.append('language', language);

    const response = await fetch(searchUrl.href);
    const books = await response.json();
    ...
}

위에 REST REST를 호출하는 데 사용할 정확한 URL이 생성됩니다. 일반적으로 지정할 수 있는 쿼리 매개변수는 세 가지이지만 이 UI에서는 두 가지만 지정합니다.

  • page - 페이지로 나누기의 현재 페이지를 나타내는 정수입니다.
  • language - 작성된 언어를 기준으로 필터링할 언어 문자열입니다.

그런 다음 Fetch API를 사용하여 도서 세부정보가 포함된 JSON 배열을 가져옵니다.

    const linkHeader = response.headers.get('Link')
    console.log('Link', linkHeader);
    if (!!linkHeader && linkHeader.indexOf('rel="next"') > -1) {
        console.log('Show more button');
        document.getElementById('buttons').style.display = 'block';
    } else {
        console.log('Hide more button');
        document.getElementById('buttons').style.display = 'none';
    }

응답에 Link 헤더가 있는지에 따라 [More books...] 버튼을 표시하거나 숨깁니다. Link 헤더가 더 많은 책이 있는지 알려 주는 힌트이기 때문입니다. Link 헤더의 next URL)

    const library = document.getElementById('library');
    const template = document.getElementById('book-card');
    for (let book of books) {
        const bookCard = template.content.cloneNode(true);

        bookCard.querySelector('slot[name=title]').innerText = book.title;
        bookCard.querySelector('slot[name=language]').innerText = book.language;
        bookCard.querySelector('slot[name=author]').innerText = book.author;
        bookCard.querySelector('slot[name=year]').innerText = book.year;
        bookCard.querySelector('slot[name=pages]').innerText = book.pages;

        const img = document.createElement('img');
        img.setAttribute('id', book.isbn);
        img.setAttribute('class', 'img-barcode-' + book.isbn)
        bookCard.querySelector('slot[name=barcode]').appendChild(img);

        library.appendChild(bookCard);
        ...
    }
}

함수의 위 섹션에서 REST API가 반환하는 각 도서에 대해 템플릿을 나타내는 웹 구성요소로 템플릿을 클론하며 책의 세부정보로 템플릿 슬롯을 채웁니다.

JsBarcode('.img-barcode-' + book.isbn).EAN13(book.isbn, {fontSize: 18, textMargin: 0, height: 60}).render();

ISBN 코드를 좀 더 예쁘게 만들기 위해 JsBarcode 라이브러리를 사용하여 실제 책의 뒷면 표지와 같은 멋진 바코드를 만듭니다.

로컬에서 애플리케이션 실행 및 테스트

지금은 코드가 충분하므로 애플리케이션이 작동하는 모습을 볼 차례입니다. 먼저 실제로 배포하기 전에 Cloud Shell 내에서 로컬로 작업하게 됩니다.

애플리케이션에 필요한 NPM 모듈은 다음과 함께 설치됩니다.

$ npm install

그리고 평소대로 앱을 실행합니다.

$ npm start

또는 다음을 통해 변경사항이 자동으로 다시 로드됨nodemon ,

$ npm run dev

애플리케이션이 로컬에서 실행 중이며 브라우저의 http://localhost:8080에서 액세스할 수 있습니다.

App Engine 애플리케이션 배포

이제 애플리케이션이 로컬에서 잘 실행된다고 확신했으므로 App Engine에 배포할 차례입니다.

애플리케이션을 배포하기 위해 다음 명령어를 실행해 보겠습니다.

$ gcloud app deploy -q

약 1분 후에 애플리케이션이 배포됩니다.

https://${GOOGLE_CLOUD_PROJECT}.appspot.com 형식의 URL에서 애플리케이션을 사용할 수 있습니다.

App Engine 웹 애플리케이션의 UI 탐색

추가된 기능은 다음과 같습니다.

  • 더 많은 책을 로드하려면 [More books...] 버튼을 클릭합니다.
  • 특정 언어로 된 도서만 보려면 특정 언어를 선택합니다.
  • 선택 상자의 작은 교차로 선택 항목을 지우면 모든 도서 목록으로 돌아갑니다.

10. 삭제(선택 사항)

앱을 계속 유지하지 않으려는 경우 전체 프로젝트를 삭제하여 리소스를 절약하고 비용을 절감할 수 있습니다.

gcloud projects delete ${GOOGLE_CLOUD_PROJECT}

1일 축하합니다.

Cloud Functions, App Engine, Cloud Run 덕분에 일련의 서비스를 만들어 다양한 웹 API 엔드포인트 및 웹 프런트엔드를 노출하고, REST API 개발을 위한 훌륭한 설계 패턴을 따라 도서 라이브러리를 저장, 업데이트, 탐색할 수 있게 했습니다. 알 수 있습니다

학습한 내용

  • Cloud Functions
  • Cloud Firestore
  • Cloud Run
  • App Engine

자세히 알아보기

구체적인 예를 살펴보고 싶다면 다음 목록을 확인해 보세요.

  • API 게이트웨이를 활용하여 데이터 가져오기 함수 및 REST API 컨테이너에 공통 API 퍼사드를 제공하거나 API 액세스를 위한 API 키 처리와 같은 기능을 추가하거나 API 소비자를 위한 비율 제한을 정의합니다.
  • App Engine 애플리케이션에 Swagger-UI 노드 모듈을 배포하여 REST API의 테스트 플레이그라운드를 문서화하고 제공합니다.
  • 프런트엔드의 기존 탐색 기능 외에 데이터를 수정할 수 있는 추가 화면을 추가하고 새 도서 항목을 만듭니다. 또한 Cloud Firestore 데이터베이스를 사용하고 있으므로 실시간 기능을 활용하여 변경사항이 있을 때 표시되는 도서 데이터를 업데이트합니다.