(준)공식 문서/Vue.js

[ Vue.js 3 공식 문서 ] 2. Essentials - Components Basics (Component, Props, Emits )

Je-chan 2022. 2. 28. 14:05

[ Reference ] 

 

https://vuejs.org/guide/essentials/component-basics.html


  컴포넌트는 UI 를 독립적이고 재사용 가능한 방향으로 사용할 수 있도록 도와준다. App 을 구성할 때 컴포넌트를 중첩해서 사용하는 것은 일반적인 형태다 

 

 

  컴포넌트를 중첨ㅂ해서 사용하는 것은 일반 HTML 을 사용하는 것과 유사해 보인다. 하지만 Vue 는 독자적인 컴포넌트 모델을 사용하고 있으며 개발자가 작성한 컨텐츠나 로직을 각 컴포넌트에서 캡슐화할 수 있다. 


1. Defining a Component

  build 의 과정을 거친다면, 우리는 보통 ** .vue ** 확장자(이 확장자가 SFC 를 의미) 를 사용해서 Vue 컴포넌트를 만들 것이다. 

 

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

 

  build 과정을 거치지 않는다면 순수 자바스크립트 객체로 Vue의 특수 옵션을 사용해서 Vue 컴포넌트를 만들 수 있다. (* Vue 에 대한 기본적인 개념을 익히기 위해서 이렇게 사용할 수 있으나 실무에서는 이렇게 사용할 일은 없는 것 같다.)

 

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
  // or `template: '#my-template-element'`
}

 


2. Using a Component

  자식 컴포넌트를 사용하기 위해서 부모 컴포넌트에 import 하는 과정이 필요하다. 우리가 counter 컴포넌트를 ButtonCounter.vue 파일에 넣었을 경우, ButtonCounter.vur 컴포넌트가 렌더링 될 때 counter 컴포넌트는 기본값으로 노출된다. <script setup> 을 사용하는 경우, import 된 컴포넌트는 자동으로 template 에서 사용이 가능하다. 다른 방식으로 컴포넌트를 전역에서 등록할 수 있다. 그런 경우, 이 컴포넌트는 import 할 필요 없이 모든 컴포넌트에서 사용할 수 있게 된다.

 

<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
  <ButtonCounter />
  <ButtonCounter />
</template>

 

  위의 예시에서 ButtonCounter 컴포넌트는 여러번 재사용되었다. 이 ButtonCounter 컴포넌트에 count 라는 상태와 ** @click ** 가 존재하는 경우, 각 컴포넌트는 독립적으로 상태 값을 갖는다. 컴포넌트를 생성할 때마다 새로운 인스턴스를 생성하기 때문이다. (* ButtonCounter 가 초기에 count  상태를 0 으로 잡았다면 초기 렌더링 될 때 각 컴포넌트는 모두 count 값이 0 일 것이다. 클릭 이벤트로 count의 값이 1씩 추가된다고 하면, 첫 번째 ButtonCounter 컴포넌트를 클릭하면 첫 번째 컴포넌트가 갖는 count 의 값은 1 이 되지만 다른 두 번째, 세 번째 컴포넌트들은 count 값이 여전히 0일 것이다.)

 

  SFC 에서는 일반 HTML 태그와 혼돈을 주지 않기 위해 PascalCase 로 태그 이름을 작성하는 것이 권장된다.(* 권장이라 표현했으나 거의 의무적이긴 하다) 일반 HTML 태그는 대소문자 구분이 없지만, Vue SFC 에서는 구분 있고, 어떤 컴포넌트든 위 예시의 ButtonCounter 와 같이 /> 를 이용해 닫을 수 있다. (* 물론, <ButtonCounter></ButtonCounter> 로 작성해도 문제 될 것은 없다.) 

 

  만약 DOM 에 직접적으로 접근해서 만지고 싶다면 template 은 브라우저에서 진행되는 HTML 파싱의 영향 안에 들어가야 한다. 그런 경우에는 kebab-case 를 사용한다. 

 

