Vue2 컴포넌트

  • 컴포넌트는 이름을 갖고 있는 재사용가능한 뷰 인스턴스이다.
  • HTML 엘리먼트 확장하여 재사용 가능한 코드로 캡슐화
  • Vue 컴포넌트는 Vue 인스턴스이기도 하다. => 모든 옵션 객체를 사용할 수 있고, 라이프사이클 훅을 사용할 수 있다.

🍒 기본 예제

// Define a new component called button-counter
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

new Vue로 만든 루트 엘리먼트를 만들고,

// root element
new Vue({ el: '#components-demo' })

이 안에 컴포넌트를 추가한다.

<div id="components-demo">
  <button-counter></button-counter>
</div>

컴포넌트 또한 뷰 인스턴스 이므로, 옵션 객체에서 쓰이는 data, computed, watch, methods와 생명주기 훅을 사용할 수 있다. 단, 루트만 사용할 수 있는 el과 같은 옵션은 사용할 수 없다.

컴포넌트 재사용

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

컴포넌트가 새로 생성되면, 새로운 인스턴스가 생기는 것이다. 따라서 각 컴포넌트는 가자의 데이터를 갖게 된다.

컴포넌트에서 data 옵션은 반드시 함수

컴포넌트를 정의할 때 data 옵션은 반드시 함수여야 한다.
이렇게 함으로써, 각 인스턴스는 각자 독립적인 데이터 객체를 관리할 수 있게 된다.

  data () {
    return {
      count: 0,
    };
  },

Single Root Element

모든 컴포넌트는 하나의 루트 엘리먼트를 가져야 한다.

🍒 컴포넌트 등록 - 전역 or 지역

앱은 헤더, 사이드바, 컨텐츠 컴포넌트 등으로 구성되어있을 것이다.
이 컴포넌트들을 템플릿으로 쓰기 위해서는 등록이 되어야 뷰가 알 수 있다.

컴포넌트는 전역(global)과 지역(local)으로 등록할 수 있다.

컴포넌트 이름

컴포넌트를 등록할 떄는 반드시 "이름"을 줘야 한다.
kebab-case 로 이름을 지정하면, 엘리먼트에서 케밥케이스를 사용해야 한다. e.g.<my-component-name>

Vue.component('my-component-name', { /* ... */ })

PascalCase 로 이름을 지정하면, 케밥케이스나 파스칼 케이스로 엘리먼트에서 사용할 수 있다. e.g.<my-component-name> 혹은 <MyComponentName>.
알아둬야 할 점은 케밥케이스가 DOM에서 직접적으로 유효한 이름이라는 것이다.

직접 해봤을 때, Vue에 컴포넌트 등록시 파스칼 케이스를 쓰는것이 eslint 룰인 것 같고, 엘리먼트로 사용할 때는 케밥케이스로 사용해야 의도한 대로 이름이 지정된다.

전역 등록 : Vue.component

Vue.component를 사용하여 전역으로 컴포넌트를 등록할 수 있다.
전역으로 등록된 컴포넌트는 어떠한 루트 뷰 인스턴스(new Vue)에서도 사용할 수 있고, 심지어 그 안에 있는 subcomponent에서도 쓸 수 있다. 즉, 이 컴포넌트끼리는 그 안에서 서로를 사용할 수 있다는 것이다.

그러나, 전역 컴포넌트로 등록하는 것은 바람직하지 않을 수있다. 전역으로 컴포넌트를 등록한다는 것은 웹팩과 같은 빌드 시스템을 사용하였을 떄, 그 컴포넌트가 그만 사용되는 경우에도, 최종 빌드된 것에는 전역 컴포넌트가 포함되어 있다는 것이다. 이는 불필요하게 자바스크립트 다운로드 시간을 증가시킨다.

Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })
<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

지역 등록 : plain js object

전역 컴포넌트의 비효율성 때문에, 지역 컴포넌트를 사용하게 된다.
지역 컴포넌트는 plain js object로 만들어지게 된다.

var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }

컴포넌트를 만들었으면, 해당 컴포넌트를 사요하고자 하는 쪽에 components 옵션에 추가해야 한다. 이 때 key는 컴포넌트의 이름이고, value는 컴포넌트의 옵션객체(options object)이다.

new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

전역컴포넌트와 다른 점은 지역 컴포넌트는 subcomponent에서 사용할 수 없다는 것이다. CompontB에서 ComponentA를 쓰려면, 마찬가지로 components에 등록해야 한다.

import ComponentA from './ComponentA.vue'

export default {
  components: {
    ComponentA
  },
  // ...
}

