# 컴포넌트 기초

# 기본 예제

뷰 컴포넌트 예제를 살펴봅시다:

// Vue 애플리케이션 생성
const app = Vue.createApp({})

// button-counter라는 새로운 전역 컴포넌트 정의
app.component('button-counter', {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button>
      You clicked me {{ count }} times.
    </button>`
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

INFO

여기에서는 간단한 예제를 보여드리지만, 일반적인 Vue 애플리케이션은 문자열 템플릿 대신 단일 파일 컴포넌트를 사용하고 있습니다. 단일 파일 컴포넌트에 대한 자세한 내용은 이 섹션에서 확인할 수 있습니다.

컴포넌트는 이름이 있는 재사용 가능한 인스턴스입니다(이 경우에서는 <button-counter>). 이 컴포넌트를 루트 인스턴스 안에서 커스텀 엘리먼트처럼 사용할 수 있습니다:

<div id="components-demo">
  <button-counter></button-counter>
</div>
1
2
3
app.mount('#components-demo')
1

See the Pen Component basics by Vue (@Vue) on CodePen.

컴포넌트는 재사용 가능한 인스턴스이므로, data, computed, watch, methods와 라이프사이클 훅과 같은 루트 인스턴스와 같은 옵션을 허용합니다. 유일한 예외는 el과 같은 몇 가지 루트만의 옵션(root-specific options)입니다.

# 컴포넌트 재사용

컴포넌트는 얼마든지 반복해서 재사용할수 있습니다:

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>
1
2
3
4
5

See the Pen Component basics: reusing components by Vue (@Vue) on CodePen.

버튼을 클릭할 때 각 버튼은 별도의 count를 유지합니다. 컴포넌트를 사용할 때 마다 새로운 인스턴스가 만들어지기 때문입니다.

# 컴포넌트 구성하기

애플리케이션이 중첩된 컴포넌트 트리로 구성되는 것은 일반적입니다.

Component Tree

예를 들어 헤더, 사이드바, 컨텐츠 영역에 대한 컴포넌트가 있을 수 있으며, 각각 일반적으로 네비게이션 링크, 블로그 글 등에 대한 다른 컴포넌트를 포함합니다.

템플릿에서 이러한 컴포넌트를 사용하려면, Vue가 해당 컴포넌트에 대해 알 수 있도록 등록해야 합니다. 컴포넌트 등록에는 전역(global) 등록지역(local) 등록의 2가지 유형이 있습니다. 지금까지 생성된 앱의 component 메소드를 사용해서 컴포넌트를 전역으로만 등록했습니다.

const app = Vue.createApp({})

app.component('my-component-name', {
  // ... 옵션들 ...
})
1
2
3
4
5

전역적으로 등록된 컴포넌트는 나중에 생성된 app 인스턴스의 템플릿에서 사용할 수 있으며, 해당 루트 인스턴스의 컴포넌트 트리의 모든 하위 컴포넌트에서도 사용할 수 있습니다.

지금은 등록에 대해 알아야할 모든 것이지만, 이 페이지를 읽고 내용에 익숙해지면 나중에 다시 돌아와 Component Registration에 대한 전체 가이드를 읽어보는 것이 좋습니다.

# Props를 이용해 자식 컴포넌트에게 데이터 전달하기

앞서서 블로그 포스팅을 위한 컴포넌트 생성에 대해 언급했습니다. 문제는 화면에 보여줄 특정 게시문의 제목, 내용과 같은 데이터를 전달할 수 없으면, 해당 컴포넌트가 유용하지 않다는 것입니다. 이렇게 컴포넌트에 데이터를 넘기기 위해 props가 등장합니다.

Props는 컴포넌트에 등록할 수 있는 커스텀 속성입니다. 값이 prop 속성에 전달되면, 그 값은 해당 컴포넌트 인스턴스의 속성이 됩니다. blog-post 컴포넌트에 제목을 전달하기 위해 props옵션을 사용하여 이 컴포넌트가 허용하는 prop목록에 제목을 포함할 수 있습니다.

const app = Vue.createApp({})

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

app.mount('#blog-post-demo')
1
2
3
4
5
6
7
8

컴포넌트는 원하는 만큼 props를 가질 수 있으며 기본적으로는 어떤 종류의 값이라도 넘길 수 있습니다. data 속성과 마찬가지로 컴포넌트 인스턴스에서 prop 값에 접근할 수 있음을 할 수 있습니다.

일단 prop이 등록되면 다음과 같이 커스텀 속성으로 데이터를 전달할 수 있습니다:

<div id="blog-post-demo" class="demo">
  <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>
1
2
3
4
5

See the Pen Component basics: passing props by Vue (@Vue) on CodePen.

그러나 일반적인 앱에서는 data에 posts 배열이 있을 수 있습니다.

const App = {
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}

const app = Vue.createApp(App)

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

app.mount('#blog-posts-demo')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

그런다음 각 컴포넌트를 렌더링하려고 합니다.

<div id="blog-posts-demo">
  <blog-post
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
  ></blog-post>
</div>
1
2
3
4
5
6
7

위에서 v-bind를 사용해서 props를 동적으로 전달할 수 있음을 알 수 있습니다. 이는 미리 렌더링할 정확한 컨텐츠를 모르는 경우 특히 유용합니다.

지금까지 prop에 대해 알아야할 모든 것입니다. 하지만 이 페이지를 읽고 내용이 익숙해지만 나중에 다시 돌아와 Props에 대한 전체 가이드를 읽어보는 것이 좋습니다.

# 하위 컴포넌트 이벤트 수신

<blog-post> 컴포넌트를 개발할 때, 일부 기능은 상위와 다시 통신해야 할 수 있습니다. 예를 들어, 블로그 게시물의 텍스트를 확대하고 나머지 페이지는 기본 크기로 유지하는 접근성 기능을 포함할 수 있습니다.

상위 컴포넌트의 data 속성에 postFontSize를 추가하여 이 기능을 지원할 수 있습니다:

const App = {
  data() {
    return {
      posts: [
        /* ... */
      ],
      postFontSize: 1
    }
  }
}
1
2
3
4
5
6
7
8
9
10

이 속성은 모든 블로그 포스트의 폰트 크기를 제어하기 위해 템플릿에서 사용할 수 있습니다:

<div id="blog-posts-events-demo">
  <div v-bind:style="{ fontSize: postFontSize + 'em' }">
    <blog-post v-for="post in posts" :key="post.id" :title="title"></blog-post>
  </div>
</div>
1
2
3
4
5

이제 모든 포스트의 내용의 글자를 키우는 버튼을 추가해봅시다:

app.component('blog-post', {
  props: ['title'],
  template: `
    <div class="blog-post">
      <h4>{{ title }}</h4>
      <button>
        Enlarge text
      </button>
    </div>
  `
})
1
2
3
4
5
6
7
8
9
10
11

문제는 이 버튼이 아무런 일울 하지 못한다는 거죠:

<button>
  Enlarge text
</button>
1
2
3

버튼을 클릭할 때 부모 컴포넌트와 통신하여 모든 글자의 크기를 키워야 합니다. 다행히 컴포넌트 인스턴스는 이 문제를 해결하기위한 커스텀 이벤트 시스템을 제공합니다. 부모 컴포넌트는 기본 DOM 이벤트의 경우와 마찬가지로 v-on 또는 @을 사용하여 하위 컴포넌트 인스턴스의 모든 이벤트를 수신하도록 선택할 수 있습니다.

<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>
1

이렇게 하면 자식 컴포넌트는 내장된 $emit 메소드에 이벤트 이름을 넘겨서 이벤트를 발송할수 있습니다:

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

@enlarge-text="postFontSize += 0.1" 리스너 덕분에, 부모는 이벤트를 받아 postFontSize의 값을 업데이트합니다.

See the Pen Component basics: emitting events by Vue (@Vue) on CodePen.

컴포넌트의 emits옵션에서 생성된 이벤트를 나열할 수 있습니다.

app.component('blog-post', {
  props: ['title'],
  emits: ['enlarge-text']
})
1
2
3
4

이렇게하면 컴포넌트에서 생성된 모든 이벤트를 확인하고, 선택적으로 그들을 검증(validate)할 수 있습니다

# 이벤트와 함께 값 emit하기

이벤트와 함께 특정 값을 emit하는 것이 유용할 때도 있습니다. 예를 들어, <blog-post> 컴포넌트가 글자 크기를 얼마나 확대할지를 담당하도록 할 수 있습니다. 이 경우 $emit의 두번째 파라미터를 사용하여, 이 값을 제공할 수 있습니다.

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

그런 다음 부모의 이벤트를 수신할 때, $event를 사용하여 emit한 이벤트의 값에 접근할 수 있습니다:

<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>
1

또는 이벤트 핸들러가 메소드인 경우:

<blog-post ... @enlarge-text="onEnlargeText"></blog-post>
1

그런다음 값이 해당 메소드의 첫 번째 파라미터로 전달됩니다:

methods: {
  onEnlargeText(enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}
1
2
3
4
5

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

커스텀 이벤트를 사용하여, v-model과 함께 작동되는 커스텀 입력(input)을 만들 수도 있습니다. 다음을 기억하십시오:

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

위 코드는 다음 코드와 동일한 코드 입니다:

<input :value="searchText" @input="searchText = $event.target.value" />
1

컴포넌트에서 사용되는 경우 v-model 대신에 다음과 같이 수행할 수 있습니다:

<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>
1
2
3
4

WARNING

in-DOM 템플릿으로 작업하고 있기 때문에, 여기서는 kebab-case와 함께 model-value를 사용했습니다. DOM Template Parsing Caveats 섹션에서 kebab-case와 camelCase 속성에 대한 자세한 설명을 볼 수 있습니다.

v-model이 실제로 작동하려면 컴포넌트 내의 <input>이 반드시 다음 규칙을 따라야 합니다:

  • value라는 속성을 modelValue prop에 바인딩합니다.
  • input에서 새로운 값과 update:modelValue이벤트를 emit해야 합니다.

다음과 같이 되어야 합니다:

app.component('custom-input', {
  props: ['modelValue'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `
})
1
2
3
4
5
6
7
8
9

