소스 :
https://github.com/braverokmc79/pwa-ex12-test
실행
1) npm install
2) npm run build
3) serve dist
결과물
파이어베이스 인증 사용
이메일 인증, 비밀번호 재설정, 이메일 주소 변경 처리 등과 같은 세밀한 서비스도 제공해 준다.
1. 프로젝트 준비하기
1) 다음을 참조해서 프로젝트 생성
=> Vue-Cli 3 을 이용하여 빠르게 PWA( 웹앱 )생성 및 Https 로 파이어 베이스 호스팅 배포 하기
프로젝트 설정 옵션 값은 Babel, PWA, Router, Vuex 를 선택
2) vuetify 2. 추가
$ vue add vuetify
3) 라이브러리 설치
npm install firebase vuex-persist mdi
a) 파이어베이스 인증 사용을 위해 파이어베이스를 설치
b) Vuex의 성능을 강화하는 vuex-persist 를 설치 (로그인 상태값을 처리할 때 문제가 없으려면 이 기능이 필요)
c)구글 로고 등을 사용하기 위해 머티리얼 디자인 아이콘도 추가
2. 매니페스트 작성하기
=> 참조 : [PWA] 3. Manifest.json
Manifest 파일은 1개의 json 파일로 웹 브라우저에게 App에 대한 부가적인 정보를 전달하는 역할을 합니다.
브라우저는 이 정보를 이용해 화면을 다르게 표시하거나(핸드폰 크기 & 핸드폰 가로 / 세로) Homescreen icon(앱을 설치했을 때의 아이콘) 생성 기능을 제공합니다.
manifest 내용 전체는 중괄호({})를 사용하여 object 형태로 표시하며, property 종류에는 다음과 같은 것들이 있습니다.
- name : PWA의 full name 이며, homescreen icon을 클릭할 때 나타나는 splash screen 하단에 나타나는 text 값입니다.
- short_name : 브라우저가 app name을 표시할 공간이 충분하지 않을 때 사용하는 text이며, homescreen icon의 name으로 사용됩니다.
- start_url : app이 최초 실행 될 때 load되는 file의 경로입니다.
- scope : app 내에서 PWA 기능을 적용할 범위를 나타내며, '.' 으로 표시하면 전체에 적용합니다.
- display : homescreen icon을 통해 실행될 때 app 화면이 보여지는 형태를 지정합니다.
-> standalone : default 설정이며, native app처럼 browser control, url bar 등이 없이 app화면으로만 채워짐,
-> browser : 일반적인 web page처럼 url bar가 보여집니다.
- background_color : splashscreen의 배경색을 지정합니다.
- theme_color : app 전체적인 테마색을 지정합니다.
- description : browser가 app의 description 정보를 요구할 때 사용됩니다.
- dir : app에 사용되는 언어의 읽기 방향을 나타내며, ltr(left to right)이 default 입니다.
- lang : app의 main language를 지정합니다.
- orientation : device에서 app 실행 시 app이 보여지는 방향을 지정합니다.
-> portrait-primary : 세로 방향(어느 방향으로 뒤집어도 정 세로 방향 유지)
-> any : device 방향에 따라 가로/세로 방향이 바뀜
- > portrait : 세로 방향, device를 180도 회전하면 그에 맞게 바뀜
-> landscape : 가로방향, device를 180도 회전하면 그에 맞게 바뀜
- icons : homescreen icon의 종류를 정의하는 속성이며, splash screen에서도 사용됩니다. icons set을 정의하면 브라우저가 device 크기와 해상도에 따라 가장 적합한 icon을 사용하게 됩니다. 각 icon은 (src, type, sizes)로 정의해야 합니다.
- relate_applications : 사용자가 관심 있을 수도 있는 web app와 동일한 기능을 하는 native app의 install 정보를 표시합니다.
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" }
3. 파이어베이스 인증 사용하기
다음 링크 주소를 통해 파이어베이스 가입후 프로젝트를 생성한다.
https://macaronics.net/m04/vue/view/2053
만약에 기존 프로젝트에 덮어 씌우려면 다음과 같이 하면 된다.
프로젝트 설정 클릭 후 하단에 다음과 같은 정보 보이는데. apikey 와 authDomain 주소를 복사해 놓는다.
복사한 주소는 @/datasources/firebase.js 파일에 붙여 넣을 것이다.
파이어베이스에서 다음과 같이 이메일/비밀번호 와 구글 을 사용하도록 설정한다.
src/datasources/firebase.js 파일을 생성하고 다음과 같은 코드를 작성
// 파이어베이스 앱 객체 모듈 가져오기 import firebase from 'firebase/compat/app' // 파이어베이스 DB를 초기화 const oFirebase = firebase.initializeApp({ // 파이어베이스 콘솔에서 복사하여 붙여넣기 apiKey: "AIzaSyA3XCvs-3jz1M07_la2ORmWfEijfpTnqhA", authDomain: "pwa-auth-login.firebaseapp.com", }); // 파이어베이스 인증 객체 공개 export const oFirebaseAuth = oFirebase.auth();
프로그램을 실행하다 보면 다양한 상황이 발생할 수 있습니다. 인증할 때 꼭 고려해야 할 부분은 로그인한 후에 사용자가 브라우저를 닫았다가 다시 열어 재접속하는 경우
입니다. 이때 로그인 상태를 기억하고 다시 자동으로 로그인되면 편리합니다.
자동으로 생성된 main.js 파일을 열고 다음과 같이 코드를 수정합니다.
Firebase.auth is not a function 오류 발생시 다시 설치
$ npm install firebase --save
파이어베이스 인증 연결하기
datasources/firebase.js 파일을 생성하고 다음과 같은 코드를 작성한다.
교재 파이어베이스 "firebase": "^9.6.10", 버전은 다음과 같이 설정하지만 Web version 8
// 파이어베이스 앱 객체 모듈 가져오기 import firebase from "firebase/compat/app"; // 파이어베이스 DB를 초기화 const oFirebase = firebase.initializeApp({ // 파이어베이스 콘솔에서 복사하여 붙여넣기 apiKey: "AIzaSyA3XCvs-3jz1M07_la2ORmWfEijfpTnqhA", authDomain: "pwa-auth-login.firebaseapp.com", }); console.log(" oFirebase : ", oFirebase); // 파이어베이스 인증 객체 공개 export const oFirebaseAuth = oFirebase.auth();
파이어베이스 2023-03월 시점 : 버전 "firebase": "^9.18.0" 은 다음과 같이 설정한다. Web version 9
참조 :
https://firebase.google.com/docs/auth/web/start?hl=ko
https://firebase.google.com/docs/database/web/start?hl=ko
//////////////////////////////////////////////////// // 2023-03월 시점 : 버전 "firebase": "^9.18.0" // https://firebase.google.com/docs/auth/web/start?hl=ko // https://firebase.google.com/docs/database/web/start?hl=ko // import { initializeApp } from "firebase/app"; import { getAuth } from "firebase/auth"; const firebaseConfig = { // // 파이어베이스 콘솔에서 복사하여 붙여넣기 apiKey: "AIzaSyA3XCvs-3jz1M07_la2ORmWfEijfpTnqhA", authDomain: "pwa-auth-login.firebaseapp.com", }; // Initialize Firebase const app = initializeApp(firebaseConfig); const auth = getAuth(app); // // 파이어베이스 인증 객체 공개 console.log("auth : ", auth); export const oFirebaseAuth = auth;
src/main.js 은 다음과 같이 설정한다.
인증 상태 관찰자 설정 및 사용자 데이터 가져오기 참조
https://firebase.google.com/docs/auth/web/start?hl=ko
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import './registerServiceWorker' import vuetify from './plugins/vuetify'; // 파이어베이스 앱 객체 모듈 가져오기 import '@/datasources/firebase' //인증 상태 관찰자 설정 및 사용자 데이터 가져오기 //https://firebase.google.com/docs/auth/web/start?hl=ko import { getAuth, onAuthStateChanged } from "firebase/auth"; Vue.config.productionTip = false new Vue({ router, store, vuetify, created() { const auth = getAuth(); onAuthStateChanged(auth, (pUserInfo) => { if (pUserInfo !== null) { console.log("pUserInfo : ", pUserInfo); console.log("pUserInfo : ", pUserInfo.uid); const uid = pUserInfo.uid; // 이미 로그인 되어있었는지 등의 상태를 파악하여 처리함 store.dispatch('fnDoLoginAuto', pUserInfo); }else{ console.log("로그인 안됨"); } }); }, render: h => h(App) }).$mount('#app')
사용자 상태 변경 시 처리
여기서는 파이어베이스의 auth() 함수 객체 맴버 중에서 onAuthStateChanged() 이벤트 핸들러를 준비합니다. 이 핸들러는 사용자가 로그인, 로그아웃, 비밀번호 변경 등
중요한 상태 변경이 발생할 때 실행됩니다. 소스에서는 사용자가 로그인, 로그아웃, 비밀번호 등의 변경을 수행했는지 확인하기 위해 onAuthStateChanged() 이벤트 핸들러를
설정합니다. 만약 이벤트가 발생하면 매개변수로 pUserInfo 를 통해 사용자 정보를 받은 후 Vuex 객체인 store 의 fnDoLoginAuto() 함수를 실행합니다.
컴포넌트에서 Vuex 상태값을 설정하려면 Actions 속성의 함수를 거쳐야 하므로 dispatch() 함수롤 실행된것에 유의
fnDoLoginAuto() 함수는 자동으로 로그인을 실해아기 위한 작업을 수행합니다. 예를 들면 이미 한 번 로그인되었으므로 로그인되었던 사용자의 계정 정보를 Vuex 에 다시 저장하는
것이 주된 내용입니다.
4. 앱 실행화면 만들기
머티리얼 아이콘과 머티리얼 디자인 아이콘은 서로 다른 서비스입니다. 헷갈리지 않도록 주의
머티리얼 디자인 아이콘 사이트에서 mdi-google 만 기억
https://fonts.google.com/icons?icon.set=Material+Symbols
웹사이트의 <head> 태그영역 안에 아래의 코드를 삽입합니다.
<!-- 구글 머티리얼 디자인 아이콘 추가--> <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">
구글 머티리얼 아이콘(Google Material icons) 사용 방법(무료 아이콘)
https://m.blog.naver.com/shiaru/222722972842
구글 머티리얼 아이콘(Material icons) 폰트 사용하기
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 ex12_start 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>
router/index.js 파일을 열고 다음과 같이 코드를 수정
import Vue from 'vue' import VueRouter from 'vue-router' //파이어베이스 앱 객체 모듈 가져오기 //import firebase from 'firebase/app'; //https://firebase.google.com/docs/auth/web/start?hl=ko import { getAuth, onAuthStateChanged } from "firebase/auth"; Vue.use(VueRouter) const routes = [ { path: '/', name: 'start-page', component:()=>import('../components/start_page.vue') }, { path:'/main', name:"main_page", component:()=>import('../components/main_page.vue'), //메인페이지는 인증과 연동 meta:{bAuth:true} }, { path:'/login', name:'login_page', component:()=>import('../components/login_page.vue') }, { path:'/register', name:'register_page', component:()=>import('../components/register_page.vue') }, { //사용자가 라우터에 등록된 것 외에 다른 주소 입력 시 에러 페이지 연결 path:'*', name:'error_page', component:()=>import('../components/error_page.vue') } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) //라우터 이동에 개입하여 인증이 필요한 경우 login 페이지로 전환 //https://firebase.google.com/docs/auth/web/manage-users?hl=ko router.beforeEach((to, from, next)=>{ const bNeedAuth=to.matched.some(record=>record.meta.bAuth) const auth = getAuth(); const bCheckAuth= auth.currentUser; console.log(" bNeedAuth 값이 true 이면 로그인이 필요한 페이지로 이동:" ,bNeedAuth); console.log(" !bCheckAuth 널값이면 true :" ,!bCheckAuth); if (bNeedAuth && !bCheckAuth) { next('/login') } else { next() } }) export default router
인증과 관계된 부분은 meta 속성 추가
이제 라우터를 등록해 보겠습니다. 라우터를 등록할 때는 이동할 페이지 경로, 이름, 컴포넌트 템플릿을 정의 합니다.
이때 배열 변수를 사용해서 화면에 보여 줄 페이지를 JSON 형식으로 구분합니다.
그리고 main_page 는 인증된 사용자만 라우터로 접근하도록 해야 합니다.
이를 위해 라우터에서 인증과 관계된 부분은 meta 속성을추가로 사용합니다. 즉, meta 속성에 bAuth 변수가 true 일 때만 접근하도로록 선업합니다.
에러 페이지로 연결
그리고 실제 서비스처럼 라우터로 등록된 페이지 이외로 접근을 시도할 때는 모두 하나의 에러 페이조 연결합니다.
이를 위해서 path 속성에 와일드카드 '*' 를 사용하면 모든 페이지가 연결 됩니다.
내비게이션 가드
내비게시션 가드란 사용자가 접근 권한이 없는 페이지로 이동할 때 그 페이지를 볼 수 없도록 보호하는 것을 말합니다.
내비게싱션 가드를 사용하려면 앞에서 meta 속성에 선언한 bAuth 값을 라우터 처리에서 확인하는 중간 개입이 필요합니다. 라우터의 중간 개입은 router 객체의
beforEach() 함수를 사용합니다. 이것을 사용하면 to, from,next 등 3가지 매개변수를 전달 받습니다. 각각 라우터의 원래 목적지인 이동할 페이지 라우터 객체, 출발지인
전 페이지 라우터, 객체, 변경을 실행하는 next() 함수를 의미합니다. 특히 beforeEach() 함수가 완료 되려면 매개변수로 전달 받은 next() 함수를 반드시 실행해야 합니다.
소스에서는 먼저 bNeedAuth 상수 변수에 이동할 페이지가 로그인이 필요한지를 bAuth 속성값에 true 또는 false 로 저장합니다. 만약 true 이면 로그인이 필요한 라우터
페이지입니다. 두 번째로 bCheckAuth 변수에는 파이어베이스의 현재 로그인 사용자 정보를 저장합니다. 만약 로그인 사용자가 있다면 값을 가지므로 true 이고, 없으면
null 값이므로 false 입니다.
그리고 조건을 검사하여 라우팅할 페이지가 로그인 이 필요한 페이지 (bNeedAuth가 true) 이고 현재 로그인되지 않았다면 (bCheckAuth 가 false) 강제로 로그인을
수행하도도록 "/login" 페이지로 이동합니다. 반면에 이미 로그인 된 상태이므로 원래 라우팅 할 목적지로 이동 합니다.
App.vue 화면은 다음 같다
pc 화면
모바일 화면
App.vue
<template> <v-app> <v-card class="mx-auto overflow-hidden" height="100%" width="100%" > <v-app-bar color="primary accent-4" dark prominent height="64px" > <!-- 햄버거 아이콘은 반응형 크기가 sm 이상일 때 숨김 --> <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon> <router-link to="/" style="cursor: pointer" class="ma-2"> <!-- 머티리얼디자인아이콘 사용 시 아이콘 이름에 'mid-' 붙임 --> <v-icon class="hidden-xs-only" large color="teal lighten-4">mdi-home </v-icon> </router-link> <v-toolbar-title class="headline"> 이메일-구글 인증 로그인 </v-toolbar-title> <v-spacer></v-spacer> <!-- items 배열을 읽어와서 차례로 메뉴로 바인딩 하여 표시함 --> <v-btn depressed color="primary" class="ma-2" value="true" v-for="(item, i) in fnGetMenuItems" :to="item.to" :key="i"> <v-icon left v-html="item.icon"></v-icon> {{item.title}} </v-btn> <!-- 로그인 된 경우만 로그아웃 버튼을 표시함 --> <v-btn depressed color="primary" class="ma-2" @click="fnDoLogout" v-if="fnGetAuthStatus"> <v-icon left>mdi-arrow-right-bold-box-outline</v-icon> 로그아웃 </v-btn> <!-- 로그인 페이지 --> <v-btn depressed color="primary" class="ma-2" @click="fonLoginPage" v-if="!fnGetAuthStatus"> <v-icon left>mdi-arrow-right-bold-box-login</v-icon> 로그인 </v-btn> </v-app-bar> <!-- 왼쪽 토글 메뉴 --> <v-navigation-drawer v-model="drawer" absolute bottom temporary > <v-list nav dense > <v-list-item-group v-model="group" active-class="deep-purple--text text--accent-4" > <v-list-item class="mt-5" v-for="(item, i) in fnGetMenuItems" :to="item.to" :key="i"> <v-icon left v-html="item.icon"></v-icon> {{item.title}} </v-list-item> <!-- 로그인 된 경우만 로그아웃 버튼을 표시함 --> <v-list-item @click="fnDoLogout" v-if="fnGetAuthStatus"> <v-icon left>mdi-arrow-right-bold-box-outline</v-icon> 로그아웃 </v-list-item> </v-list-item-group> </v-list> </v-navigation-drawer> <v-card-text> <router-view/> </v-card-text> <v-footer app> <div class="mx-auto">© CODE-DESIGN.web.app</div> </v-footer> </v-card> </v-app> </template> <script> export default { name: 'App', data: () => ({ drawer: false, group: null, }), watch: { group () { this.drawer = false }, }, computed:{ // 스토어에서 현재 인증 상태인지 반환 fnGetAuthStatus() { console.log("스토어에서 현재 인증 상태인지 반환 : ",this.$store.getters.fnGetAuthStatus); return this.$store.getters.fnGetAuthStatus }, // 로그인 여부에 따라 다르게 탐색서랍과 툴바 메뉴명 항목 배열 반환 fnGetMenuItems() { if (!this.fnGetAuthStatus) { return [{ title: '회원가입', to: '/register', icon: 'mdi-lock-open-outline', }] } else { return [{ title: '메인 페이지', to: '/main', icon: 'mdi-account' }] } } }, // 스토어의 로그아웃 기능사용 methods: { fnDoLogout() { this.$store.dispatch('fnDoLogout') }, // 스토어에 이메일 로그인 처리 요청 fonLoginPage() { console.log("로그인페이지 이동1") this.$router.push("/login"); }, }, }; </script>
사용된 버전은 2.6.14 로
개발 방법 vuetify 내용이 처음 보면 정말 이해가 안가고 어렵게 느껴진다. 그러나
vuetify 사이트에서 특정 기능을 검색한후 샘플 코드를 복사후 적용하는 방법을 사용하면 쉽게 적용할 수 있다.
vuetify 에서 "v-navigation-drawer" 로 검색하면 다음 주소가 나오는데,
https://v2.vuetifyjs.com/en/api/v-navigation-drawer/
맨 상단에서
#Component Pages
클릭후 이동하면 샘플 소스 코드 들을 볼 수 있다.
적용한 소스 코드는 두번째 소스 코드로 다음과 같다.
<template> <v-card class="mx-auto overflow-hidden" height="400" width="344" > <v-system-bar color="deep-purple darken-3"></v-system-bar> <v-app-bar color="deep-purple accent-4" dark prominent > <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon> <v-toolbar-title>My files</v-toolbar-title> <v-spacer></v-spacer> <v-btn icon> <v-icon>mdi-magnify</v-icon> </v-btn> <v-btn icon> <v-icon>mdi-filter</v-icon> </v-btn> <v-btn icon> <v-icon>mdi-dots-vertical</v-icon> </v-btn> </v-app-bar> <v-navigation-drawer v-model="drawer" absolute bottom temporary > <v-list nav dense > <v-list-item-group v-model="group" active-class="deep-purple--text text--accent-4" > <v-list-item> <v-list-item-title>Foo</v-list-item-title> </v-list-item> <v-list-item> <v-list-item-title>Bar</v-list-item-title> </v-list-item> <v-list-item> <v-list-item-title>Fizz</v-list-item-title> </v-list-item> <v-list-item> <v-list-item-title>Buzz</v-list-item-title> </v-list-item> </v-list-item-group> </v-list> </v-navigation-drawer> <v-card-text> The navigation drawer will appear from the bottom on smaller size screens. </v-card-text> </v-card> </template>
스토어에서 현재 인증 상태인지 반환
현재 인증 여부를 알려면 Vuex 의 상태값으로부터 그 결과를 확인해야 합니다. Vuex 상탯값을 사용하려면 앞의 main.js 파일에서 생성했던 store 객체에 접근해야 합니다.
그런데 문제는 현재 컴포넌트인 App.vue 에서 main.js 로 접근하는 것은 서론 다른 파일이므로 불가능합니다. 하지만 뷰 프레임워크에서는 뷰 객체에 있는 속성에 한해
전역 접근을 허용해주는데 바로 $ 라는 특수 기호를 사용하면 됩니다. $ 를 사용하면 뷰 객체를 생성할 때 선언한 속성들에 접근할 수 있습니다.
소스를 보면 this 를 사용해 뷰 객체를 얻고, $store 코드로 뷰 객체 안에 있는 store 객체를 사용합니다. 이어서 getters 속성에 있는 fnGetAuthStatus() 함수로 로그인 여부를
반환 받습니다.
로그인 여부에 따라 탐색 서랍과 툴바 메뉴명 배열을 다르게 반환
사용자에게 로그인 중인지 알려면 로그인 여부에 따라 메뉴 모양이 달라야 합니다. 반응형으로 디자인할 때도 마찬가지입니다. 그러려면 각 메뉴를 상황에 맞게 항목으로
반환 할 수 있어야 합니다. 소스에서 이 역할을 fnGetMenuItems() 함수가 수행합니다. 즉, 아직 로그인 전이면 '회원가입' 메뉴를 로그인 되었으면 '메인 페이지' 메뉴를 반환합니다.
이 때 this 를 사용해서 computed 속성에 있는 fnGetAuthStatus() 함수에 접근함으로 써 로그인 여부를 확인합니다.
스토어의 로그아웃 기능 사용
로그아웃하려면 Vuex에 정의된 actions 속성의 fnDoLogout() 함수를 실행하면 됩니다. 그러면 fnDoLogout() 함수는 Vuex 중앙에서 로그아웃과 관련된 파이어베이스 처리, 사용자
상태값 처리 ,라우터 이동 등 모든 작업을 관리해 줍니다. 따라서 this.$store 를 통해서 Vuex 에 접근한 후 dispatch() 함수로 actions 속성 fnDoLogout() 함수를 실행합니다.
5. 스토어 작성하기
vuex는 뷰의 애플리케이션을 제작할 때 뷰 컴포넌트 간에 중앙 집중식 상태값 저장소 기능을 해주는 라이브러 입니다.
vuex를 사용하여 큰 프로젝트를 진행할 때 알아 두면 유용한 2가지 노하우를 여기서 사용합니다. 하나는 규모가 커진 스토어를 모듈 단위로
분리하는 방법이고, 다른 하나는 vuex 의 성능을 강화해서 상탯값을 로컬 스토리지에 저장하는 것입니다.
스토어 메인은 common 과 provider 라는 2개의 자식 모듈로 나우기 위한 부모 스토어라고 할 수 있습니다.
모듈 단위로 분리하려면 modules 속성으로 하위 폴더에 생성한 모듈의 이름을 연결합니다.
스토어 하위 폴더에 2개의 모듈을 만들어 사용합니다. 하나는 common 모듈로서 시간 지연, 에러 메시지 처리의 공통 내용을 정의 합니다.
두 번째는 provider 모듈인데 구글의 오픈 로그인과 이메일 인증 처리 내용을 담당합니다.
소스처럼 선언하면 store 라는 객체 변수는 Vuex 의 객체를 가지면서 내부적으로는 common 과 provider 라는 2개의 모듈로 구성됩니다. 관리는 2개로
나워서 하지만 컴포넌트에서 store 객체를 사용할 때는 하나의 스토어를 이용할 때와 방법이 같아서 편합니다. 즉, 관리할 스토어가 커지면
모듈로 분리하되 사용법은 같습니다.
지속정인 값 유지를 위한 Vuex 로컬 스토리지에 저장하기
이번 예제에는 구글 오픈 로그인이라는 외부 서비스를 이용하다 보니 다른 도메인으로 화면이 넘어갈 때 Vuex 의 상태값이 소멸합니다.
모든 정보를 중앙에서 관리하는 Vuex 의 장점이 사라지는 순간입니다. 그래서 이러한 문제를 해결하기 위해 보조로 vuex-persist 플러그인을 사용합니다.
★★
new VuexPersistence
common 스토어 모듈 작성하기
common 스토어는 공통으로 사용할 수 있는 시간 지연, 오류 메시지를 저장하는 기능이 있습니다. 먼저 src 폴더 아래 store 폴더를 만들고 그 아래
common 과 provider 폴더를 만듭니다. 그리고 두 폴더 각각에 index.js 파일을 추가합니다.
1) store/index.js
import Vue from 'vue' import Vuex from 'vuex' import VuexPersistence from 'vuex-persist' import modProvider from '../store/provider' import modCommon from '../store/common' Vue.use(Vuex) // 규모가 커짐에 따라 스토어를 모듈로 분리함 // (1) common 모듈 : 시간 지연, 에러메시지 처리의 공통 내용 // (2) provider 모듈 : 구글과 이메일 인증 처리 내용 export default new Vuex.Store({ modules: { common: modCommon, provider: modProvider }, plugins: [(new VuexPersistence({ storage: window.localStorage })).plugin] })
2) store/common/index.js
export default { state: { bIsLoading: false, // 처리 중 시간이 걸림을 나타냄 sErrorMessage: '', // 처리 중 오류 메시지 내용 }, mutations: { // 처리 중 시간이 걸리는지 여부 설정 fnSetLoading(state, payload) { state.bIsLoading = payload }, // 처리 중 오류 메시지 저장 fnSetErrorMessage(state, payload) { console.log("처리 중 오류 메시지 저장",payload) state.sErrorMessage = payload }, fnSetInitMessage(state, payload){ state.sErrorMessage=""; } }, getters: { // 처리 중 시간이 걸리는지 여부 반환 fnGetLoading (state) { return state.bIsLoading }, // 처리 중 에러 메세지 내용 반환 fnGetErrorMessage (state) { return state.sErrorMessage }, }, actions: { setInitLoading({commit}, payload) { //로딩 false 및 메시지 초기화 console.log("로딩 false 및 메시지 초기화"); commit('fnSetLoading', false); commit('fnSetInitMessage') }, } }
3) store/provider/index.js
// 파이어베이스 앱 객체 모듈 가져오기 import firebase from 'firebase/compat/app' import { getAuth, signInWithPopup, GoogleAuthProvider ,signInWithEmailAndPassword ,createUserWithEmailAndPassword } from "firebase/auth"; // 파이어베이스 패키지 모듈 가져오기 import 'firebase/compat/auth' import router from '@/router' export default { state: { oUser: null // 사용자 정보를 담을 객체 }, mutations: { // 사용자 객체를 저장 fnSetUser(state, payload) { state.oUser = payload } }, getters: { // 사용자 객체를 반환 fnGetUser(state) { return state.oUser }, // 사용자 객체의 값의 유무로 로그인 여부 반환 fnGetAuthStatus(state) { return (state.oUser != null) } }, actions: { // 이메일 회원 가입 처리 fnRegisterUser({ commit }, payload) { commit('fnSetLoading', true) // 스토어에 시간걸림으로 상태 변경 // 파이어베이스에 이메일 회원 생성 및 저장 console.log("1. 파이어베이스에 이메일 회원 생성 및 저장 " ,payload.pEmail, payload.pPassword); const auth = getAuth(); createUserWithEmailAndPassword(auth, payload.pEmail, payload.pPassword) .then((userCredential) => { const user = userCredential.user; console.log("회원 가입 성공 : ", user); commit('fnSetUser', { email: user.email})// <-- 파이어베이스 v9 마이그레이션 : user 추가 commit('fnSetLoading', false) // 스토어에 시간완료 상태 변경 commit('fnSetErrorMessage', '') // 스토어 에러메시지 초기화 alert("회원 가입을 축하 합니다."); router.push('/main') // 로그인 후 첫 화면으로 이동 }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; console.log("errorCode " ,errorCode , errorMessage); commit('fnSetErrorMessage', errorMessage) commit('fnSetLoading', false) }); }, // 이메일 회원 로그인 fnDoLogin({ commit }, payload) { commit('fnSetLoading', true) // 스토어에 시간걸림으로 상태 변경 // 파이어베이스에 이메일 회원 로그인 인증 처리 요청 //firebase.auth(). console.log(" 2 .파이어베이스에 이메일 회원 로그인 인증 처리 요청 "); const auth = getAuth(); signInWithEmailAndPassword(auth, payload.pEmail, payload.pPassword) .then(pUserInfo => { // 로그인이 성공하면 스토어에 계정정보 저장 commit('fnSetUser', { id: pUserInfo.user.uid, // <-- 파이어베이스 v9 마이그레이션 : user 추가 name: pUserInfo.user.displayName, // <-- 파이어베이스 v9 마이그레이션 : user 추가 email: pUserInfo.user.email, // <-- 파이어베이스 v9 마이그레이션 : user 추가 photoURL: pUserInfo.user.photoURL // <-- 파이어베이스 v9 마이그레이션 : user 추가 }) commit('fnSetLoading', false) // 시간걸림 상태 해제 commit('fnSetErrorMessage', '') // 에러메세지 초기화 router.push('/main') // 로그인 후 화면으로 이동 }) .catch(err => { commit('fnSetErrorMessage', err.message) commit('fnSetLoading', false) }) }, // 구글 계정 회원 로그인(팝업) fnDoGoogleLogin_Popup({ commit }) { commit('fnSetLoading', true) // 스토어에 시간걸림으로 상태 변경 // 파이어베이스에 구글 회원 로그인 인증 처리 요청 // 로그인제공자객체를 생성 var oProvider = new firebase.auth.GoogleAuthProvider(); // 오픈 계정의 범위를 설정합니다. // https://developers.google.com/identity/protocols/googlescopes console.log(" oProvider : ",oProvider); oProvider.addScope('profile'); oProvider.addScope('email'); //firebase.auth().signInWithPopup(oProvider) //문서 //https://firebase.google.com/docs/auth/web/google-signin?hl=ko#web-version-9_4 const auth = getAuth(); signInWithPopup(auth, oProvider) .then(pUserInfo => { // 로그인이 성공하면 스토어에 계정정보 저장 commit('fnSetUser', { id: pUserInfo.user.uid, // <-- 파이어베이스 v9 마이그레이션 : user 추가 name: pUserInfo.user.displayName, // <-- 파이어베이스 v9 마이그레이션 : user 추가 email: pUserInfo.user.email, // <-- 파이어베이스 v9 마이그레이션 : user 추가 photoURL: pUserInfo.user.photoURL // <-- 파이어베이스 v9 마이그레이션 : user 추가 }) console.log("로그인이 성공 pUserInfo : ",pUserInfo); commit('fnSetLoading', false) // 시간걸림 상태 해제 commit('fnSetErrorMessage', '') // 에러메세지 초기화 router.push('/main') // 로그인 후 화면으로 이동 }) .catch(err => { commit('fnSetErrorMessage', err.message) commit('fnSetLoading', false) }) }, // 자동 로그인 처리 fnDoLoginAuto({ commit }, pUserInfo) { // 자동 로그인 시 스토어에 계정정보 저장 commit('fnSetUser', { id: pUserInfo.uid, name: pUserInfo.displayName, email: pUserInfo.email, photoURL: pUserInfo.photoURL }) commit('fnSetLoading', false) // 시간걸림 상태 해제 commit('fnSetErrorMessage', '') // 에러메세지 초기화 }, // 로그아웃 처리 fnDoLogout({commit} ) { // 파이어베이스에 로그아웃 요청 console.log("로그 아웃 요쳥"); const auth = getAuth(); auth.signOut() commit('fnSetUser', null) // 스토어에 계정정보 초기화 router.push('/') // 첫 화면으로 이동 } } }
6. 컴포넌트 작성기
시작시 페이지 컴포넌트 작성하기
1) start_page.vue (시작페이지)
시작 페이지는 사용자가 로그인하지 않은 상태에서 처음으로 만나는 페이지입니다. 익명의 사용자가 접속했을 때 만나는 화면 디자인
<template> <v-container> <v-row class="text-center"> <v-col cols="12" class="text-center mt-5"> <h1 class="display-1 my-1">시작화면 페이지</h1> <p class="body-1">로그인 없이 방문자 누구나 접속 가능한 페이지입니 다.</p> <!-- 시간지연의 경우 회전 프로그레스 원 표시 --> <v-progress-circular v-if="fnGetLoading" indeterminate :width="7" :size="70" color="grey lighten-1"> </v-progress-circular> </v-col> <v-col offset="3" cols="6" class="text-center mt-5"> <!-- 구글 계정 로그인 버튼 표시 및 처리 --> <v-btn @click="fnDoGoogleLogin_Popup" block outlined color="red" large dark> <!-- 머티리얼디자인아이콘 사용 시 아이콘 이름에 'mdi-' 붙임 --> <v-icon left color="red">mdi-google</v-icon> 구글 로그인 </v-btn> </v-col> <v-col offset="3" cols="6" class="text-center mt-5"> <!-- 이메일 계정 로그인 버튼 표시 및 처리 --> <v-btn to="/login" block color="red" large dark> <v-icon left>mdi-email</v-icon> 이메일 로그인 </v-btn> </v-col> </v-row> </v-container> </template> <script> export default { name: 'start-page', data: () => ({ }), methods:{ fnDoGoogleLogin_Popup(){ //스토어에 구글 계정 로그인 처리 요청 this.$store.dispatch("fnDoGoogleLogin_Popup"); } }, computed:{ //시간 지연 상태 스토어에서 읽어서 반환 fnGetLoading(){ return this.$store.getters.fnGetLoading; } } } </script>
2)main_page.vue(메인페이지)
두번째 단계에서는 스토어에 접근하여 계정 정보를 가져오는 방법과 비밀번호 재설정 이메일을 사용자에게 발송하는 기능을 다루어 봅니다.
비밀번호을 잊었을 때 사용자에게 변셩하는 서비스를 제공하는 부분은 파이어베이스가 처리해 주므로 매우 편리합니다.
비밀번호 재설정
<template> <v-container> <v-row class="text-center"> <v-col xs="12" class="mt-5 text-center"> <h1 class="display-1 my-1">로그인 후 화면 페이지</h1> <p class="body-1">로그인을 통해 인증된 사용자가 방문한 페이지입니다.</p> </v-col> </v-row> <v-row> <v-col dark offset="1" cols="10" class="mt-5 text-center"> <!-- 구글 로그인인 경우 사진 이미지 정보 표시 --> <img v-if="fnGetUser.photoURL" :src="fnGetUser.photoURL" class="avatar_style" alt=""> <!-- 계정 이름 표시 --> <h3 class="pt-2 mt-4 grey lighten-2">{{ fnGetUser.name }}</h3> <!-- 계정 이메일 표시 --> <p class="pb-2 grey lighten-2">{{fnGetUser.email}}</p> </v-col> <v-col offset="3" cols="6" class="text-center mt-1"> <!-- 이메일 계정 로그인 버튼 표시 및 처리 --> <v-btn @click="fnSendPasswordReset" block color="orange" large dark> <v-icon left>mdi-email</v-icon> 비밀번호 재설정 </v-btn> </v-col> </v-row> </v-container> </template> <script> //파이어베이스에서 oFirebaseAuth 객체 변수 가져옴 import {oFirebaseAuth} from "@/datasources/firebase"; import { getAuth, sendPasswordResetEmail } from "firebase/auth"; export default { name: 'main_page', computed:{ //스토어에서 로그인된 계정 정보 반환 fnGetUser(){ let oUserInfo=this.$store.getters.fnGetUser; return oUserInfo; } }, methods:{ fnSendPasswordReset(){ // 비밀번호 재설정 메일 발송하기 // ==>기존 // oFirebaseAuth.sendPasswordResetEmail(this.fnGetUser.email).then(function () { // console.log("비밀번호 재설정메일을 발송했습니다!"); // alert("비밀번호 재설정메일을 발송했습니다!"); // }).catch(function (error) { // console.log(error); // }) //공식 문서 ==>비밀번호 재설정 이메일 보내기 //https://firebase.google.com/docs/auth/web/manage-users?hl=ko const auth = getAuth(); sendPasswordResetEmail(auth, this.fnGetUser.email) .then(() => { alert("비밀번호 재설정메일을 발송했습니다!"); }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; console.log(errorMessage); }); } } } </script>
댓글 이어서 볼것 =>
댓글 ( 5)
댓글 남기기