티스토리 뷰

Vue.JS

[Vue.JS] 컴포넌트 (기본)

버미노트 2019. 1. 16. 22:56

이번 포스트에서는 SPA에서 가장 중요하다 이야기 되는 컴포넌트에 관해 이야기 할 것입니다.

1. 기본 예제

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

위의 예제는 <button-counter> 라는 이름으로 재사용이 가능한 컴포넌트입니다.

<div id="app">
  <button-counter><button-counter/>
</div>
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button @click="count++">You clicked me {{ count }} times.</button>'
});

new Vue({
  el: '#app'
});

위의 예제와 같이 컴포넌트를 사용할 수 있습니다. 컴포넌트를 생성할 때 사용되는 옵션은 new Vue에 사용되는 옵션(data, computed, watch, methods ...)과 동일 합니다. 단 하나의 예외는 컴포넌트에는 el 옵션을 사용할 수 없다는 것입니다.

위의 코드의 결과는 CodePen에서 확인 할 수 있습니다.

2. 컴포넌트 재사용

<div id="app">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: 'You clicked me {{ count }} times.'
});

new Vue({
  el: '#app'
});

컴포넌트는 원하는 만큼 여러번 재사용이 가능합니다. 위의 예제의 버튼들은 클릭 될 때 각각의 count 값을 나타냅니다. 컴포넌트를 사용할 때 마다 컴포넌트의 인스턴스가 생성되기 때문에 컴포넌트 마다 서로 다른 count를 가지게 되는 것입니다.

위의 예제 결과는 CodePen에서 확인 할 수 있습니다.

* 참고 - 컴포넌트의 data 옵션은 반드시 함수형이어야 합니다.

new Vue에서 옵션으로 넘겨주는 data는 객체형, 함수형 모두 가능합니다. 하지만 컴포넌트에서 data는 반드시 함수형이여야 합니다.

3. 컴포넌트 구조

Vue는 흔히 컴포넌트가 트리 형태로 중첩되어 사용됩니다.

예를 들면, 헤더와 사이드바, 콘텐츠 영역으로 나뉘며, 콘텐츠 영역은 포스트 등.. 각각의 영역은 다른 컴포넌트로 구성되어 집니다.

Vue의 template에 컴포넌트가 사용되기 위해서는 Vue가 알 수 있도록 컴포넌트가 등록되어야 합니다. 전역으로 사용할 수 있는 글로벌 방식, 해당인스턴스에서만 사용할 수 있는 로컬 방식 두가지 방법으로 컴포넌트를 등록 할 수 있습니다.

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

위의 방법을 사용하여 글로벌 방식으로 컴포넌트를 등록할 경우, 모든 Vue 인스턴스에서 컴포넌트를 사용 할 수 있게 됩니다.

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

var ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}
import ComponentA from './ComponentA.vue'

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

위의 방법을 사용하여 로컬 방식으로 컴포넌트를 등록할 경우, 컴포넌트를 등록한 Vue 인스턴스에서만 컴포넌트를 사용할 수 있습니다.

4. props 사용하기

부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하기 위해 흔히 사용 됩니다. propsdata와 유사하게 사용 가능한 속성입니다.

Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'
})

위의 예제와 같이 props를 정의하면 정의한 이름으로 template에 사용 가능합니다. props로 원하는 만큼 전달하여 사용할 수 있으며, 모든 값을 props로 전달 가능합니다.

<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>

부모 컴포넌트에서는 위의 예제와 같이 자식 컴포넌트로 데이터 전달을 할 수 있습니다.

<div id="app">
  <blog-post
    v-for="post in posts"
    v-bind:key="post.id"
    v-bind:title="post.title"
  ></blog-post>
</div>
Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'
})

new Vue({
  el: '#app',
  data: {
    posts: [
      { id: 1, title: 'My journey with Vue' },
      { id: 2, title: 'Blogging with Vue' },
      { id: 3, title: 'Why Vue is so fun' }
    ]    
  }
});

위의 예제와 같이 v-forprops를 함께 사용하면 코드 중복 없이 간결하게 코딩이 가능합니다.

위의 예제는 CodePen에서 확인 할 수 있습니다.

5. 단일 루트

컴포넌트는 단일 루트로 감싸져야 합니다. 이전에 이야기 하면서 예를 들었던 <blog-post>로 이야기 하자면,

<h3>{{ title }}</h3>

<blog-post> 컴포넌트는 위의 코드와 같이 title만 나타내는 하나의 엘리먼트만 가지고 있습니다.

<h3>{{ title }}</h3>
<div v-html="content"></div>

여기에 content를 나타내는 엘리먼트를 추가하기 위해 위의 예제와 같이 작성하면 에러가 발생하게 됩니다.

