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을 마치면 웹 프런트엔드는 다음과 같이 표시됩니다.

학습할 내용
- Cloud Functions
- Cloud Firestore
- Cloud Run
- App Engine
2. 설정 및 요건
자습형 환경 설정
- Google Cloud Console에 로그인하여 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다. 아직 Gmail이나 Google Workspace 계정이 없는 경우 계정을 만들어야 합니다.



- 프로젝트 이름은 이 프로젝트 참가자의 표시 이름입니다. 이는 Google API에서 사용하지 않는 문자열이며 언제든지 업데이트할 수 있습니다.
- 프로젝트 ID는 모든 Google Cloud 프로젝트에서 고유하며, 변경할 수 없습니다(설정된 후에는 변경할 수 없음). Cloud 콘솔은 고유한 문자열을 자동으로 생성합니다. 일반적으로는 신경 쓰지 않아도 됩니다. 대부분의 Codelab에서는 프로젝트 ID (일반적으로
PROJECT_ID로 식별됨)를 참조해야 합니다. 생성된 ID가 마음에 들지 않으면 다른 임의 ID를 생성할 수 있습니다. 또는 직접 시도해 보고 사용 가능한지 확인할 수도 있습니다. 이 단계 이후에는 변경할 수 없으며 프로젝트 기간 동안 유지됩니다. - 참고로 세 번째 값은 일부 API에서 사용하는 프로젝트 번호입니다. 이 세 가지 값에 대한 자세한 내용은 문서를 참고하세요.
- 다음으로 Cloud 리소스/API를 사용하려면 Cloud 콘솔에서 결제를 사용 설정해야 합니다. 이 Codelab 실행에는 많은 비용이 들지 않습니다. 이 튜토리얼이 끝난 후에 요금이 청구되지 않도록 리소스를 종료하려면 만든 리소스 또는 프로젝트를 삭제하면 됩니다. Google Cloud 신규 사용자는 300달러(USD) 상당의 무료 체험판 프로그램에 참여할 수 있습니다.
Cloud Shell 시작
Google Cloud를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 Cloud에서 실행되는 명령줄 환경인 Google Cloud Shell을 사용합니다.
Google Cloud Console의 오른쪽 상단 툴바에 있는 Cloud Shell 아이콘을 클릭합니다.

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

