1. 개요
이 Codelab에서는 사용자가 웹 애플리케이션에서 사진을 업로드하고 업로드된 사진과 썸네일을 둘러볼 수 있게 하는 Google App Engine에 웹 프런트엔드를 만듭니다.

이 웹 애플리케이션은 멋진 사용자 인터페이스를 제공하기 위해 Bulma라는 CSS 프레임워크를 사용하며, 빌드할 애플리케이션의 API를 호출하는 Vue.JS JavaScript 프런트엔드 프레임워크를 사용합니다.
이 애플리케이션은 세 개의 탭으로 구성됩니다.
- 홈페이지 페이지에는 업로드된 모든 이미지의 썸네일과 사진을 설명하는 라벨 목록 (이전 실습에서 Cloud Vision API가 감지한 라벨)이 표시됩니다.
- 업로드된 가장 최근 사진 4개로 만든 콜라주를 표시하는 콜라주 페이지입니다.
- 사용자가 새 사진을 업로드할 수 있는 업로드 페이지
결과 프런트엔드는 다음과 같습니다.

이 세 페이지는 간단한 HTML 페이지입니다.
- 홈 페이지 (
index.html)는/api/picturesURL에 대한 AJAX 호출을 통해 노드 App Engine 백엔드 코드를 호출하여 미리보기 이미지 사진 및 라벨 목록을 가져옵니다. 홈페이지는 이 데이터를 가져오는 데 Vue.js를 사용합니다. - 콜라주 페이지 (
collage.html)는 최신 사진 4장이 조합된collage.png이미지를 가리킵니다. - 업로드 페이지 (
upload.html)는 POST 요청을 통해 사진을/api/picturesURL에 업로드할 수 있는 간단한 양식을 제공합니다.
학습할 내용
- App Engine
- Cloud Storage
- Cloud Firestore
2. 설정 및 요구사항
자습형 환경 설정
- Google Cloud Console에 로그인하여 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다. 아직 Gmail이나 Google Workspace 계정이 없는 경우 계정을 만들어야 합니다.



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

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