이제 v-model는 컴포넌트에서 완벽히 동작합니다.

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

커스텀 컴포넌트에서 v-model 기능을 만드는 또 다른 방법은 computed 속성의 기능을 사용하여 getter와 setter를 정의하는 것입니다

다음 예제에서는 computed 속성을 사용하여 custom-input 컴포넌트를 리팩토링합니다.

get 메소드는 modelValue 속성 또는 바인딩에 사용 중인 속성을 반환해야 합니다. set 메소드는 해당 속성에 해당하는 $emit를 호출해야 합니다.

app.component('custom-input', {
  props: ['modelValue'],
  template: `
    <input v-model="value">
  `,
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) { this.$emit('update:modelValue', value)
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

지금은 커스텀 엘리먼트 이벤트에 대해 알아야할 모든 것입니다. 하지만 이 페이지를 읽고 컨텐츠에 익숙해지면 나중에 다시 돌아와 Custom Events에 대한 전체 가이드를 읽어보는 것이 좋습니다.

# 슬롯(Slot)을 이용한 컨텐츠 제공

HTML 엘리먼트와 마찬가지로 다음과 같이 컨텐츠를 컴포넌트에 전달할 수 있으면 유용합니다.

<alert-box>
  Something bad happened.
</alert-box>
1
2
3

다음처럼 렌더링 될겁니다:

See the Pen Component basics: slots by Vue (@Vue) on CodePen.

다행히 이 작업은 Vue의 커스텀 <slot> 엘리먼트로 매우 간단하게 만들 수 있습니다.

app.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})
1
2
3
4
5
6
7
8

