[Vue 빠른시작] 컴포넌트 기초 예제 추가(Options API ver)

이 파트도 공부하다보니 api 버전을 나눠야할 필요가 있을것 같아 따로 작성한다.

이후글 보기 :
[Vue 빠른시작] 컴포넌트 기초 예제 추가(Composition API ver)



컴포넌트를 사용하면 UI를 독립적이고 재사용 가능한 일부분으로 분할하고 각 부분을 개별적으로 다룰 수 있다. 따라서 앱이 중첩된 컴포넌트의 트리로 구성되는 것은 일반적이다.

이것은 기본 HTML 엘리먼트를 중첩하는 방법과 매우 유사하지만, Vue는 각 컴포넌트에 사용자 정의 컨텐츠와 논리를 캡슐화할 수 있는 자체 컴포넌트 모델을 구현한다. Vue는 기본 웹 컴포넌트와도 잘 작동한다.



1. Defining a Component

빌드 방식을 사용할 때 일반적으로 Single-File Component(SFC)라고 하는 .vue 확장자를 사용하는 전용 파일에 각 Vue 컴포넌트를 정의한다.

// Options API ver
<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">당신은 {{ count }} 번 클릭했습니다.</button>
</template>

빌드 방식을 사용하지 않을 때, Vue 컴포넌트는 Vue 관련 옵션을 포함하는 일반 JavaScript 객체로 정의할 수 있다.

