소스 :
https://github.com/braverokmc79/pwa-ex-11
기본적으로 nodejs 설치 및 vue 와 Vue CLI 설치 되어 있어야 한다.
nodejs 설치
https://nodejs.org/ko/download/
nodejs 설치 후
vue 설치
npm install vue
명령을 사용하여 설치한 버전 번호를 확인합니다(vue --version).
Vue CLI를 설치하려면 npm을 사용합니다. 업그레이드하려면 -g 플래그를 사용하여 전역으로 설치해야 합니다(vue upgrade --next).
1 npm install -g @vue/cli
프로젝트 결과 화면
1. vue add vuetify 설치
2. npm install firebase vuefire 설치
3. 매니페스트 작성
public/manifest.json
{ "name": "카메라 갤러리", "short_name": "카메라 갤러리", "icons": [ { "src": "./img/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "./img/icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "start_url": "./index.html", "display": "standalone", "background_color": "#ffffff", "theme_color": "#ffffff" }
4. 파이어베이스 프로젝트 만들기
https://console.firebase.google.com/
1) firebase.google.com 에 접속해서 pwa-camera 라는 이름으로 프로젝트 만들기
2) 파이어베이스 프로젝트 설정 화면에서 웹앱에서 파이어베이스 추가하기 (닉네임 pwa-camera 로 등록)
3) 파이어베이스 SDK 추가에서 databaseURL, storageBucket 값을 복사해서 기록해 두기
4) Realtime Database 만들기 -> 테스트 모드로 시작
5. 파이어베이스 스토리지 및 DB 연동 파일 생성하기
1) src/datasources/firebase.js
// 파이어베이스 앱 객체 모듈 가져오기 import firebase from 'firebase/compat/app' // 파이어베이스 패키지 모듈 가져오기 import 'firebase/compat/database'; import 'firebase/compat/storage'; // 파이어베이스 DB를 초기화 const oFirebase = firebase.initializeApp({ // 파이어베이스 콘솔에서 복사하여 붙여넣기 databaseURL: "https://pwa-camera.firebaseio.com", storageBucket: "pwa-camera.appspot.com", }); // 파이어베이스 DB객체 연결 const oDB = oFirebase.database(); // 파이어베이스 DB객체 중에서 pictures 항목을 다른 곳에서 사용하도록 공개 export const oPicturesinDB = oDB.ref('pictures'); // 파이어베이스 스토리지 객체 공개 export const oStorage = oFirebase.storage();
2)src/main.js 에 다음을 추가
//뷰파이어 노드 모듈 가져와서 Vue에 연결 import { rtdbPlugin } from "vuefire"; Vue.use(rtdbPlugin);
import Vue from "vue"; import App from "./App.vue"; import "./registerServiceWorker"; import router from "./router"; import vuetify from "./plugins/vuetify"; //뷰파이어 노드 모듈 가져와서 Vue에 연결 import { rtdbPlugin } from "vuefire"; Vue.use(rtdbPlugin); Vue.config.productionTip = false; new Vue({ router, vuetify, render: (h) => h(App), }).$mount("#app");
3) DB 연동 테스트 확인
App.vue 파일에 firebase.js 파일 임포트 후 확인
<script> // 파이어베이스 DB 객체 가져옴 import { oPicturesinDB } from '@/datasources/firebase' console.log("oPicturesinDB : " ,oPicturesinDB); export default { name: 'App', data: () => ({ // }), }; </script>
6. 앱 실행 화면 만들기
1) public/index.html 수정
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <!-- 상태 표시줄 테마 색상을 흰색으로 변경 --> <meta name="theme-color" content="#ffffff"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title>카메라 갤러리</title> <!-- 구글 머티리얼 디자인 아이콘 추가--> <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"> </head> <body> <noscript> <strong>We're sorry but ex11 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
2)datasources/picture-data.js 파일 추가
export default { aPictures: [{ 'id': 1, 'url': 'https://farm1.staticflickr.com/654/22663129542_e3df218c90_b.jpg', 'title': '청춘플랫폼_우수부스투표', 'info': '졸업작품 전시회에 출품한 작품 중에서 방문자들이 직접 우수 전시 부스를 투표로 선정하는 모습입니다.' }, { 'id': 2, 'url': 'https://farm1.staticflickr.com/739/22663128752_7b347e01a9_b.jpg', 'title': '전시장모습', 'info': '전시가 열렸던 예술의 전당 전시실 안의 모습입니다. ' }, { 'id': 3, 'url': 'https://farm1.staticflickr.com/674/22055529403_aeeb5fc371_b.jpg', 'title': 'pc실습실', 'info': '윈도우OS 환경에서 다양한 멀티미디어디자인 S/W를 실습하는 공간입니다.' }, { 'id': 4, 'url': 'https://farm1.staticflickr.com/706/22650669476_d8bf33c153_b.jpg', 'title': '졸업작품전시회 주제', 'info': '청춘 플랫폼이라는 주제로 젊은 열정이 펼쳐지는 무대가 마련되었습니다.' }, { 'id': 5, 'url': 'https://farm1.staticflickr.com/677/22055530223_91e3f73227_b.jpg', 'title': '명함디자인', 'info': '자신의 미래 모습을 담은 명함을 직접 디자인하고 인쇄 제작하여 전시한 모습입니다.' }, { 'id': 6, 'url': 'https://farm1.staticflickr.com/777/22650669326_e180cd84ca_b.jpg', 'title': '전시_방명록', 'info': '전시장을 방문한 관객들의 다양한 소감을 방명록의 기록으로 남겨졌습니다.' }, { 'id': 7, 'url': 'https://farm1.staticflickr.com/759/22687855771_92dc8b3245_b.jpg', 'title': '매킨토시 실습실', 'info': '맥OS 환경에서 멀티미디어디자인 관련 S/W를 사용하여 작업하기 위한 맥 실습실입니다.' }, { 'id': 8, 'url': 'https://farm6.staticflickr.com/5646/22053918354_ee031eae46_b.jpg', 'title': '캘리그래피_디자인', 'info': '학생들이 직접 디자인한 아날로그 정서의 캘리그래피 작품입니다.' }, { 'id': 9, 'url': 'https://farm1.staticflickr.com/762/22687856291_963df9b90a_b.jpg', 'title': '실습실복도', 'info': '전공존에 따라 색상이 그룹으로 연결된 실습실 복도 모습입니다.' }, { 'id': 10, 'url': 'https://farm1.staticflickr.com/766/22055529593_c162dc3143_b.jpg', 'title': '인터뷰_촬영', 'info': '졸업작품을 준비한 학생들의 과정을 인터뷰의 기록으로 남기는 모습입니다. ' }, { 'id': 11, 'url': 'https://farm6.staticflickr.com/5775/22053918014_f0e0873b7c_b.jpg', 'title': '전시_메모', 'info': '관객들이 전시를 보면서 느낀 점을 간단한 메모의 글로 남긴 흔적입니다.' }, { 'id': 12, 'url': 'https://farm6.staticflickr.com/5672/22055530083_8f633d57f3_b.jpg', 'title': '종이컵_캘리그래피', 'info': '종이컵을 캔버스 삼아 학생들의 자유로운 생각을 캘리그래피의 작품으로 제작한 종이컵 아트워크입니다.' } ] }
3)src/router/index.js 라우터 추가 및 변경
[4페이지]
· 홈(home_page)
· 세부 정보(info_page)
· 포스트(post_page)
· 카메라 촬영(camera_page)
import Vue from "vue"; import VueRouter from "vue-router"; Vue.use(VueRouter); const routes = [ { path: "/", name: "home_page", component: () => import("@/components/home_page.vue"), }, { path: "/camera", name: "camera_page", component: () => import("@/components/camera_page.vue"), }, { path: "/info", name: "info_page", component: () => import("@/components/info_page.vue"), }, { path: "/post", name: "post_page", component: () => import("@/components/post_page.vue"), }, ]; const router = new VueRouter({ routes, mode: "history", }); export default router;
4) App.vue
<template> <v-app> <v-app-bar app color="red" dark fixed> <!-- 홈 화면이 아닌 경우 돌아가기 버튼 표시 --> <v-btn icon v-if="$route.name !== 'home_page'" @click="$router.go(-1) "> <v-icon>arrow_back</v-icon> </v-btn> <v-toolbar-title>카메라 갤러리</v-toolbar-title> <v-spacer></v-spacer> <!-- 홈 화면에서만 촬영아이콘 표시--> <v-btn icon v-if="$route.name=='home_page'" @click="$router.push('/camera')"> <v-icon>camera_alt</v-icon> </v-btn> </v-app-bar> <!-- 라우터에 등록했던 홈, 카메라 촬영, 상세 정보, 포스트 페이지가 표시 --> <v-main> <router-view /> </v-main> </v-app> </template> <script> export default { name: 'App' } </script>
7. 컴포넌트 작성하기
1) 홈컴포넌트 작성하기
home_page.vue
<template> <v-container> <v-row> <v-col cols="12" sm="6" md="4" lg="3" xl="2" v-for="item in this.oPictures" :key="item.key" > <v-card class="py-2 px-2"> <!-- 사진을 읽어서 표시하고 세부 페이지로 이동하도록 링크 설정 --> <v-img :src="item.url" height="200px" class="pointer" @click="fnDisPlayInfo(item['.key'])"> </v-img> <!-- 사진의 제목과 내용을 바인딩하여 표시 --> <v-card-title> <h1 class="title grey--text text-darken-3 mb-3">{{ item.title }}</h1> <p class="body-1 grey--text">{{ item.info }}</p> </v-card-title> </v-card> </v-col> <v-col cols="12" class="text-cener"> <!-- 만약에 업로된 이미지가 없으면 안내 문구 표시 --> <p v-if="!this.oPictures.length"> 사진이 없습니다. 추가해 주세요! </p> </v-col> <!-- 오른쪽 하단에 포스트 추가 버튼 표시 --> <v-btn @click="$router.push('/post')" color="red" dark fixed bottom right fab> <v-icon>add</v-icon> </v-btn> </v-row> </v-container> </template> <script> //파이어베이스 DB 객체 가져옴 import {oPicturesinDB} from "@/datasources/firebase"; export default { name: 'App', data(){ return{ oPictures:[] //사진 데이터 저장 변수 } }, //파이어베이스와 연결된 뷰파이어 oPictures 객체 준비 firebase:{oPictures:oPicturesinDB}, methods:{ //라우터를 이용해서 세부 페이지로 이동할 때 사진의 ID 전달 fnDisPlayInfo(pID){ this.$router.push({ name:"info_page", params:{p_id:pID} }) } } }; </script> <style> /* 마우스 커서가 손 모양이 되도록 설정 */ .pointer{ cursor: pointer; } .text-cener{ text-align: center; } </style>
2) 세부정보 컴포넌트 작성하기
info_page.vue
<template> <v-container> <v-row> <v-col cols="12"> <v-card class="py-3 px-3"> <!-- contain 어트리뷰트를 사용해서 세부 사진을 컨테이너 크기에 맞도록 자동으로 조절하여 표시 --> <v-img height="450px" contain :src="this.itemPic.url" ></v-img> <!-- 제목이 있는 경우만 하단에 제목과 내용 표시 --> <v-card-text v-if="this.itemPic.title"> <h1 class="headline mt-1 txt-center">{{ this.itemPic.title }}</h1> <p class="body-1 mt-1 text-center">{{ this.itemPic.info }}</p> </v-card-text> <v-col cols="12" class="mt-3 text-center"> <!-- 현재 사진을 삭제하는 버튼 처리 --> <v-btn color="grey" fab dark @click="fnDeleteItem()"> <v-icon>delete</v-icon> </v-btn> </v-col> </v-card> </v-col> </v-row> </v-container> </template> <script> //파이어베이스에서 DB와 스토리지 객체 가져옴 import {oStorage, oPicturesinDB} from "@/datasources/firebase"; export default { name: 'App', //파이어베이스와 연결된 뷰파이어 oPictures 객체 준비 firebase:{oPictures:oPicturesinDB}, data(){ return{ oPictures:[], //사진 데이터 저장 변수 itemPic:null, //검색 결과 항목을 저장할 객체 변수 } }, created(){ //라우터의 매개변수로 전달된 항목 ID값 읽기 const itemID =this.$route.params.p_id; //find 검색 기능으로 파이어베이스에서 해당 ID 항목 검색 및 저장 this.itemPic =this.oPictures.find(item=> item['.key']===itemID); }, methods: { fnDeleteItem() { // 파이어베이스 DB의 사진 항목 삭제 oPicturesinDB.child(this.itemPic['.key']).remove() // 스토리지에 이미지가 존재할 경우(카메라 사용)만 삭제 if (this.itemPic['filename']) oStorage.ref('images').child(this.itemPic['filename']).delete() // 홈화면으로 이동 this.$router.push('/'); } }, }; </script>
3) 포스트 컴포넌트 작성하기
post_page.vue
<template> <v-container> <!-- 첫 번째 행에는 사진 표시 --> <v-row mt-4> <v-col offset="1" cols="10"> <v-card> <v-img height="200px" :src="this.sPicUrl"></v-img> </v-card> </v-col> </v-row> <!-- 두 번째 행에는 기본 제목을 표시하고 수정할 수 있도록 함 --> <v-row mt-5> <v-col offset="2" cols="8"> <v-text-field name="title" label="사진 제목" v-model="sTitle" autofocus></v-text-field> </v-col> </v-row> <!-- 세번째 행에는 기본 내용을 표시하고 수정할 수 있도록 함 --> <v-row> <v-col offset="2" cols="8"> <!-- 3줄로 편집 제한--> <v-text-field name="info" label="사진 설명" v-model="sInfo" multi-line rows="3"></v-text-field> </v-col> </v-row> <v-row> <v-col cols="12" class="text-center"> <v-btn color="orange" darak large @click="fnSubmitPost()">업로드</v-btn> </v-col> </v-row> </v-container> </template> <script> //JSON 파일로부터 이미지 정보 가져옴 import oPictureData from '@/datasources/picture-data'; //파이어베이스에서 DB 객체 가져옴. import {oPicturesinDB} from '@/datasources/firebase'; export default { //파이어베이스와 연결된 firebase : { oPictures:oPicturesinDB }, name: 'App', data(){ return{ oPictures:[], //사진 데이터 저장 변수 //초깃값으로 JSON 파일의 이미지 배열 저장 aPictures:oPictureData.aPictures, sTitle:'', sInfo:'', sPicUrl:'' } } , mounted() { //JSON 파일에서 사진 정보를 랜덤으로 읽어 와서 사진과 포스트 글 준비 let nIndex=Math.floor(Math.random()*12); const itemPic=this.aPictures[nIndex]; this.sTitle=itemPic.title; this.sInfo=itemPic.info; this.sPicUrl=itemPic.url; }, methods: { fnSubmitPost(){ //DB에 저장하고 홈 화면으로 이동 oPicturesinDB.push({ 'url':this.sPicUrl, 'title':this.sTitle, 'info':this.sInfo }).then(this.$router.push('/')) } }, }; </script>
4) 카메라 촬영 컴포넌트 작성하기
파이어베이스에서 storage 사용 설정을 한다.
다음화면은 storage 사용 설정 후 동영상 업로드 목록 화면
카메라 영상 표시하기
카메라 영상을 실시간으로 표시하려면 video 엘리먼트를 사용하면 된다.
그리고 video 엘리먼트에 담긴 영상을 갤러리에 전달하려면 ref 어트리뷰트를 지정해야 한다.
camera_page.vue
<template> <v-container> <v-row> <v-col cols="12" class="text-center"> <!-- 카메라 영상 부분을 표시 --> <video ref="rVideo" class="style_video"></video> </v-col> <v-col cols="12" class="mt-5 text-center"> <!-- 만약에 업로드된 이미지가 없으면 안내 문구 표시--> <p>현재 iOS는 지원하지 않습니다.</p> </v-col> </v-row> <div class="text-center my-3"> <!-- 카메라 캡처 버튼을 영상 하단 중앙에 위치 --> <v-btn v-if="!this.bIsWait" color="red" fab dark bottom @click="fnCameraCapture( )"> <v-icon>camera</v-icon> </v-btn> <v-progress-circular v-if="this.bIsWait" :size="50" indeterminate color="grey"></v-progress-circular> </div> </v-container> </template> <script> // 파이어베이스에서 DB와 스토리지 객체 가져옴 import { oStorage, oPicturesinDB } from '@/datasources/firebase' export default { // 파이어베이스와 연결된 뷰파이어 oPictures 객체 준비 firebase: { oPictures: oPicturesinDB }, data() { return { oPictures: [], // 사진 데이터 저장 변수 oVideoStream: null, // 카메라 영상 스트림을 저장할 객체변수 bIsWait: false } }, mounted() { // Web API를 통해서 사용자 카메라의 접근(영상 only)을 요청함 navigator.mediaDevices.getUserMedia({ video: true }).then(pVideoStream => { // 카메라 영상 스트림 정보를 oVideoStream에 저장함 this.oVideoStream = pVideoStream // 카메라 영상 스트림 정보를 video 엘리먼트에 표시함 this.$refs.rVideo.srcObject = pVideoStream this.$refs.rVideo.play() }).catch(function (err) { console.log(err) }) }, destroyed() { // 현재 화면을 종료할 경우 현재 재생되는 영상 트랙을 찾아 종료시킴 const oTrack = this.oVideoStream.getTracks() oTrack.map(pTrack => pTrack.stop()) }, methods: { fnCameraCapture() { this.bIsWait = true // 현재 재생되는 트랙을 찾아 스틸이미지로 캡처함 const oVideoTrack = this.oVideoStream.getVideoTracks()[0] let oCapturedImage = new window.ImageCapture(oVideoTrack) const options = { imageHeight: 359, imageWidth: 640, fillLightMode: 'off' }; const self = this // 캡처된 이미지를 파이어베이스 DB와 스토리지에 저장함 return oCapturedImage.takePhoto(options).then(pImageData => { // 영상 정지 const oTrack = self.oVideoStream.getTracks() oTrack.map(pTrack => pTrack.stop()) console.log('캡처: ' + pImageData.type + ', ' + pImageData.size + '바이트'); // 저장할 이미지 파일이름으로 사용할 ID 준비 const nID = new Date().toISOString() //pImageData 서버에 저장 처리 하면 된다. 여기서는 // 파이어베이스 스토리지에 이미지 파일 저장 let uploadTask = oStorage.ref('images').child('pic' + nID).put(pImageData); uploadTask.on('state_changed', function (snapshot) { // state_changed 이벤트를 통해서 얼만큼의 바이트가 업로드 중인지 콘솔에 표시 let progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; console.log('업로드: ' + progress + '% 완료', snapshot.state); }, function (error) { console.log(error) // 오류 발생 시 콘솔에 표시 }, function () { // 성공적으로 업로드 완료 후 파이어베이스 DB에 정보 저장 uploadTask.snapshot.ref.getDownloadURL().then(function (downloadURL) { console.log('업로드URL:', downloadURL); oPicturesinDB.push({ 'url': downloadURL, 'title': '', 'info': '', 'filename': 'pic' + nID }) .then(self.$router.push('/')) // 저장 후 홈 화면으로 이동 }); }); }) } } } </script> <style> /* 카메라 영상의 너비값을 브라우저 너비에 맞춤 */ .style_video { width: 100% } </style>
8.워크박스로 서비스 워커에서 캐시 관리하기
카메라 사진 갤러리 앱 만들기 예제에서 카메라로 직접 찍은 사진은 기본 캐시가 되지 않으므로 InjectMainfest 모드를 통해 캐시해야 한다.
지금까지는 캐시를 관리할 때 GenerateSW 모드를 사용하며 프리캐시와 런타임 캐시를 쉽고 빠르게 설정값으로 관리할 수 있었다. 하지만 프로젝트 성
격상 서비스 워커에서 직접코드를 넣어야 할 경우가 발생하면 GenerateSW 모드 대신에 InjectManifest 모드를 사용해야 한다. 이렇게 하면
서비스 워커에 자신의 코드를 넣을 수 있으며 프리캐시와 런타임 캐시를 직접 관리할 수 있다.
기존
vue.config.js
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: [ 'vuetify' ] })
InjectManifest 플러그인 모드는 vue.config.js 파일을 루트 폴더에 생성한 후에 다음 내용을 입력
다음과 같이 변경한다.
vue.config.js
module.exports = { pwa: { // 서비스워커를 코드로 수정하기 위해 InjectManifest 모드 사용 workboxPluginMode: 'InjectManifest', workboxOptions: { swSrc: "./src/service-worker.js" } } }
src 폴더에 service-worker.js 를 추가
importScripts( "https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js" ); // <-- workbox 최신번전 CDN 추가 //워크박스는 캐시를 수행할 때 진행 상황을 콘솔 창에 표시해 줍니다. //개발하면서 이러한 메시지를 볼 수 있으면 편리한데 디버그 모드일 때만 가능합니다. //배포를 위해 더는 디버그가 필요없다면 프로덕션 모드로 변경하면 됩니다. // Workbox를 디버그모드로 설정 // workbox.setConfig({ // debug: true, // }); // 배포용 프로덕션 모드 workbox.setConfig({ debug: false, }); // Vue-Cli에서 기본으로 제공하는 프리캐시 설정을 Workbox에 적용 workbox.precaching.precacheAndRoute(self.__WB_MANIFEST); // <-- self.__WB_MANIFEST로 변경 // 촬영된 이미지 캐시 workbox.routing.registerRoute( new RegExp( "https://firebasestorage.googleapis.com/v0/b/pwa-camera.appspot.com/.*" ), new workbox.strategies.StaleWhileRevalidate({ cacheName: "camera-images", plugins: [ new workbox.expiration.ExpirationPlugin({ // <-- ExpirationPlugin로 대문자 'E' 변경 maxEntries: 60, //이미지를 총 60개만 캐시하도록 지정 maxAgeSeconds: 365 * 24 * 60 * 60, // 1년 지정 }), ], }) );
워크박스는 캐시를 수행할 때 진행 상황을 콘솔 창에 표시해 줍니다.
개발하면서 이러한 메시지를 볼 수 있으면 편리한데 디버그 모드일 때만 가능합니다.
배포를 위해 더는 디버그가 필요없다면 프로덕션 모드로 변경하면 됩니다.
// Workbox를 디버그모드로 설정 workbox.setConfig({ debug: true, }); // 배포용 프로덕션 모드 workbox.setConfig({ debug: false, });
workbox.routing.registerRoute( //캐시 경로 //캐시 전략 )
9.빌드 및 firebase 서버에 배포하기
다음을 참조
https://macaronics.net/m04/vue/view/2048
출처 :
Do it! 프로그레시브 웹앱 만들기
반응형 웹 개발부터 하이브리드 앱 배포까지 PWA 완전 정복!
김응석 저자(글)
이지스퍼블리싱 · 2020년 08월 06일
8.5(7개의 리뷰)
도움돼요(50%의 구매자)
https://product.kyobobook.co.kr/detail/S000001817978
댓글 ( 4)
댓글 남기기