위에서 볼 수 있듯이 원하는 위치에 slot을 추가하기만 하면 됩니다. 끝났습니다!

지금은 slot에 대해 알아야 할 모든 것이지만, 이 페이지를 다 읽고 내용에 익숙해지면 다시 돌아와 Slots에 대한 전체 가이드를 읽어보는 것이 좋습니다.

# 동적 컴포넌트

때로는 탭 인터페이스에서와 같이 컴포넌트 간의 동적으로 전환하는 것이 유용합니다:

See the Pen Component basics: dynamic components by Vue (@Vue) on CodePen.

위는 Vue의 is라는 특별한 속성을 가진 <component> 엘리먼트에 의해 만들 수 있습니다.

<!-- currentTabComponent가 변하면 컴포넌트가 바뀝니다.  -->
<component :is="currentTabComponent"></component>
1
2

위 예제에서 currentTabComponent는 다음 중 하나를 포함할 수 있습니다:

  • 이미 등록된 컴포넌트의 이름
  • 컴포넌트의 옵션 객체

전체 코드를 실험하려면 이 샌드박스 (opens new window)를 참조하고, 등록된 컴포넌트명 대신에 컴포넌트의 옵션 객체에 바인딩하는 예제는 이 버전 (opens new window)을 참조하세요.