모듈 시스템에서 지역 컴포넌트로 등록하기

모듈 시스템에서는 components 폴더를 만들어서, 각 컴포넌트가 하나의 파일을 갖도록 하는게 좋다.

ComponntB.vue파일에서 componentA와 componentB를 import해와서 등록하면 된다.

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    ComponentA,
    ComponentC
  },
  // ...
}

base component를 자동으로 지역 컴포넌트로 등록하기

예제 코드

button, input과 같은 엘리먼트는 여러 컴포넌트에서 자주 사용하게 될 수 있다. 이를 base component라고 한다.

이 경우 require.context를 사용해서 전역으로 컴포넌트를 등록할 수 있다.

🍒 Props

뷰 컴포넌트에서 부모-자식의 관계는 props는 아래로, events는 위로이다.

  • 네이밍
    HTML 속성은 대소문자를 비교하지 않기 때문에, js에서는 camelCase를, 해당하는 HTML에는 kebab-case를 사용해야 한다.

    문자열 템플릿을 사용하면 이 제한이 적용되지 않는다.

props로 데이터 전달하기

자식 컴포넌트가 상위 컴포넌트를 직접 참조할 수 없으므로, props 옵션으로 데이터를 하위컴포넌트에 전달할 수 있다.

Vue.component('child', {
  // props 정의
  props: ['message'],
  // 데이터와 마찬가지로 prop은 템플릿 내부에서 사용할 수 있으며
  // vm의 this.message로 사용할 수 있습니다.
  template: '<span>{{ message }}</span>'
})
<child message="안녕하세요!"></child>

props에 static 값 전달하기

<blog-post title="My journey with Vue"></blog-post>

dynamic Props 전달하기 : v-bind

동적으로 변하는 값은 v-bind로 전달달해야 한다.
데이터가 상위에서 업데이트될 때마다, 하위에도 전달된다.

<!-- Dynamically assign the value of a variable -->
<blog-post v-bind:title="post.title"></blog-post>

<!-- Dynamically assign the value of a complex expression -->
<blog-post
  v-bind:title="post.title + ' by ' + post.author.name"
></blog-post>

객체의 모든 속성을 props로 전달하기

객체의 모든 속성을 props로 전달하려면, v-bind:prop-name 대신 v-bind로 전달할 수 있다.

<blog-post v-bind="post"></blog-post>

리터럴 vs 동적

  • 숫자 전달하기

    숫자를 전달할 때, 리터럴하게 전달하면 일반 문자열이 전달된다.
    숫자를 전달하려면 js expression으로 평가되도록 v-bind를 사용해야 한다.

    ​​​​<!-- 리터럴 : 이것은 일반 문자열 "1"을 전달합니다. -->
    ​​​​<comp some-prop="1"></comp>
    ​​​​<!-- 동적 : 이것은 실제 숫자로 전달합니다. -->
    ​​​​<comp v-bind:some-prop="1"></comp>
    
  • boolean 전달하기

    • 어떠한 값도 넘기지 않는 것은 암시적으로 true를 넘기는 것이다.
    • false가 static하더라도, v-bind를 써야, 뷰가 이것이 string이 아닌 js expression으로 인식한다.
      => string을 넘기려는 경우를 제외하고는 다, v-bind를 써야하는구나.
    ​​​​<!-- Including the prop with no value will imply `true`. -->
    ​​​​<blog-post is-published></blog-post>
    
    ​​​​<!-- Even though `false` is static, we need v-bind to tell Vue that -->
    ​​​​<!-- this is a JavaScript expression rather than a string.          -->
    ​​​​<blog-post v-bind:is-published="false"></blog-post>
    
    ​​​​<!-- Dynamically assign to the value of a variable. -->
    ​​​​<blog-post v-bind:is-published="post.isPublished"></blog-post>
    
  • array 전달하기

    • js expression인 것을 알리기 위해 v-bind로 배열을 넘겨야 한다.
    ​​​​<!-- Even though the array is static, we need v-bind to tell Vue that -->
    ​​​​<!-- this is a JavaScript expression rather than a string.            -->
    ​​​​<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>
    

props 검증

리액트의 propTypes처럼 prop의 타입, 필수인지 여부, 디폴트값을 설정할 수 있다.