export default {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      당신은 {{ count }} 번 클릭했습니다.
    </button>`
}

JavaScript 문자열로 정의한 템플릿은 Vue가 즉석에서 컴파일한다. 엘리먼트(보통 기본 <'template> 엘리먼트)를 가리키는 ID 셀렉터를 사용할 수도 있다. Vue는 해당 컨텐츠를 템플릿 소스로 사용한다.

위의 예는 단일 컴포넌트를 정의하고 이를 .js 파일의 내보내기 기본 값으로 내보낸다. 그러나 명명된 내보내기를 사용하여 한 파일에서 여러 개의 컴포넌트로 내보낼 수 있다.



2. Using a Component

자식 컴포넌트를 사용하려면 부모 컴포넌트에서 가져와야 한다. 파일 안에 ButtonCounter.vue라는 카운터 컴포넌트를 배치했다고 가정하면, 해당 컴포넌트 파일의 기본 내보내기가 노출된다.

// Options API ver
<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>아래에 자식 컴포넌트가 있습니다.</h1>
  <ButtonCounter />
</template>

가져온 컴포넌트를 템플릿에 노출하려면 components옵션을 사용하여 등록해야 한다. 그러면 컴포넌트는 등록된 키를 사용하여 태그로 사용할 수 있다.

컴포넌트를 전역으로 등록하면, 가져오기(import) 없이 지정된 앱의 모든 곳에서 컴포넌트를 사용할 수 있다.

또 컴포넌트는 원하는 만큼 재사용할 수 있다.

<h1>여기에 많은 자식 컴포넌트가 있습니다!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

실행화면

버튼을 클릭할 때 각 버튼은 독립적인 count를 유지한다. 컴포넌트를 사용할 때마다 해당 컴포넌트의 새 인스턴스가 생성되기 때문이다.

SFC에서는 네이티브 HTML 엘리먼트와 구별하기 위해 자식 컴포넌트에 PascalCase 태그 이름을 사용하는 것이 좋다. 기본 HTML 태그 이름은 대소문자를 구분하지 않지만, Vue의 SFC는 컴파일된 포멧으로 대소문자를 구분하여 태그 이름을 사용할 수 있다. 또한 /> 를 사용하여 태그를 닫을 수 있다.

템플릿을 DOM에서 직접 작성하는 경우(예: 기본 <template> 엘리먼트의 컨텐츠로), 템플릿은 브라우저의 기본 HTML 구문 분석 동작을 따른다. 이러한 경우 컴포넌트는 kebab-case 및 명시적 닫는 태그를 사용해야 한다.

<!-- 이 템플릿이 DOM에 작성된 경우 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>



3. Passing Props

props은 컴포넌트에 등록할 수 있는 사용자 정의 속성이다.

// Options API ver
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

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

props속성에 값이 전달되면, 해당 컴포넌트 인스턴스의 속성이 된다. 해당 속성의 값은 컴포넌트의 다른 속성과 마찬가지로 템플릿 내에서 그리고 컴포넌트의 this 컨텍스트에서 접근할 수 있다.

컴포넌트는 원하는 만큼 props를 가질 수 있으며, 기본적으로 모든 값을 모두 props에 전달할 수 있다.

props가 등록되면, 다음과 같이 데이터를 사용자 정의 속성으로 전달할 수 있다.

// BlogPost 컴포넌트 내 title props 속성에 문자열 데이터를 전달
<BlogPost title="Vue와 함께한 나의 여행" />
<BlogPost title="Vue로 블로깅하기" />
<BlogPost title="Vue가 재미있는 이유" />

그러나 일반적인 앱에서는 부모 컴포넌트에 다음과 같은 게시물 배열이 있을 수 있다.

export default {
  // ...
  data() {
    return {
      posts: [
        { id: 1, title: 'Vue와 함께한 나의 여행' },
        { id: 2, title: 'Vue로 블로깅하기' },
        { id: 3, title: 'Vue가 재미있는 이유' }
      ]
    }
  }
}

그런 다음 v-for를 사용하여 각각을 컴포넌트로 렌더링하려고 한다.

<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

v-bind가 동적 prop 값을 전달하는데 사용되었다. 이것은 미리 렌더링할 정확한 콘텐츠를 모를 때 특히 유용하다.



4. Listening to Events

<BlogPost> 컴포넌트를 개발할 때 일부 기능은 상위 항목과 다시 통신해야 할 수 있다. 부모 컴포넌트에서 pontFontSize 데이터 속성을 추가하여 이 기능을 지원할 수 있다.

data() {
  return {
    posts: [
      /* ... */
    ],
    postFontSize: 1
  }
}

템플릿에서 모든 블로그 게시물의 글꼴 크기를 제어하는 데 사용할 수 있다.

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

이제 <BlogPost> 컴포넌트의 템플릿에 버튼을 추가해 보자

<!-- BlogPost.vue의 <script> 생략 -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>텍스트 확대</button>
  </div>
</template>

컴포넌트는 커스텀 이벤트 시스템을 제공한다. 부모 컴포넌트는 네이티브 DOM 이벤트와 마찬가지로 v-on 또는 @를 사용하여 자식 컴포넌트 인스턴스의 모든 이벤트를 수신하도록 선택할 수 있다.

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

그런 다음 자식 컴포넌트는 빌트인 $emit 메서드를 호출하고 이벤트 이름을 전달하여 자체적으로 이벤트를 생성할 수 있다.

<!-- BlogPost.vue의 <script> 생략 -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">텍스트 확대</button>
  </div>
</template>

@enlarge-text="postFontSize += 0.1 리스너 덕분에 부모 컴포넌트는 이벤트를 수신하고 postFontSize 값을 업데이트한다.


$emit

현재 인스턴스에서 커스텀 이벤트를 트리거한다. 추가적인 인자는 리스너의 콜백 함수로 전달된다.

말이 어려운데 풀어서 설명하면 Vue에서 사용되는 이벤트 발송 메소드. 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하거나, 자식 컴포넌트에서 부모 컴포넌트로 이벤트를 전달할 때 주로 사용된다.

interface ComponentPublicInstance {
  $emit(event: string, ...args: any[]): void
}
export default {
  created() {
    // 커스텀 이벤트 'foo'만 트리거
    this.$emit('foo')
    // 추가 인자와 함께 트리거

this.$emit('bar', 1, 2, 3)
  }
}

emits 옵션을 사용하여 원하는 이벤트를 선언할 수 있다.

<!-- BlogPost.vue -->
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>

이것은 컴포넌트가 내보내는 모든 이벤트를 문서화하고 선택적으로 유효성 검사를 한다. 또한 Vue가 자식 컴포넌트의 루트 엘리먼트에 암시적으로 네이티브 리스너(브라우저의 이벤트)가 적용되는 것을 방지할 수 있다.

이렇게 함으로써 코드를 읽고 유지보수하기 쉬워지며, 컴포넌트 간의 통신이 명확해진다. 또한 emits를 사용하면 Vue가 자동으로 부여하는 네이티브 리스너를 방지할 수 있다.


emits

본문 말이 어려워서 이해가 안가서 풀어서 다시 설명함!😒

emits 옵션은 Vue 3에서 도입된 기능으로, 부모 컴포넌트에서 자식 컴포넌트로 이벤트를 전송할 때 사용되는 이벤트 이름을 명시적으로 선언한다. 이를 통해 컴포넌트 간의 커뮤니케이션을 더 명시적이고 견고하게 만들 수 있다.

여기서 명시적으로 선언한다는 것은 props나 emits나 배열로 선언되어서 프론트가 생소한 나같은 사람은 'porps[index] 이런 형식으로 참조해야하는거 아닌가?' 하는 생각을 했는데 Vue 문법상 그것이 아니라 '명시적인 이름' 즉 명명된 이름 자체로 사용을 한다고 한다. 명시적인 이름을 사용해 가독성을 높여 코드를 더 이해하기 쉽게 만드는 것이라고 한다.

그리고 Options API ver에서는 emits 선언 안해줘도 오류는 발생하지 않는다. (Options API는 Vue2기반) 위 예제에서 해당 구문 삭제해도 오류 안나는 것을 확인 할 수 있다. 그러나 Composition API ver으로 작성된 경우 오류 발생함.




5. Content Distribution with Slots

Vue의 사용자 정의 <slot> 엘리먼트를 사용하여 컴포넌트에 컨텐츠를 전달할 수 있다. 컨텐츠를 이동하려는 자리 표시자로 <slot>을 사용한다.

// AlertBox.vue
<template>
  <div class="alert-box">
    <strong>이것은 데모용 오류입니다.</strong>
    <br/>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  color: #666;
  border: 1px solid red;
  border-radius: 4px;
  padding: 20px;
  background-color: #f8f8f8;
}

strong {
    color: red;    
}
</style>
// Options API ver
// App.vue
<script>
import AlertBox from './AlertBox.vue'

export default {
  components: { AlertBox }
}
</script>

<template>
    <AlertBox>
      나쁜 일이 일어났습니다.
    </AlertBox>
</template>



6. Dynamic Components

탭 인터페이스와 같이 컴포넌트 간에 동적으로 전환하려고 할 때 <component> 엘리먼트의 is 속성을 사용할 수 있다.

// 탭 역할을 할 컴포넌트는 아래와 같은 양식을 가진다.
<template>
    <div class="tab">
        Posts component
    </div>
</template>
// Options API ver
<script>
import Home from './dynamicComponent/Home.vue'
import Posts from './dynamicComponent/Posts.vue'
import Archive from './dynamicComponent/Archive.vue'

export default {

    components: {
        Home,
        Posts,
        Archive,
        TextComponent: {
            template: '<p>텍스트 컨텐츠</p>'
        },
        ImageComponent: {
            template: '<img src="https://play.vuejs.org/logo.svg">'
        }
    },
    data() {
        return {
            currentTab: 'Home',
            tabs: [ 'Home', 'Posts', 'Archive' ],
            currentContent: 'TextComponent',
            contentTabs: [ 'TextComponent', 'ImageComponent' ]
        }
    }
}
</script>
\
<template>
    <div class="demo">
        <!-- 
            탭을 선택할 수 있는 버튼 표시
            현재 선택된 탭은 클래스 바인딩을 통해 활성화됨
         -->
        <button
            v-for="tab in tabs"
            :key="tab"
            :class="['tab-button', { active: currentTab === tab }]"
            @click="currentTab = tab"
        >
            {{ tab }}
        </button>
        <!-- 
            component 엘리먼트를 사용하여 동적으로 현재 탭에 해당하는 컴포넌트 표시
         -->
        <component :is="currentTab" class="tab"></component>
    </div>
    <div class="demo">
        <!-- HTML 엘리먼트를 만드는 예제 -->
        <button
            v-for="tab in contentTabs"
            :key="tab"
            :class="['tab-button', { active: currentContent === tab }]"
            @click="currentContent = tab"
        >
            {{ tab }}
        </button>
        <keppAlive>
            <component :is="currentContent" class="tab"></component>
        </keppAlive>
    </div>
</template>

<style>
.demo {
  font-family: sans-serif;
  border: 1px solid #eee;
  border-radius: 2px;
  padding: 20px 30px;
  margin-top: 1em;
  margin-bottom: 40px;
  user-select: none;
  overflow-x: auto;
}

.tab-button {
  padding: 6px 10px;
  border-top-left-radius: 3px;
  border-top-right-radius: 3px;
  border: 1px solid #ccc;
  cursor: pointer;
  background: #f0f0f0;
  margin-bottom: -1px;
  margin-right: -1px;
}
.tab-button:hover {
  background: #e0e0e0;
}
.tab-button.active {
  background: #e0e0e0;
}
.tab {
  border: 1px solid #ccc;
  padding: 10px;
}
</style>

실행화면

:is에 전달되는 값은 다음 중 하나를 포함할 수 있다

  • 등록된 컴포넌트의 이름 문자열
  • 실제 가져온 컴포넌트 객체

is 속성을 사용하여 일반 HTML 엘리먼트를 만들 수도 있다.

<component :is="...">를 사용하여 여러 컴포넌트 간에 전환할 때, 다른 컴포넌트로 전환되면 컴포넌트가 마운트 해제된다. 내장된 <KeepAlive> 컴포넌트를 사용하여 비활성 컴포넌트를 "활성" 상태로 유지하도록 강제할 수 있다.

KeepAlive는 Composition API ver에서 다시 다룬다.



7. in-DOM Template Parsing Caveats(주의사항)

Vue 템플릿을 DOM에서 직접 작성하는 경우, Vue는 DOM에서 템플릿 문자열을 검색해야 한다. 이것은 브라우저의 기본 HTML 파싱 동작으로 인해 몇 가지 주의 사항으로 이어진다.

TIP!

아래에 설명된 제한 사항은 템플릿을 DOM에서 직접 작성하는 경우에만 적용된다는 점에 유의해야 한다. 다음 소스의 문자열 템플릿을 사용하는 경우에는 적용되지 않는다.


✔️ 싱글 파일 컴포넌트

✔️ 인라인 템플릿 문자열(ex. template: '...')

✔️ <'script type="text/x-template">

7-1. 대소문자를 구분하지 않음

HTML 태그와 속성의 이름은 대소문자를 구분하지 않으므로 브라우저는 대문자를 소문자로 해석한다. 즉, DOM 내 템플릿을 사용할 때 PascalCase 컴포넌트 이름과 props의 camelCased 이름 또는 v-on 이벤트 이름은 모두 kebab-case 기반으로 사용해야 한다.

// JavaScript에서 camelCase
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
<!-- HTML에서 kebab-case -->
<blog-post post-title="안녕!" @update-post="onUpdatePost"></blog-post>

7-2. 셀프 태그 닫기

DOM 템플릿에서는 항상 명시적인 닫는 태그를 포함해야 한다.

<my-component></my-component>

이는 HTML 사양에서 몇 가지 특정 엘리먼트가 닫는 태그를 생략할 수 있도록 허용하기 때문이다. 가장 일반적인 것은 input, img 등이 있다. 다른 모든 엘리먼트의 경우, 닫는 태그를 생략하면 기본 HTML 파서는 사용자가 여는 태그를 종료하지 않은 것으로 간주한다.

<my-component /> <!-- 우리는 여기서 태그를 닫으려 했습니다... -->
<span>hello</span>

위 코드는 아래와 같이 파싱된다.

<my-component>
  <span>안녕</span>
</my-component> <!-- 그러나 브라우저는 여기에서 닫을 것입니다. -->

7-3. 엘리먼트 배치 제한

ul, ol, table, select와 같은 일부 HTNL 엘리먼트에는 내부에 표시할 수 있는 엘리먼트에 대한 제한이 있다. 또한 li, tr 및 option과 같은 일부 엘리먼트는 특정 다른 엘리먼트 내부에만 사용할 수 있다.

이러한 제한이 있는 엘리먼트가 있는 컴포넌트를 사용할 때 문제가 발생한다.

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

위 코드에서 사용자 정의 컴포넌트 blog-post-row는 잘못된 컨텐츠로 hoisted되어 최종적으로 렌더링된 출력에서 에러가 발생한다. 특별한 is 속성을 해결 방법으로 사용할 수 있다.

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

TIP!

기본 HTML 엘리먼트에 사용되는 경우, is 값은 Vue 컴포넌트로 해석되기 위해 vue: 접두사를 사용해야 한다. 이는 기본 맞춤형 내장 엘리먼트와의 혼동을 피하기 위해 필요하다.