<div class="blog-post">
  <h3>{{ title }}</h3>
  <div v-html="content"></div>
</div>

컴포넌트는 위의 예제와 같이 단일 루트로 감싸져야 합니다.

<blog-post
  v-for="post in posts"
  v-bind:key="post.id"
  v-bind:title="post.title"
  v-bind:content="post.content"
  v-bind:publishedAt="post.publishedAt"
  v-bind:comments="post.comments"
></blog-post>

위의 예제와 같이 컴포넌트가 커지고, 많은 데이터를 컴포넌트로 넘겨주어야 할 때 모든 데이터를 props로 넘겨주는 것은 비효율적일 수 있습니다.

<div id="app">
  <blog-post
    v-for="post in posts"
    v-bind:key="post.id"
    v-bind:post="post"
  ></blog-post>
</div>
Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <div v-html="post.content"></div>
    </div>
  `
})

new Vue({
  el: '#app',
  data: {
    posts: [
      { id: 1, title: 'My journey with Vue', content: '1st' },
      { id: 2, title: 'Blogging with Vue', content: '2nd' },
      { id: 3, title: 'Why Vue is so fun', content: '3th' }
    ]
  }
});

컴포넌트는 단일 루트 엘리먼트여야 합니다.

여러 데이터를 자식 컴포넌트로 전달해야 할 때 전달되어야 할 모든 값을 props로 전달 하는 것이 아니라, 위의 예제와 같이 하나의 객체로 묶어 props로 넘기는 것이 효율적입니다. 나중에 추가로 값을 전달 해야 할 경우, props를 추가하는 것이 아니라 객체에 값을 추가하여 넘겨 주는 것으로 코드 변경을 최소화 할 수 있습니다.

위의 예제는 CodePen에서 확인 할 수 있습니다.

6. 부모 컴포넌트로 데이터 전달

자식 컴포넌트에서 부모 컴포넌트로 데이터를 전달해야 할 때가 있습니다. 그 때 사용되는 것이 $emit 입니다.

예를 들어, <blog-post> 컴포넌트에서, 텍스트 확대 기능을 추가하기 위해 부모 컴포넌트로 텍스트 확대를 했다는 이벤트를 부모 컴포넌트로 전달해야 한다고 가정해 봅니다.

<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"
    ></blog-post>
  </div>
</div>
new Vue({
  el: '#app',
  data: {
    posts: [
      { id: 1, title: 'My journey with Vue', content: '1st' },
      { id: 2, title: 'Blogging with Vue', content: '2nd' },
      { id: 3, title: 'Why Vue is so fun', content: '3th' }
    ],
    postFontSize: 1
  }
});

부모 컴포넌트는 위의 예제와 같이 작성 될 수 있습니다. postFontSize로 텍스트의 크기가 정의 됩니다.

Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <button>Enlarge text</button>
      <div v-html="post.content"></div>
    </div>
  `
})

자식 컴포넌트는 위의 예제와 같이 작성하면, <button>이 눌려젔을 때 부모 컴포넌트의 postFontSize의 값이 변경 되어야 합니다.

<button @click="$emit('enlarge-text')">Enlarge text</button>

postFontSize를 변경해야 한다는 것을 부모 컴포넌트에 알려 주기 위해 위의 코드와 같이 $emit을 사용합니다.

<blog-post
  v-for="post in posts"
  v-bind:key="post.id"
  v-bind:post="post"
  @enlarge-text="postFontSize += 0.1"
></blog-post>

부모 컴포넌트에서는 자식 컴포넌트에서 전달한 이벤트를 핸들링 하기 위해 위에 코드와 같이 @enlarge-text 라는 이벤트 핸들러를 정의해야 합니다.

위의 예제는 CodePen에서 확인 할 수 있습니다.

1) $emit으로 부모 컴포넌트에 데이터 전달하기

$emit 메소드의 두번째 파라미터를 사용하면, 부모 컴포넌트로 데이터 전달이 가능합니다.

<button @click="$emit('enlarge-text', 0.1)">Enlarge text</button>

위의 예제는 부모 컴포넌트로 enlarge-text 라는 이벤트를 통해 0.1의 값을 전달하는 예제입니다.

<blog-post
  v-for="post in posts"
  v-bind:key="post.id"
  v-bind:post="post"
  @enlarge-text="postFontSize += $event"
></blog-post>

부모 컴포넌트에서는 자식 컴포넌트로 부터 전달 받은 값을 위의 예제와 같이 $event를 통해 사용 가능합니다.

<div id="app">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
      v-for="post in posts"
      v-bind:key="post.id"
      v-bind:post="post"
      @enlarge-text="onEnlargeText"
    ></blog-post>
  </div>
