• [Vue] 기초 스터디

    2020. 5. 21.

    by. 나나 (nykim)

    이 글은 Vue.js 시작하기 - Age of Vue.js 강의를 보고 작성한 노트 필기입니다.
    뷰는 1도 모르지만 친해지고 싶은 사람이 공부를 위해 작성한 글입니다 :-9
    필요할 때마다 수정/보완해 나갈 예정입니다.

     

     

                 

     

    Vue.js 소개

    Vue는 사용자 인터페이스를 만들기 위한 프로그레시브 프레임워크 입니다.

     

     

    공식 페이지

    - vuejs.org

    - kr.vuejs.org (한국어)

     

    위 사이트에서 상세한 공식 문서를 제공하고 있습니다. 

     

     

    Reactivity 

    Reactivity는 Vue가 가진 가장 큰 특징이라고 할 수 있습니다.

    Vue가 하는 일을 흉내내보면 이렇습니다.

    <!-- 개발버전, 도움되는 콘솔 경고를 포함. -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    
    <body>
      <div id="app"></div>
      <script>
        var div = document.querySelector('#app');
        var viewModel = {};
    
        (function () {
        function init() {
        Object.defineProperty(viewModel, 'str', {
        // 속성에 접근했을 때의 동작을 정의
        get: function () {
        console.log('접근');
        },
    
        // 속성에 값을 할당했을 때의 동작을 정의
        set: function (newValue) {
        console.log('할당', newValue);
        render(newValue);
        },
        });
        }
    
        function render(value) {
        div.innerHTML = value;
        }
    
        init();
        })();	
      </script>
    </body>

     

    위 코드를 실행 후, 콘솔에 viewModel.str = "안녕하세요!"`와 같이 입력하면 해당 글씨가 화면에 바로 나타나게 됩니다.

    이를 Vue로 표현하면 아래와 같습니다.

     

    <body>
      <div id="app">{{ str }}</div>
      <script>
        new Vue({
          el: '#app',
          data: {
            str: ''
          }
        });
      </script>
    </body>

     

    이는 Vue의 주요 특징 중 하나인 Reactivity(반응성)입니다.

     


     

     

    인스턴스

    모든 Vue 앱은 Vue함수로 새 Vue 인스턴스를 만드는 것부터 시작합니다.

    인스턴스를 참조하기 위한 이름으로 변수 vm을 사용한다고 합니다.

     

    var vm = new Vue({
      el: '#app',
    });

     

    Vue 인스턴스가 생성되면, 내부의 data 오브젝트 내의 모든 프로퍼티를 뷰의 '반응성 시스템'에 집어 넣습니다.

    그래서 프로퍼티 값이 바뀌면 그 새로운 값에 맞추어 View도 바뀌게 됩니다.

     

    var data = {
      a: 1
    };
    
    var vm = new Vue({
      data: data
    });
    
    data.a = 999;
    console.log(vm.a); //999

     

    하지만 이런 반응성은 나중에 추가된 프로퍼티에는 적용되지 않습니다.

    인스턴스가 생성될 때 이미 존재한 것들만 반응형이기 때문에, 빈 값이라 하더라도 초기값을 지정해주는 것이 좋습니다.

     

    data: {
      newTodoText: '',
      visitCount: 0,
      hideCompletedTodos: false,
      todos: [],
      error: null
    }

     


     

    컴포넌트

    컴포넌트는 화면의 영역을 구분하여 재사용가능한 단위입니다. 컴포넌트 기반으로 개발하면 재사용성이 올라가고 빠르게 화면을 제작할 수 있습니다.

     

    <body>
      <div id="app">
        <app-button></app-button>
      </div>
    
      <script>
        Vue.component('app-button', {
          template: '<button>버튼</button>'
        });
    
        new Vue({
          el: '#app'
        })
      </script>
    </body>

     

    전역 컴포넌트를 등록하는 가장 쉬운 방법은 Vue.component('이름', '내용')입니다.

    이때, 컴포넌트 이름은 HTML 요소와 똑같이 짓지 않도록 주의해야 합니다.

     

    <body>
      <div id="app">
        <button></button>
      </div>
    
      <script>
        Vue.component('button', {
          template: '<button>버튼</button>'
          //[Vue warn] Do not use built-in or reserved HTML elements as component
        });
    
        new Vue({
          el: '#app'
        })
      </script>
    </body>

     

    위 코드에서 <button>이 HTML 마크업인지, Vue 컴포넌트인지 알 수 없기 때문입니다 😵

     

     

     

    지역 컴포넌트

    지역 컴포넌트는 전역 컴포넌트보다 자주 사용하게 될 방법입니다.

     

    <body>
      <div id="app">
        <app-button></app-button>
      </div>
      <script>
    
        new Vue({
          el: '#app',
          components: {
            'app-button': {
              template: '<button>지역 컴포넌트</button>'
            }
          }
        })
      </script>
    </body>

     

    차이점은 여러 개의 컴포넌트를 등록하기 때문에, 인스턴스의 옵션 이름이 components인 점입니다.

    실제 개발에는 전역 컴포넌트를 쓰는 경우는 외부 라이브러리나 플러그인을 쓸 때가 대부분이라고 합니다.

     

     

     

    컴포넌트 내 data

    컴포넌트의 data 옵션은 반드시 함수여야 합니다. 그래야 각 인스턴스가 리턴된 data 오브젝트를 개별로 관리할 수 있기 때문입니다.

    따라서 아래처럼 쓰지 않고...

     

    <div id="app">
      <app-button></app-button>
    </div>
    <script>
      new Vue({
        el: '#app',
        components: {
          'app-button': {
            data: {
              counter: 0
              //[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.
            },
            template: '<button v-on:click="counter++">{{counter}}번 클릭</button>'
          }
        }
      })
    </script>

     

    아래와 같이 작성합니다.

     

    <body>
      <div id="app">
        <app-button></app-button>
        <app-button></app-button>
      </div>
      
      <script>
        new Vue({
          el: '#app',
          components: {
            'app-button': {
              data: function () {
                return {
                  counter: 0
                }
              },
              template: '<button v-on:click="counter++">{{counter}}번 클릭</button>'
            }
          }
        })
      </script>
    </body>

     

     

     

     

    컴포넌트 통신의 기본 - props

    컴포넌트는 부모-자식 관계를 이룰 수 있습니다 (훈훈하네요)

    부모는 자식에게 데이터를 전달해야 할 수도 있으며, 자식은 자신에게 일어난 일을 부모에게 알릴 필요가 있습니다.

    이렇게 명확한 체계를 만들어두면 데이터 흐름을 쉽게 파악할 수 있습니다.

     

     

    하위 컴포넌트의 템플릿은 상위 데이터를 직접 참조할 수 없습니다.

    대신 상위 컴포넌트가 props를 통해 데이터를 하위 컴포넌트에게 전달해 줄 수 있습니다.

    이때, 하위 컴포넌트는 props 옵션을 통해 받아오게 될 props를 명시적으로 선언해야 합니다.

     

    <body>
      <div id="app">
        <child></child>
      </div>
      
      <script>
        new Vue({
          el: '#app',
          components: {
            'child': {
              template: '<div>{{ msg }}</div>'
            }
          }
        })
      </script>
    </body>

     

    이 상태에서 msg라는 data를 child에게 보내주고 싶습니다.

    그럴 때는 child 컴포넌트 아래에 props를 정의합니다. msg를 받아올래요! 라고 적어두는 거죠.

     

    // ...
    components: {
      'child': {
        props: ['msg'],
        template: '<div>{{ msg }}</div>'
      }
    }
    // ...

     

    그러면 일반 문자열을 전달해 줄 수 있습니다.

     

    <div id="app">
      <child msg="안녕하세요!"></child>
    </div>

     

     

    한편 v-bind를 사용해 부모의 데이터에 props를 동적으로 바인딩할 수 있습니다.

    이렇게 하면 상위에서 데이터가 업데이트 되면 하위에서도 자동으로 업데이트 됩니다.

     

    이때는v-bind:프롭스 속성 이름="상위 컴포넌트 데이터 이름"으로 작성합니다.

     

    <body>
      <div id="app">
        <child v-bind:child-text="text"></child>
      </div>
      
      <script>
        new Vue({
          el: '#app',
          data: {
            text: '텍스트 123'
          },
          components: {
            'child': {
              props: ['childText'],
              template: '<div>{{ childText }}</div>'
            }
          }
        })
      </script>
    </body>

     

    위 코드에서 v-bind:child-text라고 적은 이유는 HTML 속성은 대소 문자를 구분하지 않기 때문입니다.

    따라서 문자열이 아닌 템플릿을 사용할 때는 kebab-case(하이픈으로 구분하는 것)을 써야 합니다.

     

    아래는 또 다른 예입니다.

     

    <div id="app">
      <child v-bind:data-one="number" v-bind:data-two="string"></child>
    </div>
    <script>
      new Vue({
        el: '#app',
        data: {
          number: 522,
          string: "Hello!"
        },
        components: {
          'child': {
            props: ['dataOne', 'dataTwo'],
            template: '<div>값1은 {{ dataOne }}이고, 값2는 {{ dataTwo }}입니다.</div>'
          }
        }
      })
    </script>

     

     

    컴포넌트 통신의 기본 - $emit event

    Vue 인스턴스는 $on(이벤트이름)으로 이벤트를 감지하고, $emit(이벤트이름)으로 이벤트를 트리거할 수 있습니다.

    이는 addEventListenerdispatchEvent와 비슷하게 작동하지만 이들 API를 지칭하는 것은 아닙니다.

     

    <div id="app">
      <child></child>
    </div>
    <script>
      var child = {
        template: '<button v-on:click="passEvent">Click me</button>',
        methods: {
          passEvent: function () {
            this.$emit('pass');
          }
        }
      }
      new Vue({
        el: '#app',
        components: {
          'child': child
        }
      })
    </script>

     

     

    개발자 도구의 이벤트 탭을 확인하면 pass 이벤트가 발생한 것을 볼 수 있습니다.

    이렇게 자식 컴포넌트가 '나 클릭 이벤트 발생했어요!'라고 알려주었으니, 이때 콘솔에 찍는 메서드를 실행시켜 봅시다.

     

    <div id="app">
      <child v-on:pass="logText"></child>
    </div>
    <script>
      var child = {
        template: '<button v-on:click="passEvent">Click me</button>',
        methods: {
          passEvent: function () {
            this.$emit('pass');
          }
        }
      }
      new Vue({
        el: '#app',
        components: {
          'child': child
        },
        methods: {
          logText: function () {
            console.log('클릭!');
          }
        }
      })
    </script>

     

    그럼 아래 흐름에 의해 콘솔에 문자가 찍히게 됩니다.

    'click 이벤트가 감지됨 → pass 이벤트 발생 → pass 이벤트가 감지됨 →logText메서드 실행'

     

     

    아래는 또다른 예시입니다.

     

    <script>
      var upButton = {
        template: '<button v-on:click="upNumber">Up</button>',
        methods: {
          upNumber: function () {
            this.$emit('increase');
          }
        }
      }
    
      var downButton = {
        template: '<button v-on:click="downNumber">Down</button>',
        methods: {
          downNumber: function () {
            this.$emit('decrease');
          }
        }
      }
    
    
      new Vue({
        el: '#app',
        data: {
          num: 10
        },
        components: {
          'up-button': upButton,
          'down-button': downButton
        },
        methods: {
          increaseNum: function () {
            this.num++;
          },
          decreaseNum: function () {
            this.num--;
          }
        }
      })
    </script>

     

     

    같은 컴포넌트 레벨 간의 통신 방법

     

    같은 자식 컴포넌트라 하더라도 바로 데이터를 전달할 수는 없고, 일단 부모 컴포넌트를 통해야 합니다.

     

     

    예를 들어 위와 같은 구조로 컴포넌트가 있다고 가정해 봅니다.

    여기서 Content 내의 버튼을 클릭한 경우 Header 내부의 숫자가 10씩 올라가도록 설정하려고 합니다.

    하지만 Content가 바로 데이터를 전달해 줄 수는 없기 때문에 Root를 통해야 합니다.

     

     

     

    즉, 위의 형태로 데이터 흐름이 이루어져야 합니다.

    이를 코드로 표현하면 아래와 같습니다.

     

    <div id="app">
      <app-header v-bind:propsdata="num"></app-header>
      <app-content v-on:pass="deliverNum"></app-content>
    </div>
    <script>
      var appHeader = {
        template: '<div>Header <p>{{ propsdata }}</p></div>',
        props: ['propsdata'],
      };
      var appContent = {
        template:
          '<div>Content <button v-on:click="passNum">Pass</button> </div>',
        methods: {
          passNum: function () {
            this.$emit('pass', 10);
          },
        },
      };
      var vm = new Vue({
        el: '#app',
        components: {
          'app-header': appHeader,
          'app-content': appContent,
        },
        data: {
          num: 0,
        },
        methods: {
          deliverNum: function (value) {
            this.num += value;
          },
        },
      });
    </script>

     

    흐름을 풀어서 설명하면 이렇습니다.

     

    1) Content 컴포넌트 내에서 버튼의 클릭 이벤트 감지

    2) Content 컴포넌트에 등록된 passNum 메서드 실행

    3) pass 이벤트를 10이라는 인자와 함께 발생시킴

    4) Root에서 Content 컴포넌트로부터 pass 이벤트 감지

    5) Root에 등록된 delieverNum 메서드 실행

    6) v-bind를 통해 변경된 num이 Header에 propsdata라는 속성의 porps로 전달됨

    7) Header 내에 props로 넘어온 변경된 num을 반영함

     

     


     

    Vue 라우터

    Vue에서 제공하는 공식 라이브러리로 싱글 페이지 앱을 손쉽게 만들 수 있도록 도와줍니다.

    공식문서 링크

    공식문서 링크(한국어)

     

    라우터를 사용하려면 우선 설치해야 합니다.

     

    # NPM
    npm install vue-router
    <!-- CDN -->
    <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

     

     

    라우터는 new VueRouter()로 사용하며, router:router로 인스턴스에 연결합니다.

     

    var router = new VueRouter({
      /*
      routes: [
        {},
        {},
        {
          path: '' //url
          component: '' //표시될 컴포넌트
        }
      ]
      */
    });
    
    new Vue({
      el: '#app',
      router: router
    })
    var router = new VueRouter({
      routes: [
        {
          path: '/main',
          component: MainComponent
        },
        {
          path: '/login',
          component: LoginComponent
        }
      ]
    });

     

    url을 변경하려면 <router-link to="path" />를 사용합니다.

    이는 a 태그로 변환되며, to에 지정한 path url로 이동합니다.

     

    <router-view />아래에는 url에 따라 서로 다른 view가 나타나게 됩니다.

     

    <div id="app">
    
      <div class="link">
        <!-- 클릭 시 해당 url로 이동 -->
        <router-link to="/main">Main</router-link>
        <router-link to="/login">Login</router-link>
      </div>
    
      <!-- 이 아래에 router에 따른 컴포넌트를 보여줌 -->
      <router-view />
    </div>
    <script>
    
      var MainComponent = {
        template: '<div>Main</div>'
      }
      var LoginComponent = {
        template: '<div>Login</div>'
      }
    
      var router = new VueRouter({
        routes: [
          {
            path: '/main',
            component: MainComponent
          },
          {
            path: '/login',
            component: LoginComponent
          }
        ]
      });
    
      new Vue({
        el: '#app',
        router: router
      });
    
    </script>

     

    router 속성으로 mode:history를 넣어주면 url을 이동할 때마다 브라우저의 세션 기록 스택에 상태를 추가합니다.

     

    var router = new VueRouter({
      mode: 'history',
      routes: [
        {
          name: 'Main Page',
          path: '/main',
          component: MainComponent
        },
        {
          path: '/login',
          component: LoginComponent
        }
      ]
    });

     

     


     

    Axios

    Axios는 뷰에서 권고하는 프로미스 기반의 HTTP 통신 라이브러리입니다.

    (*프로미스: 자바스크립트 비동기 처리를 위한 객체)

     

    Axios Github 링크

    #NPM
    npm install axios
    <!-- Axios CDN -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>

     

    아래는 Axios를 통해 데이터를 받아오는 예시입니다.

     

    <div id="app">
      <button v-on:click="getData">get user</button>
    </div>
    
    <script>
      var vm = new Vue({
        el: '#app',
        data: {
          users: []
        },
        methods: {
          getData: function () {
    
            console.log(this === vm); //TRUE! 여기서의 this는 Vue 인스턴스를 가리킨다
    
            axios.get('https://jsonplaceholder.typicode.com/users/')
    
              .then(function (response) {
                console.log(this === vm); //FALSE! 여기서의 this는 전역 글로벌 객체(Window)를 가리킨다 
                console.log(this); //비동기 처리 후의 성공, 실패 코드에서는 실행 컨텍스트가 변경된다
    
                console.log(response);
                vm.users = response.data;
              })
    
              .catch(function (error) {
                console.log(error);
              })
    
          }
        }
      })
    </script>

     

     

    이와 관련해 캡틴 판교님이 남긴 참고하면 좋은 글입니다 :)

    - 자바스크립트 비동기 처리와 콜백 함수

    - 자바스크립트 Promise 쉽게 이해하기

    - 자바스크립트 async와 await

     

     


     

    템플릿 문법

    Vue의 템플릿 문법을 통해 View를 손쉽게 조작할 수 있습니다.

    공식문서 링크

    공식문서 링크(한국어)

     

     

    보간법 (Interpolation)

    콧수염(Mustache) 구문을 통해 데이트 바인딩이 가능합니다.

    <div id="app">
      <p>메시지: {{ msg }}</p>
    </div>
    
    <script>
      var vm = new Vue({
        el: '#app',
        data: {
          msg: '안녕하세요!'
        }
      })
    </script>

     

     

    디렉티브 (Directives)

    v- 접두사가 붙는 Vue만의 특수한 속성을 디렉티브라고 부릅니다. Vue는 화면 조작에 자주 사용되는 방식을 모아 디렉티브 형태로 제공하고 있습니다.

     

    <div id="app">
      <p v-bind:id="uuid">안녕하세요.</p>
    </div>
    
    <script>
      var vm = new Vue({
        el: '#app',
        data: {
          uuid: 'id-name'
        }
      })
    </script>
    <div id="app">
      <div class="message">
        <div v-if="loading">
          로딩...
        </div>
        <div v-else>
          로그인 완료!
        </div>
      </div>
      <p>Hello!</p>
    </div>
    <script>
      var vm = new Vue({
        el: '#app',
        data: {
          loading: true
        },
      })
    </script>

    v-if는 DOM을 추가/삭제하고, v-show는 display 속성을 block/none으로 바꾸는 차이점이 있습니다.

     

    <div id="app">
      <a v-bind:href="url">링크</a>
    </div>
    
    <script>
      new Vue({
        el: '#app',
        data: {
          url: 'https://nykim.work'
        }
      })
    </script>

     

    v-bind는 반응적으로 HTML 속성을 갱신하는 데 사용합니다.

     

    <div id="app">
      <input type="text" v-on:keydown="keydownEvt" />
      <input type="text" v-on:keydown.enter="keydownEnterEvt" />
    </div>
    
    <script>
      new Vue({
        el: '#app',
        methods: {
          keydownEvt: function () {
            console.log("keydown 이벤트 발생");
          },
          keydownEnterEvt: function () {
            console.log("Enter키의 keydown 이벤트 발생");
          }
        }
      })
    </script>

     

    v-on은 DOM 이벤트를 수신합니다.

     

     

    computed 속성

    템플릿 내에는 표현식을 넣을 수 있지만 너무 많은 연산이 들어가면 유지보수가 어려워집니다.

    복잡한 로직이라면 computed 속성을 쓰는 것이 좋습니다.

     

    computed 속성은 종속 대상을 따라 저장(캐싱)됩니다.

    따라서 해당 대상이 변경될 때만 함수가 실행되며, 변경되지 않으면 여러 번 호출해도 계산되어 있던 결과를 반환합니다.

     

    <div id="example">
      <p>원본 메시지: "{{ message }}"</p>
      <p>역순으로 표시한 메시지: "{{ reversedMessage }}"</p>
    </div>
    
    <script>
      var vm = new Vue({
        el: '#example',
        data: {
          message: '안녕하세요'
        },
        computed: {
          // 계산된 getter
          reversedMessage: function () {
            // `this` 는 vm 인스턴스를 가리킵니다.
            return this.message.split('').reverse().join('')
          }
        }
      })
    </script>

     

    v-bind와 computed를 활용해 DOM에 상황에 맞는 클래스를 추가/삭제해 줄 수 있습니다.

     

    <div id="app">
      <p v-bind:class="errorColor">텍스트</p>
    </div>
    
    <script>
      new Vue({
        el: '#app',
        data: {
          isError: true
        },
        computed: {
          errorColor: function () {
            return this.isError ? 'warning' : null;
          }
        }
      })
    </script>

     

     

    watch 속성

    <div id="app">
      {{num}}
      <button v-on:click="upNumber">increase</button>
    </div>
    
    <script>
      new Vue({
        el: '#app',
        data: {
          num: 10
        },
        watch: {
          num: function () {
            this.logText();
          }
        },
        methods: {
          upNumber: function () {
            this.num = this.num + 1;
          },
          logText: function () {
            console.log("값이 바뀜!");
          }
        }
      })
    </script>

     

    watch 속성은 데이터가 변경되면 그에 반응합니다.

    computed와 비슷하지만 차이점이 있습니다. (공식문서 링크) (공식문서 링크(한국어))

     

    computed는 유효성 체크처럼 값을 받아와 확인할 때 사용하고, watch는 그보다 무거운 로직(데이터 변경에 대한 응답이 비동기식이거나 시간이 많이 소요되는 경우)를 다룰 때 사용한다고 합니다.

     

     


     

    Vue CLI (Command Line Interface)

    CLI는 내부 Webpack을 통해 빠르게 프로젝트를 생성하고, 손쉽게 플러그인을 사용할 수 있습니다.

    설치를 위해서는 node.js와 npm이 필요합니다.

     

    npm install -g @vue/cli
    #OR
    yarn global add @vue/cli

     

    설치 권한이 없다고 뜨는 경우 sudo 키워드와 함께 실행합니다. 로컬로 설치하고 싶은 경우 -g를 제외하고 실행합니다.

     

     

    Vue CLI 3.x 버전부터는 create 키워드를 통해 손쉽게 프로젝트를 만들 수 있습니다.

     

    vue create <프로젝트 이름>

     

    설치 과정에서 default 옵션으로 만들지, 플러그인을 입맛대로 바꿀 건지 물어봅니다.

    Manually를 택할 경우 Babel, TypeScript 등의 설정이 가능합니다.

     

    cd <프로젝트 이름>
    npm run serve

     

     

    vue 파일은 <template> <script> <style>의 구조를 가집니다.

     

    <!-- HelloWorld.vue -->
    
    <template>
      <div class="hello">
        <h1>{{ msg }}</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: "HelloWorld",
      props: {
        msg: String
      }
    };
    </script>
    
    <style>
    .hello {
      color: #42b983;
    }
    </style>

     

    이렇게 만든 vue 페이지는 하위 컴포넌트로서 다른 컴포넌트 내에 넣을 수 있습니다.

     

    <!-- App.vue -->
    
    <template>
      <div id="app">
        <HelloWorld msg="안녕하세요!" />
      </div>
    </template>
    
    <script>
    import HelloWorld from "./components/HelloWorld.vue";
    
    export default {
      name: "App",
      components: {
        HelloWorld
      }
    };
    </script>
    
    <style>
    #app {
      margin: 60px auto;
      text-align: center;
    }
    </style>
    

     

    VS code의 경우, vue 키워드 입력 후 Tab 키를 눌러 빠르게 템플릿을 생성할 수 있습니다.

     

     

    CLI with Axios

    아래는 Axios를 활용한 코드 예시입니다.

    (npm i axios로 설치 필요)

     

    <template>
      <form v-on:submit.prevent="submitForm">
        <div>
          <label for="username">ID</label>
          <input id="username" type="text" v-model="username" />
        </div>
        <div>
          <label for="password">PW</label>
          <input id="password" type="password" v-model="password" />
        </div>
        <button type="submit">Login</button>
      </form>
    </template>
    
    <script>
    import axios from "axios";
    
    export default {
      data: function() {
        return {
          username: "",
          password: ""
        };
      },
      methods: {
        submitForm: function() {
          var url = "https://jsonplaceholder.typicode.com/users";
          var data = {
            username: this.username,
            password: this.password
          };
          axios
            .post(url, data)
            .then(function(response) {
              console.log(response);
            })
            .catch(function(error) {
              console.log(error);
            });
        }
      }
    };
    </script>
    
    <style>
    </style>

     

     

    댓글 0