Vue.component('example', {
  props: {
    // 기본 타입 확인 (`null` 은 어떤 타입이든 가능하다는 뜻입니다)
    propA: Number,
    // 여러개의 가능한 타입
    propB: [String, Number],
    // 문자열이며 꼭 필요합니다
    propC: {
      type: String,
      required: true
    },
    // 숫자이며 기본 값을 가집니다
    propD: {
      type: Number,
      default: 100
    },
    // 객체/배열의 기본값은 팩토리 함수에서 반환 되어야 합니다.
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 사용자 정의 유효성 검사 가능
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})

type으로는 다음이 가능하다.

String
Number
Boolean
Function
Object
Array
Symbol

props는 컴포넌트 인스턴스가 생성되기 전에 검증되므로, default, validator 함수 내에서 data, computed, methods와 같은 인스턴스 데이터를 사용할 수 없다.

단방향의 데이터 흐름

모든 props는 단방향의 바인딩이다.
부모의 프로퍼티가 변경되면, 흐름이 자식 컴포넌트에게로 전달된다.
그러나, 그 반대는 해당하지 않는다.

🍒 자식 컴포넌트의 이벤트를 리스닝하기

  • 이벤트 이름 : 케밥-케이스
Vue.component('BlogPost', {
  props: ['post'],
  template: `
    <div class='blog-post'>
      <h3>{{ post.title }}</h3>
      <button v-on:click="$emit('enlarge-text', 0.1)"> 
        Enlarge text
      </button>
      <div v-html='post.content'></div>
    </div>
  `,
});

new Vue({
  el: '#blog-posts-events-demo',
  data: {
    posts: [...],
    postFontSize: 1,
  },
  methods: {
    onEnlargeText (amount) {
      this.postFontSize += amount;
    },
  },
});
    <div id='blog-posts-events-demo'>
      <div :style="{fontSize: postFontSize + 'em'}">
        <blog-post
          v-for='post in posts'
          v-bind:key='post.id'
          v-bind:post='post'
          v-on:enlarge-text='onEnlargeText'
        >
        </blog-post>
      </div>
    </div>

부모 컴포넌트는 자식 컴포넌트로부터 어떠한 이벤트를 리스닝할 것인지를 v-on으로 등록할 수 있다. 부모 컴포넌트는 enlarge-text라는 이벤트를 리스닝하고 있고, 해당 이벤트 핸들러는 메소드로 등록한 onEnlargeText가 처리하게 된다.

    <blog-post
    ...
    v-on:enlarge-text='onEnlargeText'
    ></blog-post>

자식 컴포넌트는 클릭 이벤트가 발생하면 enlarge-text 이벤트를 emit하여 부모 컴포넌트에게 알린다.

이 때 부모 컴포넌트에게 값을 전달하기 위해, 두 번째 파라미터에 값을 넣어준다.

<button v-on:click="$emit('enlarge-text', 0.1)"> 

부모 컴포넌트는 이벤트핸들러 함수의 파라미터로 값을 전달받을 수 있다.

onEnlargeText (amount) {
  this.postFontSize += amount;
},

메소드를 사용하지 않고, 바로 이벤트 핸들러를 등록했다면, $event로 값을 받을 수 있다.

<blog-post
  ...
  v-on:enlarge-text="postFontSize += $event"
></blog-post>

🍒 v-model을 컴포넌트에서 사용하기

인풋을 쓸 때 v-model을 사용하였다.

<input v-model="searchText">

v-model을 컴포넌트에서 사용할 때 동작하는 방식은 이러하다.

<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>

이게 동작하기 위해서 컴포넌트 내부에 있는 <input>은 반드시 다음을 수행해야 한다.

  • value 속성을 value prop과 bind시켜야 한다.
  • input에 새로운 값($event)과 함께 커스텀 인풋 이벤트를 emit해야 한다.

액션 내에서는 다음과 같다.

Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    >
  `
})

컴포넌트에서는 v-model을 이렇게 사용하면, 완벽히 동작하게 된다.

<custom-input v-model="searchText"></custom-input>

🍒 동적 컴포넌트 <component>

tab을 생각해보면, 동적으로 컴포넌트들이 변경되는 것을 알 수 있다.
이것은 뷰의 <component> 엘리먼트와 is 속성으로 가능하다.

코드샌드박스 예제

동적 컴포넌트 keep-alive

is를 사용해서 탭을 이동하게 했을 때, 뷰는 새로운 인스턴스를 만들기 때문에 이전 컴포넌트에서의 state는 저장되지 않는다.

처음 인스턴스가 생성된 후로, 컴포넌트 인스턴스를 캐쉬하고 싶을 경우엔 동적 컴포넌트를 <keep-alive> 엘리먼트로 감싸주면 된다.

<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

keep-alive를 사용할 때는 컴포넌트 간의 스위칭은 이름으로 이루어져야 한다. 즉, 컴포넌트의 name option이나, local/global 등록을 통해 이루어져야 한다.

Select a repo