가상 머신에는 필요한 개발 도구가 모두 들어있습니다. 영구적인 5GB 홈 디렉토리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 실습의 모든 작업은 브라우저만으로 수행할 수 있습니다.
3. API 사용 설정
App Engine에는 Compute Engine API가 필요합니다. 사용 설정되어 있는지 확인합니다.
gcloud services enable compute.googleapis.com
작업이 성공적으로 완료된 것으로 표시됩니다.
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. 코드 클론
아직 코드를 결제하지 않았다면 다음 단계를 따르세요.
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
그런 다음 프런트엔드가 포함된 디렉터리로 이동하면 됩니다.
cd serverless-photosharing-workshop/frontend
프런트엔드의 파일 레이아웃은 다음과 같습니다.
frontend
|
├── index.js
├── package.json
├── app.yaml
|
├── public
|
├── index.html
├── collage.html
├── upload.html
|
├── app.js
├── script.js
├── style.css
프로젝트 루트에는 다음과 같은 3개의 파일이 있습니다.
index.js에는 Node.js 코드가 포함됩니다.package.json는 라이브러리 종속 항목을 정의합니다.app.yaml는 Google App Engine의 구성 파일입니다.
public 폴더에는 정적 리소스가 포함됩니다.
index.html에는 모든 썸네일 사진과 라벨이 표시되는 페이지입니다.collage.html은(는) 최근 사진의 콜라주를 표시합니다.upload.html에 새 사진을 업로드하기 위한 양식이 포함되어 있습니다.app.js는 Vue.js를 사용하여index.html페이지를 데이터로 채웁니다.script.js가 탐색 메뉴와 '햄버거'를 처리합니다. 작은 화면의 아이콘style.css는 일부 CSS 지시어를 정의합니다.
5. 코드 살펴보기
종속 항목
package.json 파일은 필요한 라이브러리 종속 항목을 정의합니다.
{
"name": "frontend",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@google-cloud/firestore": "^3.4.1",
"@google-cloud/storage": "^4.0.0",
"express": "^4.16.4",
"dayjs": "^1.8.22",
"bluebird": "^3.5.0",
"express-fileupload": "^1.1.6"
}
}
적용 대상이 되는 요소는 다음과 같습니다.
- firestore: 사진 메타데이터로 Cloud Firestore에 액세스하기 위해
- storage: 사진이 저장된 Google Cloud Storage에 액세스하기 위해
- express: Node.js용 웹 프레임워크
- dayjs: 사람이 읽을 수 있는 방식으로 날짜를 표시하는 작은 라이브러리
- bluebird: JavaScript 프로미스 라이브러리
- express-fileupload: 파일 업로드를 쉽게 처리하는 라이브러리입니다.
익스프레스 프런트엔드
index.js 컨트롤러의 시작 부분에서는 앞에서 package.json에 정의된 모든 종속 항목이 필요합니다.
const express = require('express');
const fileUpload = require('express-fileupload');
const Firestore = require('@google-cloud/firestore');
const Promise = require("bluebird");
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
const path = require('path');
const dayjs = require('dayjs');
const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(relativeTime)
다음으로 Express 애플리케이션 인스턴스가 생성됩니다.
다음 두 가지 익스프레스 미들웨어가 사용됩니다.
express.static()호출은public하위 디렉터리에서 정적 리소스를 사용할 수 있음을 나타냅니다.- 또한
fileUpload()는 파일 크기를 10MB로 제한하도록 파일 업로드를 구성하여/tmp디렉터리의 인메모리 파일 시스템에서 로컬로 파일을 업로드합니다.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
limits: { fileSize: 10 * 1024 * 1024 },
useTempFiles : true,
tempFileDir : '/tmp/'
}))
정적 리소스 중에는 홈페이지, 콜라주 페이지, 업로드 페이지용 HTML 파일이 있습니다. 이러한 페이지는 API 백엔드를 호출합니다. 이 API의 엔드포인트는 다음과 같습니다.
POST /api/picturesupload.html의 양식을 통해 POST 요청을 통해 사진이 업로드됩니다.GET /api/pictures이 엔드포인트는 사진 및 라벨 목록이 포함된 JSON 문서를 반환합니다.GET /api/pictures/:name이 URL은 원본 크기 이미지의 클라우드 스토리지 위치로 리디렉션됩니다.GET /api/thumbnails/:name이 URL은 썸네일 이미지의 클라우드 스토리지 위치로 리디렉션됩니다.GET /api/collage이 마지막 URL은 생성된 콜라주 이미지의 클라우드 스토리지 위치로 리디렉션됩니다.
사진 업로드
사진 업로드 Node.js 코드를 살펴보기 전에 public/upload.html를 간단히 살펴보세요.
...
<form method="POST" action="/api/pictures" enctype="multipart/form-data">
...
<input type="file" name="pictures">
<button>Submit</button>
...
</form>
...
양식 요소가 HTTP POST 메서드 및 여러 부분으로 구성된 형식을 사용하여 /api/pictures 엔드포인트를 가리킵니다. 이제 index.js는 해당 엔드포인트와 메서드에 응답하고 파일을 추출해야 합니다.
app.post('/api/pictures', async (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
console.log("No file uploaded");
return res.status(400).send('No file was uploaded.');
}
console.log(`Receiving files ${JSON.stringify(req.files.pictures)}`);
const pics = Array.isArray(req.files.pictures) ? req.files.pictures : [req.files.pictures];
pics.forEach(async (pic) => {
console.log('Storing file', pic.name);
const newPicture = path.resolve('/tmp', pic.name);
await pic.mv(newPicture);
const pictureBucket = storage.bucket(process.env.BUCKET_PICTURES);
await pictureBucket.upload(newPicture, { resumable: false });
});
res.redirect('/');
});
먼저 실제로 업로드 중인 파일이 있는지 확인합니다. 그런 다음 파일 업로드 노드 모듈에서 가져온 mv 메서드를 통해 로컬로 파일을 다운로드합니다. 이제 로컬 파일 시스템에서 파일을 사용할 수 있으므로 Cloud Storage 버킷에 사진을 업로드합니다. 마지막으로 사용자를 애플리케이션의 기본 화면으로 다시 리디렉션합니다.
사진 나열하기
이제 멋진 사진을 보여주세요.
/api/pictures 핸들러에서 Firestore 데이터베이스의 pictures 컬렉션을 살펴보고 썸네일이 생성된 모든 사진을 생성 날짜 내림차순으로 정렬합니다.
각 사진을 이름, 사진을 설명하는 라벨 (Cloud Vision API에서 가져옴), 주요 색상, 친숙한 생성 날짜 (dayjs 사용 시 '지금부터 3일 후'와 같은 상대적 시차)와 함께 JavaScript 배열에 푸시합니다.
app.get('/api/pictures', async (req, res) => {
console.log('Retrieving list of pictures');
const thumbnails = [];
const pictureStore = new Firestore().collection('pictures');
const snapshot = await pictureStore
.where('thumbnail', '==', true)
.orderBy('created', 'desc').get();
if (snapshot.empty) {
console.log('No pictures found');
} else {
snapshot.forEach(doc => {
const pic = doc.data();
thumbnails.push({
name: doc.id,
labels: pic.labels,
color: pic.color,
created: dayjs(pic.created.toDate()).fromNow()
});
});
}
console.table(thumbnails);
res.send(thumbnails);
});
이 컨트롤러는 다음 형태의 결과를 반환합니다.
[
{
"name": "IMG_20180423_163745.jpg",
"labels": [
"Dish",
"Food",
"Cuisine",
"Ingredient",
"Orange chicken",
"Produce",
"Meat",
"Staple food"
],
"color": "#e78012",
"created": "a day ago"
},
...
]
이 데이터 구조는 index.html 페이지의 작은 Vue.js 스니펫에 의해 사용됩니다. 다음은 해당 페이지의 마크업을 단순화한 버전입니다.
<div id="app">
<div class="container" id="app">
<div id="picture-grid">
<div class="card" v-for="pic in pictures">
<div class="card-content">
<div class="content">
<div class="image-border" :style="{ 'border-color': pic.color }">
<a :href="'/api/pictures/' + pic.name">
<img :src="'/api/thumbnails/' + pic.name">
</a>
</div>
<a class="panel-block" v-for="label in pic.labels" :href="'/?q=' + label">
<span class="panel-icon">
<i class="fas fa-bookmark"></i>
</span>
{{ label }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
div의 ID는 Vue.js가 동적으로 렌더링되는 마크업의 일부임을 나타냅니다. 반복은 v-for 지시어를 통해 수행됩니다.
Cloud Vision API에서 확인할 수 있는 것처럼 그림의 주요 색상에 해당하는 멋진 색상의 테두리가 있으며 링크 및 이미지 소스의 썸네일과 전체 너비 사진을 가리킵니다.
마지막으로, 사진을 설명하는 라벨을 나열합니다.
다음은 index.html 페이지 하단에서 가져온 public/app.js 파일에 있는 Vue.js 스니펫의 JavaScript 코드입니다.
var app = new Vue({
el: '#app',
data() {
return { pictures: [] }
},
mounted() {
axios
.get('/api/pictures')
.then(response => { this.pictures = response.data })
}
})
Vue 코드는 Axios 라이브러리를 사용하여 /api/pictures 엔드포인트에 AJAX를 호출합니다. 그런 다음 반환된 데이터는 앞에서 본 마크업의 보기 코드에 바인딩됩니다.
사진 보기
사용자는 index.html에서 사진의 썸네일을 보고 클릭하여 전체 크기의 이미지를 볼 수 있으며 collage.html에서 collage.png 이미지를 볼 수 있습니다.
이러한 페이지의 HTML 마크업에서 이미지 src와 링크 href는 3개의 엔드포인트를 가리키며, 이러한 엔드포인트는 사진, 썸네일, 콜라주의 Cloud Storage 위치로 리디렉션됩니다. HTML 마크업에 경로를 하드 코딩할 필요가 없습니다.
app.get('/api/pictures/:name', async (req, res) => {
res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_PICTURES}/${req.params.name}`);
});
app.get('/api/thumbnails/:name', async (req, res) => {
res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_THUMBNAILS}/${req.params.name}`);
});
app.get('/api/collage', async (req, res) => {
res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_THUMBNAILS}/collage.png`);
});
Node 애플리케이션 실행
모든 엔드포인트가 정의되었으므로 Node.js 애플리케이션을 실행할 수 있습니다. Express 애플리케이션은 기본적으로 포트 8080에서 수신 대기하며 수신 요청을 처리할 준비가 되어 있습니다.
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Started web frontend service on port ${PORT}`);
console.log(`- Pictures bucket = ${process.env.BUCKET_PICTURES}`);
console.log(`- Thumbnails bucket = ${process.env.BUCKET_THUMBNAILS}`);
});
6. 로컬에서 테스트
클라우드에 배포하기 전에 로컬에서 코드를 테스트하여 작동하는지 확인합니다.
2개의 Cloud Storage 버킷에 해당하는 2개의 환경 변수를 내보내야 합니다.
export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
frontend 폴더 내에 npm 종속 항목을 설치하고 서버를 시작합니다.
npm install; npm start
모든 것이 잘 진행되었다면 포트 8080에서 서버를 시작합니다.
Started web frontend service on port 8080
- Pictures bucket = uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
- Thumbnails bucket = thumbnails-${GOOGLE_CLOUD_PROJECT}
버킷의 실제 이름이 로그에 표시되므로 디버깅에 유용합니다.
Cloud Shell에서 웹 미리보기 기능을 사용하여 로컬에서 실행되는 애플리케이션을 브라우저할 수 있습니다.