<button-counter></button-counter>

 


4. Passing Props

  블로그를 하나 만든다고 생각해보자. 그러면 blog 게시글을 보여주기 위한 컴포넌트가 필요할 것이다. 우리가 원하는 디자인은 안의 내용은 다를지라도 똑같은 시각적 레이아웃은 가지고 가는 것이다. 만약 게시글의 제목, 내용 등 우리가 표시하길 원하는 데이터가 흘러 들어가지 않았다면  그 컴포넌트는 우리가 원하는 결과를 만들어줄 수 없을 것이다. 반대로 말하면, 그런 데이터가 흐른다면 우리가 원하는 바를 이룰 수 있는 것이다. 이때의 데이터가 바로 ** props ** 다.

 

  ** props ** 는 개발자가 컴포넌트에 등록하는 커스터마이징된 속성이다. 우리 블로그 게시글 컴포넌트에 title 이라는 데이터를 흘려보내고 싶다면 반드시 ** defineProps ** 라는 매크로를 이용해서 컴포넌트가 받아들이는 ** props ** 의 리스트에 title 을 추가해야 한다. 

 

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>

 

  ** defineProps ** 은 <script setup> 에서만 사용이 가능한 컴파일 타임의 매크로이며 import 를 명시적으로 작성할 필요가 없다. 선언된 props 는 자동으로 template 에 추가할 수 있다. ** defineProps ** 는 전달된 모든 props 를 포함하는 객체를 반환하는데 덕분에 필요한 경우 자바스크립트로 데이터에 접근을 할 수 있게 된다.

 

const props = defineProps(['title'])
console.log(props.title)

 

  <script setup> 을 사용하지 않는다면 props 옵션을 이용해서 선언할 수 있고, ** setup( ) ** 의 첫 번째 인자로 흘러들어 간다. (* setup(props, context) )  

 

export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

 

  한 번 props 가 등록되면 그 속성 값은 사용자가 원하는 것으로 넣을 수 있다.

 

<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

 

  위의 예시와 같이 하드 코딩 하는 방식은 App 을 개발할 때 사용하지 않는 방식이다. 실제 개발할 때에는 게시글의 내용을 부모 컴포넌트에서 배열에 담아 ** v-for ** 의 디렉티브를 이용해서 사용할  수 있다.

 

const posts = ref([
  { id: 1, title: 'My journey with Vue' },
  { id: 2, title: 'Blogging with Vue' },
  { id: 3, title: 'Why Vue is so fun' }
])
<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

 

  한 가지 주의할 점은 props 를 넘기는 과정에서 ** v-bind ** 의 단축키 ** : ** 를 이용해서 동적인 props 를 넘겨줬다는 점이다. 이런 방식은 렌더링이 된 시점에서 정확하게 어떤 컨텐츠가 들어갈지를 모를 때 사용한다. (* 기존의 게시글이 10개라고 해도 게시글이 하나가 추가가 되면 11개로 늘어날 것이다. 이 경우 title 이나 content 는 정적이지 않고 동적이기 때문에 props 를 넘겨주는 과정에서 동적으로 바인딩을 해줘야만 한다.) 

 


5.  Listening to Events

  <BlogPost> 를 컴포넌트를 개발할 때, 부모 컴포넌트로 올려 보내야 하는 것들이 있어야 할 수도 있다. 예를 들어 다른 페이지의 글씨 크기는 기본값으로 두면서 블로그 게시글들에 한해서만 크게 키우는 것 등이 있다. 이런 경우 개발을 한다고 하면 부모 컴포넌트에 pontFontSize 라는 ** ref ** 를 하나 생성해서 구현해낼 수 있다.

 

const posts = ref([
  /* ... */
])

const postFontSize = ref(1)

 

   우리가 만든 저 데이터를 실제 CSS 에 적용해서 사용하고 싶다면 다음과 같이 코드를 작성하면 된다.

 

<div :style="{ fontSize: postFontSize + 'em' }">
  <BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
   />
