front/vue

[vue3 공식문서 번역]Essentials.13.Components Basics

juniKang 2022. 4. 28. 20:26

컴포넌트는 독립적이고 재사용가능한 조각으로 UI를 나눌 수 있고, 각각의 조각은 격리되어 있다. 중첩된 컴포넌트들의 트리로 조직화된 앱은 흔하다.

네이티브 HTML 엘리먼트를 내포하는 방식과 매우 유사하지만, 뷰는 각각의 컴포넌트에 로직과 커스텀 컨텐츠를 캡슐화하는 컴포넌트 모델을 구현했다. 뷰는 네이티브 웹 컴포넌트와도 잘 동작한다. 뷰 컴포넌트와 네이티브 웹 컴포넌트 사이의 관계에 대해 궁금하다면, 이걸 더 읽어봐라.

 

컴포넌트 정의

빌드 스텝을 사용할 때, 우리는 일반적으로 .vue 확장자를 사용한 각각의 뷰 컴포넌트를 가리키는 파일을 정의한다. SFC(Single-File Component)라고 알려진:

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

const count = ref(0)
</script>

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

build step을 사용하지 않을 때는, 뷰 컴포넌트는 Vue-specific options를 담고있는 순수한 자바스크립트 객체로 정의된다.

import { ref } from 'vue'

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

템플릿은 자바스크립트 문자열로 여기에 삽입되어 있으며, 뷰는 이것을 즉시 컴파일 한다. 엘리먼트( 보통 네이티브 <template> 엘리먼트)를 가르키는 ID selector를 사용할 수 있다. - Vue는 템플릿 소스로서 컨텐츠를 사용할 것이다.

 

위 에제는 싱글 컴포넌트를 정의하고,  .js 파일의 default export로서 export하지만, 같은 파일에서 여러개의 컴포넌트를 export하기 위한 named exports를 사용할 수도 있다.

 

컴포넌트 사용하기


이 가이드의 남은 부분에서 SFC 문법을 사용한다 - 컴포넌트에 대한 개념은 build step을 사용하는지에 관계없이 같다. 예제 섹션에서 양쪽 시나리오의 컴포넌트 사용법을 볼 수 있다.

자식 컴포넌트를 사용하기 위해, 부모 컴포넌트에서 import를 할 필요가 있다. ButtonCounter.vue라고 불리는 파일에 카운터 컴포넌트를 넣었다고 가정하고, 컴포넌트는 파일의 default export로서 노출될 것이다:

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

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

<script setup>과 함꼐, import된 컴포넌트들은 자동적으로 템플릿에서 사용가능해진다. 

 

컴포넌트를 전역으로 등록하는 것도 가능하며, import없이 받은 앱의 모든 컴포넌트에서 가능하게 만들 수 있다. 전역등록  vs. 지역 등록의 장단점은 Component Registration 섹션에서 다룬다.

 

컴포넌트들은 원하는 만큼 여러번 재사용될 수 있다.

<h1>Here are many child components!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

버튼을 클릭할 때, 하나하나는 각각의 분리된 count를 담고있다. 왜냐하면 컴포넌트를 각각 사용할 때마다, 새로운 인스턴스가 생성되기 때문이다. 

 

SFC에서, 네이티브 HTML 엘리먼트와 차이를 나타내기 위해 자식 컴포넌트에는 PascalCase를 사용하는게 추천된다. 비록 네이티브 HTML 태그 이름은 case에 민감하지 않지만, Vue SFC는 컴파일된 포맷이어서, 그 안에서 케이스에 민감하게 사용할 수 있다. 닫기 태그인 /> 도 사용할 수있다.

 

(네이티브 <template> 엘리먼트의 컨텐츠로서) DOM안에 직접적인 templates를 사용하면, 템플릿은 브라우저의 네이티브 HTML 파싱전략을 따를 것이다. 몇몇 상황에, 컴포넌트를 위한 명시적인 닫기 태그와 kebab-case를 사용할 필요가 있다.

<!-- 만약 이 템플릿이 DOM에 쓰였다면 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

Props 넘기기

만약 블로그를 만들고 있다면, 블로그 포스팅에 해당하는 컴포넌트가 필요할 수 있다. 우리는 모든 블로그 포스팅이 내용은 다르지만, 같은 시각적인 레이아웃을 공유하기를 바란다. 이런 컴포넌트들은 데이터를 넘겨주지 않는 한 사용할 수 없다. 우리가 보여주기는 원하는 특정한 포스팅의 제목과 내용같은 데이터들이 있다. 이곳이 props가 들어갈 곳이다.

 

Props는 컴포넌트에 등록할 수 있는 커스텀한 어트리뷰트다. 블로그 포스트 컴포넌트에 제목을 넘기기 위해, defineProps 매크로를 사용해서, 이 컴포넌트가 받을 수 있는 props의 리스트를 선언해야 한다.

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

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

