Skip to content

섹션 9: 컴포넌트 자세히 알아보기

박수정 edited this page Nov 3, 2024 · 23 revisions

이전에 학습한 방법(전역 컴포넌트)

component를 메서드를 이용하여 앱에 컴포넌트 등록

// main.js
import TheHeader from './components/TheHeader.vue';

app.component('the-header', TheHeader);

image
image


방법적으로는 문제가 없지만 가장 좋은 방법은 아니다.
component를 메서드를 이용하여 앱에 컴포넌트를 등록하면
전역(global) 컴포넌트로 등록된다.(Vue 앱 어느 곳에서나, 즉 어느 템플릿에서나 사용할 수 있는 컴포넌트)

컴포넌트를 지역적(local)으로 등록하는 방법 components 프로퍼티

image

main.js의 import, component 메서드 제거 후
App.vue 파일의 script 섹션에 import/components 프로퍼티 추가

// App.js
<script>
import TheHeader from "./components/TheHeader.vue";

export default {
  components: {
    "the-header": TheHeader,
    // TheHeader: TheHeader,
    // TheHeader
  },
  data() {
    return {
    };
  },
};
</script>
// App.vue
<template>
  <the-header></the-header>
  <!-- <TheHeader /> -->
</template>

이제 App.vue의 템플릿 또는 컴포넌트에서 어느 컴포넌트가 사용될지를 Vue에 알릴 수 있다.
그리고 여기에 등록된 컴포넌트는 App.vue에서만 사용할 수 있다.

components는 객체를 취하는데 키-값 쌍이 필요하다.
키는 커스텀 HTML 요소이다.(태그)
값은 불러온(import) 컴포넌트 구성 객체인데 포인터로 가리키고 있다.

컴포넌트 스타일링

앱 전체에 영향을 미치는 스타일은 일반적으로 App.vue(전체 애플리케이션의 진입) 파일의 style 태그에서 정의된다.
하지만 개별 컴포넌트에 적용할 스타일이 있는 경우에는 scoped 속성을 이용해
정의된 스타일이 같은 파일 내에 있는 템플릿에만 적용되도록 한다.

vue가 scoped된 스타일을 구현하는 방법

image
vue가 요소의 선택자를 변경한다.
모든 컴포넌트의 템플릿은 고유한 속성을 받기 때문에 스타일링의 범위가 해당 컴포넌트의 텝플릿 마크업으로 지정된다.

슬롯(slots)

기본

자체 컴포넌트를 동적 콘텐츠. 즉, 다른 HTML 콘텐츠의 래퍼로 사용하는 경우
코드를 구성하고 여러 컴포넌트로 분할할 때 훨씬 더 많은 옵션을 제공

Vue 기능을 사용할 수도 있는 HTML 콘텐츠를 외부 컴포넌트로부터 수신할 수 있게 해준다.
기본적으로 프로퍼티와 같지만 프로퍼티는 컴포넌트가 필요로 하는 데이터에 사용되고
슬롯은 컴포넌트에 필요한 템플릿 코드의 HTML 코드에 사용된다.

<template>
  <div>
    <slot></slot>
  </div>
</template>

<style scoped>
div {
  margin: 2rem auto;
  max-width: 30rem;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
  padding: 1rem;
}
</style>

커스텀 컴포넌트에 여러 개의 슬롯

// BaseCard.vue
<template>
  <div>
    <header>
      <slot></slot>
    </header>
    <slot></slot>
  </div>
</template>

이렇게 슬롯이 두 개 있으면 Vue는 제공된 콘텐츠가 어디로 가야하는지 알 수 없게 된다.

// BaseCard.vue
<template>
  <div>
    <header>
      <slot name="header"></slot>
    </header>
    <slot></slot>
  </div>
</template>

그래서 하나 이상의 슬롯을 사용하는 경우
슬롯 요소에 대한 name 속성을 사용해서 슬롯에 이름을 추가할 수 있다.
모든 슬롯의 이름을 지정할 필요는 없지만 이름이 없는 슬롯을 하나 남겨두면 그것이 기본 슬롯이 된다.
이름이 없는 슬롯은 하나만 있어야 한다.

// UserInfo.vue
<template>
  <section>
    <base-card>
      <template v-slot:header>
        <h3>{{ fullName }}</h3>
        <base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
      </template>
      <p>{{ infoText }}</p>
    </base-card>
  </section>
</template>