</div>

 

  <BlogPost> 컴포넌트에서 버튼을 생성해 그 버튼을 눌렀을 때 글씨 크기를 변경하고 싶다고 하자. 그러면 일단 로직은 신경 쓰지 말고 다음과 같이 UI 를 먼저 만든다. 

<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>Enlarge text</button>
  </div>
</template>

 

  UI 는 만들어졌고 이제 button 의 기능을 만들 차례다. 부모 컴포넌트는 자식 컴포넌트의 인스턴스에서 발생한 이벤트를 듣기 위해서 ** v-on **, 축약형으로 ** @ ** 을 사용할 수 있다.

 

<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

 

  (* 위 코드를 해석하면 자식 컴포넌트에서 enlarge-text 라는 이벤트를 발생시키면 postFontsize 의 값을 0.1 만큼 추가하겠다는 의미다)

 

  이제 자식 컴포넌트에서 ** $emit ** 을 해줄 차례다. ** $emit ** 은 이벤트의 이름을 부모 컴포넌트로 보내고 그 이벤트를 발생시키도록 유도한다.  

 

<!-- BlogPost.vue, omitting <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

 

  (* 코드의 의미를 해석하면 button 을 클릭했을 때, 부모 컴포넌트의 'enlarge-text' 이벤트를 실행한다는 의미다) 이렇게 코드를 작성하면 부모 컴포넌트에서 @enlarge-text="postFontSize += 0.1" 이벤트 핸들러가 실행해서 postFontSize 의 값이 0.1 만큼 커져 결국 BlogPost 의 전체 폰트 크기가 0.1 만큼 커지는 효과를 가져올 수 있다. 

 

  ** emit ** 의 사용방법은 따로 더 존재한다. ** defineProps ** 와 마찬가지로 ** defineEmits ** 를 통해 명시적으로 ** emit ** 할 이벤트를 적는다.

 

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

 

  이렇게 작성하면 컴포넌트가 ** emit ** 하는 모든 이벤트를 기록하고 선택적으로 유효성 검사(Event Validation) 을 진행할 수 있다. (* events validation 은 이후에 설명) Vue는 하위 컴포넌트 최상위 요소에 암시적으로 일반적인 이벤트 리스너를 적용하는 것을 방지할 수 있다. (* props 나 class 를 지정하면 보통 최상위 요소에 할당되는데 그것을 방지한다는 의미인 것 같다. 확실하지는 않음)

 

  ** defineProps ** 와 마찬가지로 ** defineEmits ** 는 <script setup> 에서만 사용할 수 있으며 import 가 따로 필요하지 않다. defineEmits 를 사용하면 자바스크립트 코드 내에서 이벤트를 올려 보낼 수 있는 ** emit ** 함수를 반환한다.

 

const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')

 

  만약 <script setup> 을 사용하지 않는다면 ** emits ** 옵션을 사용해서 ** emit ** 할 이벤트들을 선언할 수 있다. 이렇게 할 경우,  setup( ) 의 두 번째 인자인 context 의 한 속성으로 ** emit ** 함수가 들어간다. 

 

export default {
  emits: ['enlarge-text'],
  setup(props, context) {
    context.emit('enlarge-text')
  }
}

 


6. Content Distribution with Slots 

  HTML elements 와 마찬가지로 컴포넌트 안에 내용을 종종 넣는다. 

 

<AlertBox>
  Something bad happened.
</AlertBox>

 

  그런데 이렇게만 넣으면 에러가 발생한다. 이런 경우에는 ** <slot> ** 을 사용해서 원하는 결과를 얻어낼 수 있다.

 

<template>
  <div class="alert-box">
    <strong>Error!</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

 

  우리가 컴포넌트 태그 사이에 넣어 놓은 내용이 들어갔으면 하는 곳에 ** <slot /> ** 을 넣기만 하면 된다. Essential 이기 때문에 지금은 이 정도 선에서 간단하게만 소개를 하고, 다음에 더 자세한 이야기를 할 날이 올 것이다. 

 