가상 머신에는 필요한 개발 도구가 모두 들어있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 Codelab의 모든 작업은 브라우저 내에서 수행할 수 있습니다. 아무것도 설치할 필요가 없습니다.
3. 환경 준비 및 클라우드 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 --location=${REGION}
이 Codelab의 뒷부분에서 REST API를 구현할 때 데이터를 정렬하고 필터링해야 합니다. 이를 위해 다음 세 가지 색인을 만듭니다.
$ gcloud firestore indexes composite create --collection-group=books \
--field-config field-path=language,order=ascending \
--field-config field-path=updated,order=descending
$ gcloud firestore indexes composite create --collection-group=books \
--field-config field-path=author,order=ascending \
--field-config field-path=updated,order=descending
이 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": "^3.1.0"
},
"scripts": {
"start": "npx @google-cloud/functions-framework --target=parseBooks"
}
}
런타임 종속 항목에서는 데이터베이스에 액세스하고 도서 데이터를 저장하는 데 @google-cloud/firestore NPM 모듈만 필요합니다. 내부적으로 Cloud Functions 런타임은 Express 웹 프레임워크도 제공하므로 종속 항목으로 선언할 필요가 없습니다.
개발 종속 항목에서 함수를 호출하는 데 사용되는 런타임 프레임워크인 Functions Framework (@google-cloud/functions-framework)를 선언합니다. 변경할 때마다 배포하지 않고 함수를 실행하여 개발 피드백 루프를 개선할 수 있는 오픈소스 프레임워크로, 머신 (이 경우 Cloud Shell 내부)에서 로컬로도 사용할 수 있습니다.
종속 항목을 설치하려면 install 명령어를 사용합니다.
$ npm install
start 스크립트는 Functions Framework를 사용하여 다음 안내에 따라 함수를 로컬로 실행하는 데 사용할 수 있는 명령어를 제공합니다.
$ npm start
curl 또는 Cloud Shell 웹 미리보기를 사용하여 HTTP GET 요청을 통해 함수와 상호작용할 수 있습니다.
이제 책 데이터 가져오기 함수의 로직이 포함된 index.js 파일을 살펴보겠습니다.
const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');
Firestore 모듈을 인스턴스화하고 도서 컬렉션 (관계형 데이터베이스의 테이블과 유사)을 가리킵니다.
functions.http('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 JavaScript 함수를 내보내고 있습니다. 이 함수는 나중에 배포할 때 선언할 함수입니다.
다음 몇 가지 안내에서는 다음을 확인합니다.
- HTTP
POST요청만 허용되며, 그 외의 경우 다른 HTTP 메서드가 허용되지 않음을 나타내는405상태 코드가 반환됩니다. 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 일괄 작업을 준비하고 있습니다. 책 세부정보로 구성된 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에 저장되었는지 확인할 수도 있습니다.

위 스크린샷에서 생성된 books 컬렉션, 도서 ISBN 코드로 식별된 도서 문서 목록, 오른쪽의 특정 도서 항목 세부정보를 확인할 수 있습니다.
클라우드에 함수 배포
Cloud Functions에 함수를 배포하려면 function-import 디렉터리에서 다음 명령어를 사용합니다.
$ gcloud functions deploy bulk-import \
--gen2 \
--trigger-http \
--runtime=nodejs20 \
--allow-unauthenticated \
--max-instances=30
--region=${REGION} \
--source=. \
--entry-point=parseBooks
bulk-import이라는 기호 이름으로 함수를 배포합니다. 이 함수는 HTTP 요청을 통해 트리거됩니다. Node.JS 20 런타임을 사용합니다. 함수를 공개적으로 배포합니다 (엔드포인트를 보호하는 것이 이상적임). 함수가 상주할 리전을 지정합니다. 로컬 디렉터리의 소스를 가리키고 내보낸 JavaScript 함수인 parseBooks를 진입점으로 사용합니다.
몇 분 이내에 함수가 클라우드에 배포됩니다. Cloud 콘솔 UI에 함수가 표시됩니다.

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

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자리Stringauthor- 책 저자의 이름을 나타내는 비어 있지 않은Stringlanguage- 책이 작성된 언어가 포함된 비어 있지 않은Stringpages- 책의 페이지 수에 대한 양수Integertitle- 책 제목이 포함된 비어 있지 않은Stringyear- 도서의 발행 연도를 나타내는Integer값입니다.
도서 페이로드의 예:
{
"isbn": "9780435272463",
"author": "Chinua Achebe",
"language": "English",
"pages": 209,
"title": "Things Fall Apart",
"year": 1958
}
GET /books
모든 도서 목록을 가져옵니다. 작가 또는 언어로 필터링할 수 있으며 한 번에 10개의 결과 창으로 페이지로 나눌 수 있습니다.
본문 페이로드: 없음
쿼리 매개변수:
author(선택사항) - 저자로 도서 목록을 필터링합니다.language(선택사항) - 언어로 도서 목록을 필터링합니다.page(선택사항, 기본값 = 0) - 반환할 결과 페이지의 순위를 나타냅니다.
반환: 도서 객체의 JSON 배열
상태 코드:
200- 책 목록 가져오기 요청이 성공한 경우400- 오류가 발생한 경우
POST /books 및 POST /books/{isbn}
isbn 경로 매개변수 (이 경우 도서 페이로드에 isbn 코드가 필요하지 않음)를 사용하거나 사용하지 않고 (이 경우 도서 페이로드에 isbn 코드가 있어야 함) 새 도서 페이로드를 게시합니다.
본문 페이로드: 책 객체
쿼리 매개변수: 없음
반환: 없음
상태 코드:
201- 도서가 성공적으로 저장된 경우406-isbn코드가 잘못된 경우400- 오류가 발생한 경우
GET /books/{isbn}
경로 매개변수로 전달된 isbn 코드로 식별되는 도서를 라이브러리에서 가져옵니다.
본문 페이로드: 없음
쿼리 매개변수: 없음
반환: 도서 JSON 객체 또는 도서가 없는 경우 오류 객체
상태 코드:
200- 도서가 데이터베이스에 있는 경우400- 오류가 발생한 경우404: 책을 찾을 수 없는 경우406-isbn코드가 잘못된 경우
PUT /books/{isbn}
경로 매개변수로 전달된 isbn로 식별되는 기존 도서를 업데이트합니다.
본문 페이로드: 책 객체입니다. 업데이트가 필요한 필드만 전달할 수 있으며 다른 필드는 선택사항입니다.
쿼리 매개변수: 없음
반환: 업데이트된 도서
상태 코드:
200: 책이 업데이트된 경우400- 오류가 발생한 경우406-isbn코드가 잘못된 경우
DELETE /books/{isbn}
경로 매개변수로 전달된 isbn로 식별되는 기존 도서를 삭제합니다.
본문 페이로드: 없음
쿼리 매개변수: 없음
반환: 없음
상태 코드:
204- 도서가 삭제되면400- 오류가 발생한 경우
8. 컨테이너에서 REST API 배포 및 노출
코드 살펴보기
Dockerfile
먼저 애플리케이션 코드를 컨테이너화하는 역할을 하는 Dockerfile을 살펴보겠습니다.
FROM node:20-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . ./
CMD [ "node", "index.js" ]
Node.JS 20 '슬림' 이미지를 사용하고 있습니다. /usr/src/app 디렉터리에서 작업합니다. 종속 항목을 정의하는 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": ">= 20.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를 사용한다고 지정합니다.
웹 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'],
}));
웹 프레임워크로 Express를 사용하여 REST API를 구현합니다. body-parser 모듈을 사용하여 API와 교환되는 JSON 페이로드를 파싱합니다.
querystring 모듈은 URL을 조작하는 데 유용합니다. 이는 페이지로 나누기 목적으로 Link 헤더를 만들 때 적용됩니다 (자세한 내용은 나중에 설명).
그런 다음 cors 모듈을 구성합니다. 대부분의 헤더는 일반적으로 삭제되지만 여기서는 페이지로 나누기에 지정할 Link 헤더뿐만 아니라 일반적인 콘텐츠 길이와 유형을 유지하고 싶으므로 CORS를 통해 전달할 헤더를 명시합니다.
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);
});
}
이전 섹션에서는 author 및 language로 필터링했지만 이 섹션에서는 마지막 업데이트 날짜 순서 (마지막 업데이트가 먼저 옴)로 도서 목록을 정렬합니다. 또한 반환할 요소 수인 한도와 다음 책 배치를 반환할 시작점인 오프셋을 정의하여 결과를 페이지로 나눕니다.
쿼리를 실행하고 데이터의 스냅샷을 가져와서 함수가 끝날 때 반환되는 JavaScript 배열에 결과를 넣습니다.
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);
처음에는 로직이 약간 복잡해 보일 수 있지만, 데이터의 첫 번째 페이지가 아닌 경우 이전 링크를 추가하는 것입니다. 데이터 페이지가 가득 차면(즉, PAGE_SIZE 상수로 정의된 최대 도서 수가 포함됨) 다음 링크가 추가됩니다(더 많은 데이터가 포함된 다른 페이지가 있다고 가정). 그런 다음 Express의 resource#links() 함수를 사용하여 올바른 문법으로 올바른 헤더를 만듭니다.
참고로 링크 헤더는 다음과 같이 표시됩니다.
link: </books?page=1>; rel="prev", </books?page=3>; rel="next"
POST /books및POST /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으로 식별되는 책을 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 속성은 책이 실제로 발견되었는지 확인하는 데 유용합니다. 그렇지 않으면 오류와 404 Not Found 상태 코드를 다시 전송합니다. 도서 데이터를 가져오고 반환할 도서를 나타내는 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}`});
}
});
Google에서는 해당 레코드를 마지막으로 업데이트한 시간을 기억하기 위해 updated 날짜/시간 필드를 업데이트합니다. 기존 필드를 새 값으로 대체하는 {merge:true} 전략을 사용합니다. 그렇지 않으면 모든 필드가 삭제되고 페이로드의 새 필드만 저장되어 이전 업데이트 또는 초기 생성의 기존 필드가 지워집니다.
또한 도서의 URI를 가리키도록 Location 헤더를 설정합니다.
DELETE /books/:isbn
도서 삭제는 매우 간단합니다. 문서 참조에서 delete() 메서드를 호출하기만 하면 됩니다. 콘텐츠를 반환하지 않으므로 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 테스트
REST API 코드를 실행하는 방법 (Node를 통해 직접 또는 Docker 컨테이너 이미지를 통해)과 관계없이 이제 몇 가지 쿼리를 실행할 수 있습니다.
- 새 도서 만들기 (본문 페이로드에 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의 클라우드에 배포할 때입니다.
다음 두 단계로 진행합니다.
- 먼저 다음 명령어를 사용하여 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 콘솔 UI에서 Cloud Run 서비스가 목록에 표시되는지 다시 한번 확인할 수 있습니다.

마지막으로 다음 명령어를 사용하여 새로 배포된 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와 상호작용하는 웹 프런트엔드를 제공하는 것입니다. 이를 위해 클라이언트 측 Fetch API를 사용하여 AJAX 요청을 통해 API를 호출하는 클라이언트 JavaScript 코드가 포함된 Google App Engine을 사용합니다.
애플리케이션은 Node.JS App Engine 런타임에 배포되지만 대부분 정적 리소스로 구성되어 있습니다. 대부분의 사용자 상호작용은 클라이언트 측 JavaScript를 통해 브라우저에서 이루어지므로 백엔드 코드는 많지 않습니다. 멋진 프런트엔드 JavaScript 프레임워크는 사용하지 않고 Shoelace 웹 구성요소 라이브러리를 사용하여 UI를 위한 몇 가지 웹 구성요소와 함께 '바닐라' JavaScript만 사용합니다.
- 도서의 언어를 선택하는 선택 상자

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

- 데이터베이스에서 더 많은 책을 로드하는 버튼이 있습니다.

이러한 모든 시각적 구성요소를 결합하면 라이브러리를 탐색하는 결과 웹페이지는 다음과 같이 표시됩니다.

app.yaml 구성 파일
app.yaml 구성 파일을 살펴보고 이 App Engine 애플리케이션의 코드베이스를 자세히 살펴보겠습니다. 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, JavaScript 클라이언트 측 코드 위치를 가리킵니다. 네 번째는 App Engine 애플리케이션의 루트 URL이 index.html 페이지를 가리켜야 함을 나타냅니다. 이렇게 하면 웹사이트의 루트에 액세스할 때 URL에 index.html 접미사가 표시되지 않습니다. 마지막 하나는 다른 모든 URL (/.*)을 Node.JS 애플리케이션으로 라우팅하는 기본 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.yaml에서 CHANGE_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를 사용하여 이 애플리케이션을 실행해야 합니다. 도서의 ISBN 코드를 검증하기 위해 Express 프레임워크와 isbn3 NPM 모듈을 사용합니다.
개발 종속 항목에서는 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 미들웨어에서 제공할 수 있는 정적 애셋이 포함되어 있습니다 (최소한 개발 모드에서 로컬로 실행할 때). 마지막으로 JSON 페이로드를 파싱하려면 body-parser이 필요합니다.
정의된 몇 가지 경로를 살펴보겠습니다.
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을 반환합니다. 이렇게 하면 클라이언트 측 JavaScript 코드가 도서 목록을 가져오기 위해 호출해야 하는 위치를 알 수 있습니다.
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 웹 구성요소 라이브러리 (스크립트와 스타일시트)를 가져옵니다.
다음 줄은 책 ISBN 코드의 바코드를 만들기 위해 JsBarcode 라이브러리를 가져옵니다.
마지막 줄은 public/ 하위 디렉터리에 있는 자체 JavaScript 코드와 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 클라이언트 측 JavaScript 코드입니다.
app.js 클라이언트 측 JavaScript 코드
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을 가져옵니다. 환경 변수 덕분에 JavaScript 클라이언트 측 코드에서 호출되는 /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 API를 호출하는 데 사용할 정확한 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}
11. 축하합니다.
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 데이터베이스를 사용하므로 실시간 기능을 활용하여 변경사항이 발생하면 표시된 도서 데이터를 업데이트합니다.