PWA 정리(3): Vue & Vuetify
교재: Do it! 프로그레시브 웹앱 만들기 - 실습 github
연관 포스트:
Vue
Vue 구성
- javascript 프레임워크
-
// 뷰 cdn에 연결 <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
-
// <p> {{ vueData }} </p> new Vue({ el:'#name' //연결할 element data:{ vueData: 'hi' // {{ }}(머스태시)에 전달될 값들 }, methods: { fnName() { } // **이벤트** 발생시키거나 기능을 위한 function 정의 // 이벤트 핸들로 로직 함수로 정의할 때 }, computed: { fnName() { } // **머스태시** 안의 로직을 함수로 정의할 때 // 계산량이 많거나 캐시 필요할 때 } })
- {{ }}: 머스태시, HTML의 element
v-bind & v-model
- v-bind는 HTML의 element의 attribute
- 단방향: HTML attribute에 값 전달
<!-- blue_style: {color: blue} Vue data:{sColor: 'blue'} --> <h1 v-bind:"sColor + '_style'"> 제목입니다 </h1> <!-- v-bind 생략 가능--> <h1 :"sColor + '_style'"> 제목입니다 </h1> <!-- 결과: 파란색 글씨의 "제목입니다" 표시 -->
- v-model은 양방향, 즉 변수에 따라 결과가 달라짐
-
<!-- Vue data:{sMsg: 'hi'} --> <p>{{ sMsg }}</p> <input v-model:value="sMsg"> <!-- 결과: <p></p> 안의 값이 원래 "hi"였는데, input에 쓰는 값에 따라 달라짐 -->
- class-binding
- 엘리먼트에 적용된 클래스 선택자가 사용될지 binding으로 결정 ```html
<div :class=”{ my-style: boolean_value }”></div> ```
v-if & v-for
- v-if는 조건에 따라 바인딩
<p>{{ bFlag }}</p> <p v-if=" bFlag == true"> true <\p> <p v-else> false <\p>
- v-for은 반복되는 attribute에 쓰면 좋음
<!-- Vue data:{aElementList: {sName:'A'}, {sName:'B} } --> <ul v-for="(item, index) in aElementList"> <!-- index는 위 (item, index)처럼 가지고 올 수 있음 --> <!-- item만 가져오기도 가능 --> <li>번호:{{ index }}</li> <li>이름:{{ item.sName }}</li> </ul>
v-on
- 발생하는 이벤트 컨트롤
<!-- Vue data: {title: 'hi'}, methods: { fnChangeTitle() { this.title='안녕' } } --> <h1> {{ title }} </h1> <button v-on:click="fnChangeTitle">버튼입니다</button> <!-- 결과: <h1> {{ title }} </h1> 안의 값이 원래 "hi"였는데, 버튼을 누르면 "안녕"으로 바뀜 -->
computed
- HTML element가 바뀌는 것을 살피면서 필요한 작업 수행
<!-- Vue data: {title: 'hi'}, computed: { fnChangeCapital: function() { return this.title.toUpperCase() } } --> <p> {{ fnChangeCapital }} <p> <!-- 결과: HI -->
component & props
- component는 HTML 기본 element외에 새로 정의하여 사용할 수 있는 모듈
Vue.component('favorite-fruits', { // component의 data 속성은 반드시 function으로 정의 // 같은 component 여러개 사용 시에 data 속성 값들이 별도의 메모리에 저장될 수 있도록 하기 위함 data: function() { return { aFruits: [{sName: 'apple'}, {sName: 'banana'}, {sName: 'orange'}], } }, template: ` <div> <div v-for="item in aFruits" class="fruit_style"> <p> {{ item.sName }} </p> </div> <br> </div>`, // 역따옴표로 표현하기 })
<favorite-fruits></favorite-fruits>
- props는 component에 전달되는 attribute
Vue.component('favorite-fruits', { props:['fruit'], template: `<li>{{ fruit.text }}</li>` }) var app = new Vue({ el:'#app', data: { aFruits: [ {id:0, text:'apple'}, {id:1, text:'banana'}, {id:2, text:'orange'} ] } })
<ol> <favorite-fruits v-for="item in aFruits" v-bind:fruit="item" v-bind:key="item.id"> <!--key에 반드시 고유한 값이 전달되어야 하므로 item에서 id 값 설정--> </favorite-fruits> </ol>
Vuex
- 하나 혹은 그 이상의 화면(뷰) 사이에 있는 컴포넌트 값 전달 & 공유를 위한 라이브러리
- 속성 4가지
consst store = new Vuex.Store({ // 전역 변수처럼 사용할 값 state: { value: 0 }, // 외부에서 Vuex 데이터 변경, 동기(sync) 실행 // store.commit('fnName_1') 으로 접근 mutations: { fnName_1: function() { }, fnName_2: function() { } }, // Vuex에서 외부로 데이터 반환 // result = store.getters.fnName_3 getters: { fnName_3 (state) { return state.value; } }, // 비동기 실행 관리 ex) 외부 API // store.dispatch('fnName_4') action:{ async fnName_4({commit}, state) { const result = await api.fnAPIName(); if (result == true) commit(/* mutations function ... etc... */) } } }) var app = new Vue({ el:'# ', store })
router
- router는 페이지끼리 이동하는 기능
- 화면이 바뀌어도 새로고침이 일어나지 않아서 네이티브 앱 같은 느낌 제공
-
const tmMain = { template: `<h2>메인 페이지입니다</h2>` } const tmSub = { template: `<h2>서브 페이지입니다</h2>` } // 라우터 옵션 등록 const rtRoutes = [{ path: '/main', component: tmMain }, { path: '/sub', component: tmSub }] // 라우터 객체 생성 const router = new VueRouter({ routes: rtRoutes }) var gApp = new Vue({ el: '#...', router })
- Vue-CLI에서 템플릿 제공하여 모듈 단위로 관리 가능
-
책은 version 2.*이기에 설정 유의
$
- Vue 객체의 속성 변수에 접근하기 위해 사용
var mydata ={ name:'A' } var vue = new Vue({ el:'#...', data: mydata }) /* vue.data != mydata vue.$data == mydata */
Vuetify
- UI 컴포넌트 라이브러리
- 같이 보면 좋은 링크
-
<!-- vuetify.js에 필요한 스타일 파일 링크 --> <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/@mdi/font@3.x/css/materialdesignicons.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet"> <!-- vue & vuetify 링크 --> <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
기본 구성
<v-app> <!-- 첫 화면의 시작, v-app 토대로 랜더링, 필수!! -->
<!-- app bar: dark-글자색 흰색으로 바꾸기 fixed-앱바 위치 고정 -->
<v-app-bar app color="primary" dark fixed>
<v-app-bar-nav-icon></v-app-bar-nav-icon> <!-- 왼쪽에 menu 아이콘 -->
<v-toolbar-title>마스터 페이지</v-toolbar-title>
<!-- 오른쪽 추가 아이콘을 위한 space,
왼쪽 정렬에서 오른쪽 정렬로 바뀜 -->
<v-spacer></v-spacer>
<v-btn icon> <!--구글 제공 아이콘 사용-->
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</v-app-bar>
<!-- 본문 위치 지정, 자동 여백 지정
단독으로 쓰이면 모든 영역이 본문 -->
<v-main>
<!-- v-container 안의 element들을
화면 크기에 맞춰서 여백 자동 지정 -->
<v-container>
<!-- 타이포그래피 설정 & 여백 설정-->
<h1 class="display-1 my-5">안녕하세요</h1>
<p class="body-2 my-4">마스터 페이지입니다</p>
<v-divider></v-divider>
<h1 class="display-3 my-4">안녕하세요</h1>
<p class="body-1 my-4">마스터 페이지입니다</p>
</v-container>
</v-main>
<v-footer color="primary" dark>
<div class="mx-auto">Copyright © {{ new Date().getFullYear() }}</div>
</v-footer>
</v-app>
<script>
new Vue({
el:'#app',
vuetify: new Vuetify()
})
</script>
v-card
<v-app>
<v-main>
<v-container>
<v-card max-width="400">
<!-- picsum.photos/id/id num/width/height?option
aspect-ratio= "width:height"-->
<v-img src="https://picsum.photos/id/1068/400/300" aspect-ratio="2.3"></v-img>
<!-- 카드 안에 제목과 본문 쓰기-->
<v-card-text>
<div> <!-- color--text: text 색상-->
<h2 class="title primary--text mb-2">시대정신 선도</h2>
버추얼 컴퍼니에 관심 가져주셔서 감사합니다.
</div>
</v-card-text>
<v-card-actions>
<v-btn color="red white--text">확인</v-btn>
<v-btn outlined color="red">취소</v-btn>
<v-btn color="#9C27B0" dark>취소</v-btn>
</v-card-actions>
</v-card>
</v-container>
<v-container>
<v-row>
<v-col xs="12">
<v-card>
<v-card-text style="height: 300px;" class="grey lighten-4"></v-card-text>
<!--상대 좌표계: relative - 카드 제목 영역 안에 표시될 수 있음-->
<v-card-text style="height: 50px; position: relative">
<!-- 절대 좌표계: absolute (top right) -->
<v-btn absolute dark fab top right color="pink">
<v-icon>add</v-icon>
</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</v-main>
</v-app>
Grid
- 기본 원리
<v-container> <v-row class="text-center"> <!-- text-코드명-정렬명 --> <!-- 한 row는 12 columns 기본 --> <v-col cols="12" class="border_style">xs12</v-col> <!-- 6+3+4는 12를 넘어가므로 4는 다음 행에 추가--> <v-col cols="6" class="border_style">xs6</v-col> <v-col cols="3" class="border_style">xs3</v-col> <v-col cols="4" class="border_style">xs4</v-col> <!-- 8개까지만 생성되고 다음 행에 이어서 4개 생성--> <v-col cols="1" v-for="item in 12" v-bind:key="item.id" class="border_style">xs1</v-col> </v-row> </v-container> <!-- fluid: Removes viewport maximum-width size breakpoints--> <v-container fluid> <v-row class="text-center"> <v-col sm="4" class="border_style">sm4</v-col> <!-- 코드 sm 범위의 사이즈 이상일 때 offset 4--> <v-col sm="4" offset-sm="4" class="border_style">4</v-col> </v-row> </v-container>
- 반응형(responsive)
<v-container> <v-row> <!-- 첫번째 열의 반응형 크기 지정 1) xs: 열 12개 차지 2) sm: 열 6개 차지 --> <v-col cols="12" sm="6"> <h2 class="mb-3">About Beetle</h2> <p> 운동화는 필수 아이템. 나를 운동화로 표현해보자. Beetle의 발편한 운동화</p> </v-col> <!-- 두번째 열의 반응형 크기 지정 1) xs: 열 12개 차지 2) sm: 열 6개 차지 --> <v-col cols="12" sm="6"> <h2 class="mb-3">Beetle's Target</h2> <p>1. 관심있는 누구나</p> <p>2. 스니커즈 원하는 사람</p> <p>3. 차별화된 디자인</p> <p>4. 최신 트렌드 디자인</p> </v-col> </v-row>
- xs일 때는 2개의 row
sm일 때는 1개의 row에 2개의 column으로 구성
- xs일 때는 2개의 row
list & icon
-
<v-container> <v-card> <!-- two-line: 한 항목에 행 2개--> <v-list two-line v-for="item in aList" v-bind:key="item.id"> <v-list-item @click=""> <!-- 왼쪽 icon: avatar: 원 모양의 디자인으로 바꿔줌--> <v-list-item-avatar> <v-icon :class="item.icon_style">{{ item.icon_name }}</v-icon> </v-list-item-avatar> <!-- 오른쪽 내용--> <v-list-item-content> <v-list-item-title>{{ item.title }}</v-list-item-title> </v-list-item-content> <!-- 오른쪽에 화살표 아이콘 넣기--> <v-list-item-action> <v-btn icon> <v-icon color="grey">keyboard_arrow_right</v-icon> </v-btn> </v-list-item-action> </v-list-item> </v-list> </v-card> </v-container> <script> new Vue({ el:'#app', vuetify: new Vuetify(), data() { /*component로 사용할 때 함수 형태로 선언해야함 컴포넌트 별로 각각 data 메모리 할당하기 위함*/ return { aList:[{ icon_name:'account_balance', icon_style:'red white--text', title:'회사 소개' }, { icon_name:'photo', icon_style:'green white--text', title:'제품 이미지' }, { divider: false, icon_name:'movie', icon_style:'yellow white--text', title:'홍보 동영상' }] } } }) </script>
bottom-navigation
-
<v-footer> <!-- absolute: 스크롤에 상관없이 항상 아래에 위치 value는 true/false로 보이거나 안 보이게 할 수 있음--> <v-bottom-navigation absolute v-model="sSelect" dark> <!-- value는 sSelect 관련 값--> <v-btn text value="자전거"> 자전거 <v-icon>directions_bike</v-icon> </v-btn> <v-btn text value="지하철"> 지하철 <v-icon>subway</v-icon> </v-btn> <v-btn text value="버스"> 버스 <v-icon>directions_bus</v-icon> </v-btn> </v-bottom-navigation> </v-footer>
navigation-drawer
-
<v-app-bar app color="primary" dark> <!-- @click.stopL: 마우스를 눌렀다 떼었을 때 --> <v-app-bar-nav-icon @click.stop="bDrawer = !bDrawer"> </v-app-bar-nav-icon> <v-toolbar-title>Header 입니다</v-toolbar-title> </v-app-bar> <!-- absolute: 메뉴 아이콘 위치부터 펼쳐지게 --> <v-navigation-drawer absolute temporary v-model="bDrawer"> <v-toolbar flat height="70px"> <v-list> <v-list-item> <v-list-item-avatar> <img src="https://randomuser.me/api/portraits/men/44.jpg"> </v-list-item-avatar> <v-list-item-content> <v-list-item-title class="title">홍길동</v-list-item-title> <v-list-item-subtitle>로그인</v-list-item-subtitle> </v-list-item-content> </v-list-item> </v-list> </v-toolbar> <v-divider> <!-- 항목 사이의 구분선--> </v-divider> <v-list class="pt-3"> <v-list-item v-for="item in aMenu_items" :key="item.title" :href="item.link"> <v-list-item-action> <v-icon>{{ item.icon }}</v-icon> </v-list-item-action> <v-list-item-content> <v-list-item-title>{{ item.title }}</v-list-item-title> </v-list-item-content> </v-list-item> </v-list> </v-navigation-drawer>
페이지간 데이터 연동
router 사용
<!-- main_page -->
<div>
<v-text-field label="매개변수1" v-model="sParam1"></v-text-field>
<v-text-field label="매개변수2" v-model="sParam2"></v-text-field>
</div>
<div class="text-center" v-on="fnGoSub">
<v-btn @click="fnGoSub" class="mt-5" color="purple" dark>
<v-icon>확 인</v-icon>
</v-btn>
</div>
<script>
export default {
data: function() {
return {
sParam1: '',
sParam2: ''
}
},
methods:{
fnGoSub() {
// $router: 인스턴스 하나만 존재
// router.push() ... 에 사용
this.$router.push({
name: 'sub_page',
params: {
p_param1: this.sParam1,
p_param2: this.sParam2
}
})
}
}
}
</script>
<!-- sub page -->
<script>
<p class="display-4 my-4">{{ sTitle1 }}</p>
<p class="display-4 my-4">{{ sTitle2 }}</p>
export default {
data() {
return {
// $route : 라우팅 발생마다 생성되는 객체
sTitle1: this.$route.params.p_param1,
sTitle2: this.$route.params.p_param2
}
}
}
</script>
Vuex 사용
<!-- main page -->
<v-main>
<p class="text-center display-3 my-4">메인 페이지입니다</p>
<v-row>
<v-col offset-sm="1" sm="10">
<v-text-field label="제목" v-model="sTitle"></v-text-field>
</v-col>
</v-row>
<div class="text-center">
<v-btn large class="mt-5" color="purple" dark @click="fnSetTitle">
확 인</v-btn>
</div>
</v-main>
<script>
export default {
data() {
return {
sTitle: this.$store.getters.fnGetData
}
},
methods: {
fnSetTitle() {
this.$store.commit('fnSetData', this.sTitle);
this.$router.push('sub')
}
}
}
</script>
<!-- sub page -->
<script>
export default {
data() {
return {
sTitle: this.$store.getters.fnGetData
}
}
}
</script>