7. Dynamic Components

  가끔씩, 탭 처럼 컴포넌트간에 전환이 필요할 때가 있다. 이 경우 ** is ** 라는 속성을 이용하면 된다.

 

<component :is="tabs[currentTab]"></component>

 

  이 예시에서 ** is ** 는 두 개의 정보 중 하나 이상을 전달할 수 있다.

 

1. 등록된 컴포넌트의 이름

2. 실제로 import 된 컴포넌트의 객체

 

  ** is ** 속성은 일반적인 HTML elements 를 생성할 때에도 사용될 수 있다. 

 

  <component :id= "..." > 를 내포하고 있는 여러 컴포넌트 사이에서 전환이 이뤄질 때 전환이 이뤄지지 않은 컴포넌트는 마운트 되지 않는다. ** <KeepAlive > ** 라는 컴포넌트를 사용하게 되면 활성화되지 않은 컴포넌트를 억지로 살아있는 상태가 될 수 있도록 한다. 


8. DOM Template Parsing Caveats

  DOM 에 직접적으로 Vue template 을 적어 넣고 싶다면 Vue 는 DOM 에서 template 문자열을 검색해야만 한다. 이 방식은 브라우저의 HTML 파싱 과정 때문에 주의해야 할 문제가 발생한다. (* 사실 결론은, DOM 에 직접적으로 조작하지 말고 SFC 를 사용하라는 얘기다. 그렇기에 바쁘다면 이번 카테고리는 읽지 않아도 문제 될 것은 없지만, 일반 HTML 과 다르게 작동하는 Vue 동작에 대해 좀 더 깊은 이해를 하고 싶다면 읽어보는 것도 나쁘지 않을 것 같다)

 

Case Insensitivity

  HTML 태그와 속성은 대소문자를 구별하지 않는다. 따라서 브라우저는 대문자로 작성된 것들을 모두 소문자로 해석한다. 이 말은 DOM template 을 사용할 때 PascalCasde 로 이름 지어진 컴포넌트나 camelCase 로 이름 지어진 props, ** v-on ** 이벤트의 이름을 모두 kebab-case 로 지어야 한다는 의미다. 

 

// camelCase in JavaScript
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost']
  template: `
    <h3>{{ postTitle }}</h3>
  `
}

 

  이 위에 있는 걸 아래와 같이 변경해야 한다.

 

<!-- kebab-case in HTML -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

 

self closing Tags 

  지금까지 컴포넌트를 사용한다고 하면 Self closing 의 형태로 작성해왔다.

 

<MyComponent />

 

  그 이유는 Vue template parser 가 태그의 타입이 어떤 것이든 /> 이 모든 태그의 마지막으로 받아들이기 때문이다. 

 

  하지만, DOM template 에서는 반드시 closing tag 를 명시해야만 한다. 

 

<my-component></my-component>

 

  HTML 은 몇 특정한 태그에서만 closing tag 를 생략할 수 있다. input, img 와 같은 태그들이 아니라면, closing tag 를 생략하는 경우, opening tag 가 아직 진행 중이라고 판단할 것이다. 

 

 

Element Placement Restrictions

 

  <ul>, <table>, <select> 와 같은 몇 HTML element 들은 그 안에 들어가야 할 element 가 제한적이다. 각각 <li>, <tr>, <option> 같은 태그가 들어가야 한다. 이건 그렇게 자식으로 넣을 태그들이 제한적인 태그들에 component를 자식 element 로 넣을 때 문제를 발생시킬 수 있다.   

 

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

 

  위와 같은 태그가 있다 했을 때, <blog-post-row> 태그는 잘목된 컨텐츠로 호이스팅 돼서 결과적으로 에러를 발생시킬 수 있다. 이런 경우에는 ** is ** 라는 속성으로 해결할 수 있다. 

 

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

 

  일반적인 HTML element 에서 사용할 경우, ** vue: ** 를 붙여야만 Vue 의 구성 요소로 해석할 수 있다.