교재: Do it! 프로그레시브 웹앱 만들기 - 실습 github

연관 포스트:

  1. PWA 정리(1): 기초 정리
  2. PWA 정리(2): 구성 요소

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

기본 구성

<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 &copy; {{ 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으로 구성

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>
    


  •     <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>