</div>
Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <button @click="$emit('enlarge-text', 0.1)">Enlarge text</button>
      <div v-html="post.content"></div>
    </div>
  `
})

new Vue({
  el: '#app',
  data: {
    posts: [
      { id: 1, title: 'My journey with Vue', content: '1st' },
      { id: 2, title: 'Blogging with Vue', content: '2nd' },
      { id: 3, title: 'Why Vue is so fun', content: '3th' }
    ],
    postFontSize: 1
  },
  methods: {
    onEnlargeText: function (enlargeAmount) {
      this.postFontSize += enlargeAmount
    }
  }
});

일반 네이티브 이벤트와 동일하게 이벤트 핸들러를 사용할 수도 있습니다.

위의 예제는 CodePen에서 확인 할 수 있습니다.

2) 컴포넌트에서 v-model 사용하기

<input v-model="searchText">

input 태그에서 v-model을 위의 코드와 같이 사용할 경우,

<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

위의 코드와 같은 동작을 하게 됩니다.

<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>
  • value attribute는 custom-inputvalue prop와 바인딩 됩니다.
  • input 이벤트는 custom-input에서 $emit으로 호출되는 이벤트입니다.

위와 같이 작성이 된 코드는,

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

위와 같이 다시 작성 될 수 있습니다. 위의 코드는 또 다시,

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

위의 코드와 같이 작성될 수 있습니다. 컴포넌트에서 v-model을 사용하는 방법은 [Vue.JS] 컴포넌트 (고급:Custrom Events) 에서 더 자세히 이야기 하도록 하겠습니다.

7. slot 사용하기

<alert-box>
  Something bad happened.
</alert-box>

컴포넌트를 사용할 때, HTML 엘리먼트와 같이 위의 코드처럼 컴포넌트에 콘덴츠를 전달해야 할 때가 있습니다. 그럴 때 사용해야 하는 것이 slot 입니다.

Vue.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})

alert-box 컴포넌트가 위의 코드와 같이 코딩되어 있다면,

slot 사용 예제 결과
slot 사용 예제 결과

<alert-box>Somthing bad happend.</alert-box>의 예제 결과는 위의 사진과 같습니다. 위의 예제 코드 결과는 CodePen에서 확인 할 수 있습니다. slot에 대한 좀 더 자세한 이야기는 [Vue.JS] 컴포넌트 (고급:slot) 에서 이야기 하도록 하겠습니다.

8. 동적 컴포넌트

동적으로 컴포넌트를 전환해야 할 때가 있습니다. 그럴 때 사용하기 유용한 것이 <component>is 입니다.

<!-- Component changes when currentTabComponent changes -->
<component v-bind:is="currentTabComponent"></component>

currentTabComponent의 리턴 값은 컴포넌트의 이름이나 컴포넌트 객체입니다. 위의 예제는 Fiddle에서 확인 할 수 있습니다. 동적 컴포넌트에 관한 좀 더 자세한 내용은 [Vue.JS] 컴포넌트 (고급:Dynamic & Async Components) 에서 이야기 하도록 하겠습니다.

9. DOM 템블릿 문법 경고 해결

<ul>, <ol>, <table>, <select> 등의 HTML 태그들의 자식 태그로는 <li>, <tr>, <option>등으로 항상 정해져 있습니다. 이러한 규칙을 어기게 되면 DOM은 경고를 뱉어내가 됩니다.

<table>
  <blog-post-row></blog-post-row>
</table>

예를 들어, 위의 코드와 같이 작성된 코드가 있다고 할 때, <table>의 자식 태그들은 <tr> 등.. 이 와야 하지만 그렇지 않기 때문에 DOM은 경고를 발생시킵니다. <blog-post-row><tr> 태그로 구성되어 있다고 해도 경고를 피해 갈 수 없습니다. DOM이 발생하는 경고를 피하기 위해 is를 사용할 수 있습니다.

<table>
  <tr is="blog-post-row"></tr>
</table>

위와 같이 is를 이용하거나,

  • template 속성을 사용한다 (e.g. template: '...')
  • .vue 파일로 컴포넌트를 생성한다.
  • <script type="text/x-template"> 를 사용한다.

위의 3가지 방법을 사용하면 DOM의 경고를 피할 수 있는 방법이 있습니다.

참고

'Vue.JS' 카테고리의 다른 글

[Vue.JS] 컴포넌트 (고급:Custrom Events)  (0) 2019.01.24
[Vue.JS] 컴포넌트 (고급:props)  (2) 2019.01.21
[Vue.JS] 폼 입력 바인딩  (0) 2018.12.15
[Vue.JS] 이벤트 핸들링  (0) 2018.12.06
[Vue.JS] 리스트 렌더링  (0) 2018.11.25
댓글
공지사항
최근에 올라온 글