1. 개요
이 Codelab에서는 사용자가 웹 애플리케이션에서 사진을 업로드하고 업로드된 사진과 썸네일을 둘러볼 수 있게 하는 Google App Engine에 웹 프런트엔드를 만듭니다.
이 웹 애플리케이션은 멋진 사용자 인터페이스를 제공하기 위해 Bulma라는 CSS 프레임워크를 사용하며, 빌드할 애플리케이션의 API를 호출하는 Vue.JS JavaScript 프런트엔드 프레임워크를 사용합니다.
이 애플리케이션은 세 개의 탭으로 구성됩니다.
- 홈페이지 페이지에는 업로드된 모든 이미지의 썸네일과 사진을 설명하는 라벨 목록 (이전 실습에서 Cloud Vision API가 감지한 라벨)이 표시됩니다.
- 업로드된 가장 최근 사진 4개로 만든 콜라주를 표시하는 콜라주 페이지입니다.
- 사용자가 새 사진을 업로드할 수 있는 업로드 페이지
결과 프런트엔드는 다음과 같습니다.
이 세 페이지는 간단한 HTML 페이지입니다.
- 홈 페이지 (
index.html
)는/api/pictures
URL에 대한 AJAX 호출을 통해 노드 App Engine 백엔드 코드를 호출하여 미리보기 이미지 사진 및 라벨 목록을 가져옵니다. 홈페이지는 이 데이터를 가져오는 데 Vue.js를 사용합니다. - 콜라주 페이지 (
collage.html
)는 최신 사진 4장이 조합된collage.png
이미지를 가리킵니다. - 업로드 페이지 (
upload.html
)는 POST 요청을 통해 사진을/api/pictures
URL에 업로드할 수 있는 간단한 양식을 제공합니다.
학습할 내용
- 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/pictures
upload.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