defineProps는 <script setup>안에서만 사용가능한 compile-time 매크로이며, 명시적인 import가 필요하지 않다. 선언된 props는 템플릿으로 자동으로 노출된다. defineProps는 또한 컴포넌트로 패스되는 모든 props를 담고있는 개체를 리턴해서, 만약 필요하면 자바스크립트에서 접근할 수 있다:

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

 

만약 <script setup>을 사용하지 않으면, props는 props 옵션을 사용해서 선언할 수 있고, props 객체는 첫번째 아규먼트로 setup()에 패스될 수 있다:

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

컴포넌트는 원하는 만큼 많은 proprs를 가질 수 있으며, 기본적으로, 어떤 값이든 어떤 prop에 패스될 수 있다.

 

prop이 한 번 등록되면, 커스텀한 애트리뷰트로 데이터를 넘길 수 있다. 이렇게 :

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

하지만, 일반적인 앱에서, 부모 컴포넌트에서 포스트의 배열을 가질 수 있다:

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

그다음 v-for를 사용해서, 각각의 컴포넌트를 렌더링할 수 있다:

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

동적인 props를 넘기기 위해 v-bind를 사용할 수 있음을 기억하라. 앞으로 렌더링할 정확한 컨텐츠를 모를 때 특히 유용하다.

 

props에 대해 지금당장 알아야할 건 이게 다다. 하지만, 이 페이지를 다 읽고, 이 내용에 익숙해지면, Prop 섹션을 읽어봐라.

 

이벤트 듣기

<BlogPost> 컴포넌트를 개발하면서, 몇몇 기능은 부모에게 다시 전달해야할 필요가 있을 수 있다. 예를들면, 페이지의 나머지는 기본 사이즈로 유지하면서, 블로그 포스트의 텍스트를 키우는 접근 기능을 포함하기로 결정할 수 있다.

 

부모에서, postFontSize ref를 추가해서 기능을 지원할 수 있다:

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