기본 HTML 요소인 template 태그로 래핑한 다음
name 슬롯에 들어갈 콘텐츠가 포함된 template 태그에
특수 디렉티브인 v-slot 디렉티브를 추가한다.
Vue에 특정 콘텐츠가 어디로 가야 할지를 알려줄 수 있다.(v-slot에 인수 추가)
template 태그는 기본 HTML 태그로 화면에는 아무것도 렌더링 하지 않아 컨테이너로 사용

슬롯이 있는 템플릿 내부에 포함되지 않은 콘텐츠는 자동으로 기본 슬롯으로 이동한다.

<template>
  <section>
    <base-card>
      <template v-slot:header>
        <h3>{{ fullName }}</h3>
        <base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
      </template>
      <template v-slot:default>
        <p>{{ infoText }}</p>
      </template>
    </base-card>
  </section>
</template>

남아 있는 콘텐츠는 기본 슬롯으로 이동한다는 사실을 좀 더 분명하게 표시하기 위해
해당 콘텐츠 주의에 template를 추가한 다음 v-slot:default를 추가해준다.
(default는 예약어이다.)

콘텐츠를 슬롯에 할당하는 것을 잊은 것이 아니라 기본 슬롯으로 보낸다는 것을 분명히 한다.

슬롯에 기본 내용 제공하기(기본 폴백 콘텐츠)

// BaseCard.vue
<template>
  <div>
    <header>
      <slot name="header">
        <h2>기본</h2>
      </slot>
    </header>
    <slot></slot>
  </div>
</template>
// BadgeList.vue
<template>
  <section>
    <base-card>
      <template v-slot:header>
        <h2>Available Badges</h2>
      </template>
      <template v-slot:default>
        (내용 생략)
      </template>
    </base-card>
  </section>
</template>

image
BadgeList.vue에 <h2>Available Badges</h2>가 있으므로 기본 제목 제공되지 않음

// BadgeList.vue
<template>
  <section>
    <base-card>
      <template v-slot:default>
        (내용 생략)
      </template>
    </base-card>
  </section>
</template>

image
template 영역 없애니 기본 제목 노출됨

개선하고 싶은 상황

image
기본 폴백 콘텐츠를 렌더링하지 않으면 BadgeList.vue에는 제목이 표시되지 않는다.
문제 상황은 HTML 마크업을 살펴보면 DOM에 빈 header 요소가 있다.
header에 내용이 없음에도 <header></header>가 출력중

$slots를 사용해서 특정 슬롯에 대한 데이터를 수신하는지 확인하고
아니라면 해당 정보를 사용해서 특정 요소를 렌더링하지 않을 수 있을까?

제공된 콘텐츠에 엑세스를 통해 해결할 수 있다.

Vue에서 제공하는 특수 내장 프로퍼티 $slots
슬롯을 정의하는 컴포넌트에서 mounted 수명 주기를 추가한 후 console.log(this.$slots) 입력
$slots는 컴포넌트가 다른 슬롯에 대해 수신하는 슬롯 데이터에 관한 정보 보유

image
컴포넌트를 두 번 사용하고 있으니 console.log(this.$slots)도 두 번 실행 중
이 객체에서 우리가 제공하는 다른 슬롯에 액세스 할 수 있다.

<template>
  <div>
    <header>
      <slot name="header">
        <!-- <h2>기본</h2> -->
      </slot>
    </header>
    <slot></slot>
  </div>
</template>

<script>
export default {
  mounted() {
    console.log("header", this.$slots.header);
    console.log("default", this.$slots.default);
  },
};
</script>

image BadfgeList.vue 컴포넌트에서는 헤더 슬롯에 대한 콘텐츠를 제공하지 않았기 때문에
BaseCard.vue 컴포넌트에서 제공된 콘텐츠에 엑세스하려고 시도하면
BadgeList.vue에 사용될 BaseCard.vue 컴포넌트가 생성될 때 undefined가 출력

제공된 콘텐츠에 엑세스를 어떻게 활용하는가?

<template>
  <div>
    <header v-if="$slots.header">
      <slot name="header">
        <!-- <h2>기본</h2> -->
      </slot>
    </header>
    <slot></slot>
  </div>
</template>

header 요소에 v-if를 추가하고 $slots.header가 참인지 확인한다.
undefined가 나오면 거짓 → 따라서 이 header 요소는 렌더링되지 않는다.

image

v-slot 축약어 #(해시)

<template v-slot:header>
↓
<template #header>



범위가 지정된 슬롯

슬롯을 정의한 컴포넌트 내부에서 슬롯에 대한 마크업을 전달한 컴포넌트에 데이터를 전달할 수 있게 함.
슬롯을 정의한 컴포넌트에서 slot에 프로퍼티 추가