종료하려면 CTRL-C을(를) 사용하세요.
7. App Engine에 배포
애플리케이션을 배포할 준비가 되었습니다.
App Engine 구성
App Engine의 app.yaml 구성 파일을 검사합니다.
runtime: nodejs16 env_variables: BUCKET_PICTURES: uploaded-pictures-GOOGLE_CLOUD_PROJECT BUCKET_THUMBNAILS: thumbnails-GOOGLE_CLOUD_PROJECT
첫 번째 줄은 런타임이 Node.js 10을 기반으로 한다고 선언합니다. 원본 이미지와 썸네일 이미지의 두 버킷을 가리키도록 두 개의 환경 변수가 정의됩니다.
GOOGLE_CLOUD_PROJECT를 실제 프로젝트 ID로 바꾸려면 다음 명령어를 실행합니다.
sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml
배포
App Engine에 사용할 지역을 설정합니다. 이전 실습에서 사용한 것과 동일한 지역을 사용해야 합니다.
gcloud config set compute/region europe-west1
그리고 다음을 배포합니다.
gcloud app deploy
1~2분 후에 애플리케이션이 트래픽을 제공하고 있다는 메시지가 표시됩니다.
Beginning deployment of service [default]... ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 8 files to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [default]...done. Setting traffic split for service [default]...done. Deployed service [default] to [https://GOOGLE_CLOUD_PROJECT.appspot.com] You can stream logs from the command line by running: $ gcloud app logs tail -s default To view your application in the web browser run: $ gcloud app browse
또한 Cloud 콘솔의 App Engine 섹션을 방문하여 앱이 배포되었는지 확인하고 버전 관리 및 트래픽 분할과 같은 App Engine의 기능을 살펴볼 수 있습니다.

8. 앱 테스트
테스트하려면 앱 (https://<YOUR_PROJECT_ID>.appspot.com/) 앱의 기본 App Engine URL로 이동하면 프런트엔드 UI가 실행되고 있는 것을 확인할 수 있습니다.

9. 정리(선택 사항)
앱을 유지하지 않으려면 전체 프로젝트를 삭제하여 리소스를 정리하여 비용을 절감하고 전반적으로 클라우드를 효율적으로 활용할 수 있습니다.
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
10. 축하합니다.
축하합니다. App Engine에서 호스팅되는 Node.js 웹 애플리케이션은 모든 서비스를 결합하고 사용자가 사진을 업로드하고 시각화할 수 있도록 합니다.
학습한 내용
- App Engine
- Cloud Storage
- Cloud Firestore