이 속성은 일반 HTML 엘리먼트와 함께 사용할 수 있지만, 컴포넌트로 취급되므로 모든 속성이 DOM 속성으로 바인딩됩니다. value와 같이 일부 속성이 예상대로 작동하려면 .prop 수식어를 사용하여 바인딩해야 합니다.

지금은 동적 컴포넌트에 대해 알아야할 전부입니다. 하지만 이 페이지를 읽고 내용이 익숙해지면 나중에 다시 돌아와 Dynamic & Async Components에 대한 전체 가이드를 읽어 보는 것이 좋습니다.

# DOM 템플릿 파싱 주의사항

<ul>, <ol>, <table><select>와 같은 일부 HTML 엘리먼트에는 내부에 표시할 수 있는 엘리먼트에 대한 제한 사항과 <li>, <tr>, <option>과 같은 일부 엘리먼트만 표시될 수 있습니다. 이러한 제한이 있는 엘리먼트가 있는 컴포넌트를 사용할 때 문제가 발생합니다. 예를 들면:

<table>
  <blog-post-row></blog-post-row>
</table>
1
2
3

커스텀 컴포넌트 <blog-post-row>가 잘못된 컨텐츠로 표시되어 최종 렌더링된 출력에서 오류가 발생합니다. 다행히 v-is 특수 디렉티브를 사용하여 문제를 해결할 수 있습니다:

<table>
  <tr v-is="'blog-post-row'"></tr>
</table>
1
2
3

WARNING

v-is 값은 자바스크립트 문자열 리터럴이어야 합니다:

<!-- 잘못됨. 아무것도 렌더링 되지 않음 -->
<tr v-is="blog-post-row"></tr>

<!-- 올바름-->
<tr v-is="'blog-post-row'"></tr>
1
2
3
4
5

또한, HTML 속성명은 대소문자를 구분하지 않으므로, 브라우저는 모든 대문자를 소문자로 해석합니다. 즉, DOM내 템플릿을 사용할 때 camelCase된 prop명과 이벤트 핸들러 파라미터는 kebab-case(하이픈으로 구분)된 해당 항목을 사용해야 합니다:

// 자바스크립트 내 camelCase

app.component('blog-post', {
  props: ['postTitle'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
})
1
2
3
4
5
6
7
8
<!-- HTML 내 kebab-case -->

<blog-post post-title="hello!"></blog-post>
1
2
3

다음 중 하나의 문자열 템플릿을 사용하는 경우 이러한 제한이 적용되지 않는다는 점에 유의해야 합니다:

  • 문자열 템플릿 (예: template: '...')
  • 싱글 파일(.vue) 컴포넌트
  • <script type="text/x-template"> 지금까지 DOM 템플릿 파싱 경고에 대해 알아야 할 모든 것입니다. 실제로 Vue의 Essentials의 끝입니다. 축하합니다! 아직 배울 것이 더 많지만 먼저 Vue를 직접 이용하고 재미있는 것을 구축하기 위해 휴식을 취하는 것이 좋습니다. 방금 소화한 지식에 익숙해지면 다시 돌아와서 Dynamic & Async Components에 대한 전체 가이드와 사이드 바의 컴포넌트 심층 섹션에 있는 다른 페이지를 읽어보는 것이 좋습니다.

Deployed on Netlify.
Last updated: 2021-01-13, 05:23:36 UTC