slotProps에 해당하는 값은 언제나 객체이다.
(슬롯을 정의한 컴포넌트에서 slot에서 정의한 모든 프로퍼티가 병합된 객체)

// CourseGoals.vue
<template>
  <ul>
    <li v-for="goal in goals" :key="goal">
      <slot :item="goal" another-prop="test"></slot>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      goals: ["수강 완료", "Vue 학습"],
    };
  },
};
</script>
// App.vue
<template>
  <div>
    <course-goals>
      <template #default="slotProps">
        <h2>{{ slotProps.item }}</h2>
        <p>{{ slotProps.anotherProp }}</p>
      </template>
    </course-goals>
  </div>
</template>

<script>
import CourseGoals from "./components/CourseGoals.vue";

export default {
  components: {
    CourseGoals,
  },
};
</script>

default 슬롯과 같이 하나의 슬롯만을 대상으로 할 때 template 삭제 가능
// App.vue
<template>
  <div>
    <course-goals #default="slotProps">
      <h2>{{ slotProps.item }}</h2>
      <p>{{ slotProps.anotherProp }}</p>
    </course-goals>
  </div>
</template>

<script>
import CourseGoals from "./components/CourseGoals.vue";

export default {
  components: {
    CourseGoals,
  },
};
</script>



동적 컴포넌트

버튼을 누르면 버튼에 해당하는 컴포넌트를 보여주고 싶을 때
v-if를 사용하여 구현할 수 있지만 이 방법은 번거롭다.

이 때 Vue에서 사용할 수 있는 동적 컴포넌트는 component 요소이다.
component 요소는 혼자서 동작하지 않고 is라는 키 프로퍼티가 필요하다.

<template>
  <div>
    <button @click="setSelectedComponenet('active-goals')">Active Goals</button>
    <button @click="setSelectedComponenet('manage-goals')">Manage Goals</button>
    <!--
    <active-goals v-if="selectedComponent === 'active-goals'"></active-goals>
    <manage-goals v-if="selectedComponent === 'manage-goals'"></manage-goals>
    -->
    <component :is="selectedComponent"></component>
  </div>
</template>

<script>
import ActiveGoals from "./components/ActiveGoals.vue";
import ManageGoals from "./components/ManageGoals.vue";

export default {
  components: {
    ActiveGoals,
    ManageGoals,
  },
  data() {
    return {
      selectedComponent: "active-goals",
    };
  },
  methods: {
    setSelectedComponenet(cmp) {
      this.selectedComponent = cmp;
    },
  },
};
</script>



동적 컴포넌트를 항상 활성 상태로 유지하기

문제 상황

Manage Goals에 input 추가, 내용을 입력하고 Active Goals를 다녀오면 Manage Goals의 input에 입력된 텍스트가 사라진 것을 확인.
컴포넌트를 바꿀 때 이전 컴포넌트는 DOM에서 제거되기 때문

image image image


해결(keep-alive)

keep-alive는 컴포넌트를 완전히 제거하지 않고 이들의 상태를 내부에서 캐시로 저장하도록 Vue에게 알려준다.

<keep-alive>
  <component :is="selectedComponent"></component>
</keep-alive>

image image image



텔레포트(Teleport)를 활용한 요소 이동

텔레포느틑 컴포넌트의 계틍을 유지하면서 DOM의 구조를 조작하고
DOM에 추가되는 부분을 제어하기 위해 사용되는 내장 컴포넌트이다.

teleport는 to라는 한 가지 속성을 필요로 한다.
값으로는 CSS 선택자를 넣으면 된다.

image

<teleport to="body">
  <error-alert v-if="inputIsInvalid">
    <h2>Input is invalid!</h2>
    <p>Please enter at least a few characters</p>
    <button @click="confirmError">Okay</button>
  </error-alert>
</teleport>

image



프래그먼트(Fragment) 활용하기

image
위 컴포넌트는 div를 제거한다고 가정할 경우
여러 개의 상위 레벨의 HTML 요소를 가진다.

Vue.js 버전2는 템플릿 하나에 루트 레벨 요소는 오직 하나만을 가져야했으나
Vue 3에서는 원하는 만큼 많은 상위 레벨의 요소를 가질 수 있다.
이 기능을 프래그먼트(Fragments)라고 부른다.

스타일 가이드

코드를 깨끗하고 이해하기 쉽도록 유지하기 위해 살펴본다.

공식 문서
기본 컴포넌트 이름
단일 인스턴스 컴포넌트의 경우(전체 앱에서 한 번만 사용/ex. TheHeader) 이름 앞에 The를 붙인다.