const postFontSize = ref(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>Enlarge text</button>
  </div>
</tempalte>

이 버튼은 아직은 아무일도 못한다 - 모든 포스트의 글자크기를 키울수 있도록 부모와 통신하기위한 버튼을 클릭하는걸 원한다. 이걸 하기 위해, 컴포넌트 인스턴스는 커스텀 이벤트 시스템을 제공한다. 부모는 자식컴포넌트 인스턴스에 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')">Enlarge text</button>
  </div>
</template>

@enlarge-text="postFontSize += 0.1" 리스너에 감사하며, 부모 컴포넌트는 이벤트를 받고, postFontSize의 값을 업데이트할 수 있다.

 

defineEmits 매크로를 사용해서 선택적으로 내보낼 이벤트를 선언할 수 있다:

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

이렇게 하면 컴포넌트가 내보내는 모든 이벤트를 기록하고 선택적으로 유효성 검사한다. 또한 뷰는 자식 컴포넌트의 루트 엘리먼트에 네이티브 리스너로 암묵적으로 적용하는 것을 막을 수 있다.

 

definePorps와 비슷하게, defineEmits 또한 <script setup> 에서만 쓸 수 있으며 import할 필요가 없다. 자바스크립트 코드에서 이벤트를 내보내기 위해 쓰일 수 있는 emit 함수를 리턴한다. 

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

emit('enlarge-text')

<script setup>을 사용하지 않는다면, emits options을 사용해서 내보낼 이벤트를 선언할 수도 있다. setup context의 프로퍼티로서 emit 함수에 접근할 수 있다( 두번째 아규먼트로 setup()에 패스됨):

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

지금 커스텀 컴포넌트 이벤트에 대해 알아야할 전부다. 하지만 이 페이지를 다 읽고, 이 내용에 익숙하다고 느끼면, Custom Events에 대해 읽어보는걸 권장한다.

Slots와 컨텐츠 분배

HTML 엘리먼트와 같이, 컴포넌트로 컨텐츠를 넘기기위해 사용할 떄 유용하다. 이렇게:

<AlertBox>
  Something bad happened.
</AlertBox>

렌터할 때 이렇게 될거다

This is an Error for Demo Purposes
Something bad happened.

Vue의 커스텀 <slot> 엘리먼트를 사용해서 할 수 있다:

<template>
  <div class="alert-box">
    <strong>THis is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

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

위에서 보듯이, content가 가야할 곳이 어디인지 플레이스홀더(위치지정자)로서 <slot>을 사용할 수 있다.

 

이게 지금 slots에 대해 알아야할 전부지만, 다 읽고 Slots섹션을 읽어보라.

동적 컴포넌트

때때로, 컴포넌트들을 동적으로 변경해주는게 유용할 때도 있다. 탭 인터페이스와 같이:

예제

위 예제는 뷰의 <component> 엘리먼트와 스페셜 is 어트리뷰트로 인해 가능하다:

<!-- 현재 탭이 변경됐을 때 컴포넌트가 바뀜 -->
<component :is="tabs[currentTab]"></component>

위 예제에서, :is로 넘긴 값이 담을 수 있는건:

 - 등록된 컴포넌트의 name string, 또는

 - 실제 임포트된 컴포넌트 객체

 

또한 일반적인 HTML 엘리먼트를 만드는 데에도 is 어트리뷰트를 사용할 수 있다.

 

<component :is="...">과 여러개의 컴포넌트들을 바꿀 때, 컴포넌트는 다른걸로 바뀔 떄 언마운트 될 것이다. 내장된 <KeepAlive> 컴포넌트를 사용해서 alive 상태로 사용되지 않는 컴포넌트를 강제할 수 있다.

 

DOM 템플릿 파싱 주의사항(Caveats)

만약 DOM에 직접적으로 뷰 템플릿을 쓴다면, 뷰는 DOM으로부터 template string을 되찾아올 것이다. 이건 브라우저의 네이티브 HTML 파싱 전략으로 인해 몇가지 주의사항을 야기한다.


오직 돔에 템플릿을 직접적으로 작성했을 떄만 논의되는 아래 한계가 발생한다. 이것들은 아래 소스에서 string templates을 썻을때는 적용되지않는다.
- Single-File Components
- Inlined template strings(예: template :`...`)
- <script type="text/x-template">

 

Case 무감각

HTML 태그와 어트리뷰트 이름은 케이스에 무감각해서, 브라우저는 모든 대문자를 소문자로 해석한다. in-DOM 템플릿을 사용할 때, PascalCase 컴포넌트이름과 cameCased prop 이름이나 v-one 이벤트 이름이 모두 그들의 kebab-cased(hypen-delimited) 를 사용할 때와 동일하게 동작한다.

// 자바스크립트에서는 cameCase
const BlogPost = {
  props: ['postTitle'],
  emits: ['ipdatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
<!-- HTML에서는 kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

스스로 닫는 태그

전에 코드 샘플에서 컴포넌트의 스스로 닫는 코드를 사용한 적이 있다:

<MyComponent />

이건 뷰의 템플릿 문법해석이 타입에 상관없이 어떤 태그던지 닫기위한 지시어로 />를 간주하기 때문이다.

 

하지만 돔 템플릿에서는, 항상 명백하게 닫는 태그를 명시해주어야 한다:

<my-component></my-component>

그 이유는 HTML스펙은 오직 몇가지 특정한 엘리먼트만 closing tag없이 쓸 수 있도록 허락했기 때문이다. 가장 흔한예로 <input>이나 <img>같은.. 모든 다른 엘리먼트들은 닫는 태그를 빼면, 네이티브 HTML 파서는 열린태그를 종료하지 않았다고 생각할 것이다. 예를들면, 아래 snippet:

<my-component /> <!-- 우리는 여기서 태그를 닫을 작정이다 -->
<span>hello</span>

구문분석은 이렇게 된다:

<my-component>
  <span>hello</span>
</my-component> <!-- 하지만 브러우저는 여기서 닫을 것이다. -->

엘리먼트 배치 제한

<ul>, <ol>, <table>과 <select>같은 몇가지 HTML 엘리먼트들은 안에 가질수 있는 엘리먼트에 제한이있고, <li>,<tr>, <option>과 같은 몇가지 엘리먼트들을 특정 다른 엘리먼트의 안에서만 나타날수 있는 제한이 있다.

 

이런 제한은 엘리먼트와 컴포넌트를 사용할 떄에도 이슈를 발생시킨다. 예를들면:

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

커스텀 컴포넌트인 <blog-post-row>는 유효하지 않은 컨텐츠로 등록될 것이고, 최종 렌더링 결과에서 에러를 발생시킬 것이다. 해결 방벅으로 특수한 is 어트리뷰트를 사용할 수 있다.

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


네이티브 HTML 엘리먼트를 사용할 때, is의 값은 vue:를 접두어로 꼭 사용해야 한다. Vue 컴포넌트로서 해석되기 위해서.. 네이티브 커스터마이즈 빌트인 엘리먼트와 혼동을 피하기위해 필요하다.

이게 현재 돔 템플릿 구분문석(parsing) 주의사항(caveats)의 전부다- 실제로, 뷰의 근본의 끝이다. 추갛한다! 더 배울게 있지만, 첫째로, 우린 좀 쉬고ㅓ 뷰를 써보기를 추천한다 - 뭔가 재밌는걸 만들거나,. 아직 해보지 않았다면 에제도 좀 봐라.

 

너가 이 지식에 대해 익숙하고 소화가 되면, components in depth로 가서 공부해봐라.