import { createApp } from 'vue';
import App from './App.vue';
import TheHeader from './components/TheHeader.vue';
import BaseBadge from './components/BaseBadge.vue';
import BadgeList from './components/BadgeList.vue';
import UserInfo from './components/UserInfo.vue';
const app = createApp(App);
app.component('the-header', TheHeader);
app.component('base-badge', BaseBadge);
app.component('badge-list', BadgeList);
app.component('user-info', UserInfo);
app.mount('#app');
지금까지 compontent 메서드를 사용해서 앱에 컴포넌트를 등록한다. 방법 자체에는 문제가 없으나, 가장 좋은 방법은 아니다.
component 메서드를 등록해서 앱에 컴포넌트를 등록하면, 전역(global) 컴포넌트로 등록된다. 전체 Vue앱에서, 어느 템플릿에서나 전역적으로 사용 가능하게 된다.
어디서든 사용할 수 있으니 아주 유용하나, 컴포넌트를 모두 전역적으로 등록하면 애플리케이션이 처음 로드될 때 Vue가 컴포넌트를 모두 로드해야 한다. 브라우저가 처음에 모든 컴포넌트에 대한 코드를 다운로드해야 한다. 작은 애플리케이션에는 문제가 안되더라도 큰 애플리케이션에선 문제가 될 수 있다.
뿐만 아니라, 지금 main.js에서도 목록이 길다. 수백개의 컴포넌트를 등록한다면 아주 복잡하며 길어지고, 각 컴포넌트가 사용되는 위치도 파악하기 어려워질 것이다.
한번만 사용되는 컴포넌트를 전역적으로 사용할 필요는 없다. 컴포넌트를 가지고 있다는 것 자체가 코드를 아웃소싱(위탁)하고 App.vue 파일을 간결하게 유지하기 위해서니까.
그럼 어떤 방법으로 컴포넌트를 등록하라는 얘기일까?
앱에서 component 메서드를 사용해 전역적으로 등록했더라도, 사용하려는 컴포넌트에서 지역적(local)으로 등록할 수 있다.
위의 App.vue에서 TheHeader, BaseBadge, BadgeList 컴포넌트들은 App.vue에서만 지역적으로 사용되는 컴포넌트 들이다. 따라서 이를 지역적(local) 등록으로 변경해보자.
import { createApp } from 'vue';
import App from './App.vue';
import BaseBadge from './components/BaseBadge.vue';
const app = createApp(App);
app.component('base-badge', BaseBadge);
app.mount('#app');
지역적으로 등록할 컴포넌트들을 지웠다.
<template>
<div>
<the-header></the-header>
<badge-list></badge-list>
<user-info
:full-name="activeUser.name"
:info-text="activeUser.description"
:role="activeUser.role"
></user-info>
</div>
</template>
<script>
import TheHeader from "@/components/TheHeader.vue";
import BadgeList from "@/components/BadgeList.vue";
import UserInfo from "@/components/UserInfo.vue";
export default {
components: {
// 'the-header': TheHeader
// TheHeader: TheHeader
TheHeader,
BadgeList,
UserInfo
},
data() {
return {
activeUser: {
name: 'Maximilian Schwarzmüller',
description: 'Site owner and admin',
role: 'admin',
},
};
},
};
</script>
<style>
html {
font-family: sans-serif;
}
body {
margin: 0;
}
</style>
import를 지역적으로 템플릿을 사용할 부모 컴포넌트에 선언하자.
export default {
components: {
// 'the-header': TheHeader
// TheHeader: TheHeader
TheHeader,
BadgeList,
UserInfo
},
components에 해당 컴포넌트에서 사용할 부속 컴포넌트들을 선언해준다. 컴포넌트를 선언할 수 있는 3가지 방법을 모두 적어두었다. TheHeader라고 작성하더라도, 자동으로 그 위의 TheHeader: TheHeader 코드로 변경된다.
+) 파스칼 케이스(TheHeader)로 작성하면 셀프 태그(
<TheHeader />
를 사용할 수 있다.
사이에 태그(the-header)를 쓰면 셀프태그를 사용할 수 없다.
여기에 등록된 컴포넌트는 자식컴포넌트를 포함해서 어떤 다른 곳도 아닌, 여기 App.vue에서만 사용할 수 있다.
스타일링을 추가한 위치와 관계없이 항상 앱 전체에 영향을 주는 전역 스타일로 취급된다. BridgeList.vue의 <style>
안에 h3
에 스타일링을 부여하면 해당 스타일이 오직 그 템플릿(BridgeList.vue)에만 영향을 미치는 것을 의미하지 않는다. 특정 vue에 style을 부여하더라도, 그 스타일은 모든 파일에 영향을 미치는 것이다.
때로는 이런 설정(앱 전체에 요구되는 스타일링)이 필요할 수도 있다. 그런경우 App.vue에서 애플리케이션에서 전역적으로 적용할 스타일을 정의하곤 한다.(본문body, 폰트font 같은 경우) App.vue가 전체 애플리케이션의 진입점이므로.
이에 비해 개별 컴포넌트에 특정 스타일을 지정하고 싶은경우, Vue에서 제공하는 기능이 있다.
<style scoped>
scoped를 추가하면 Vue는 여기에 정의된 스타일이 같은 파일 내 템플릿에만 적용되도록 한다. (형제, 자식 등 모든 컴포넌트에 영향을 미치지 않는다.)
개발자 도구를 통해 확인해보면 Vue가 내부에서 선택자를 변경한 것을 확인할 수 있다. header라는 스타일을 부여했을때, 각 컴포넌트가 가지는 고유한 속성을 해당 스타일에 더해준다.(여기선 data-v-9a9f)
<header data-v-9a9f>...</header>
스타일링 중에 무언가 안에 html을 감싸는 wrapper css같은 구조를 컴포넌트화 하고 싶을 수 있다. 이런경우 어떤식으로 구현할 수 있을까?
<template>
<div>
{{ content }}
</div>
</template>
<script>
export default {
props: ['content']
}
<style scoped>
div { .... }
이 BaseCard란 그림자를 지닌 사각 상자로 이후에 이 컴포넌트 안에 컨텐츠를 채우는 방식으로 사용할 것이다. <div>
태그에 스타일링을 부여하여 그림자진 사각 상자를 구현했다. 이제 부모 컴포넌트에서 이 BaseCard안에 html코드를 입력하면 된다
그런데.. 어떻게 구현하면 될까?
🤔 음.. 일단 부모가 이 상자안에 데이터를 전달할테니까.. props로 받으면 되나? content란 이름으로 데이터를 받고, 상자안에 데이터를 담는거니까, div안에 {{ content }}로 뿌리면 되지 않을까?
이건 작동하지 않을 것이다. 이렇게 작성해서 카드를 사용하면 우리가 사용한 Vue기능이 포함된 HTML 콘텐츠를 전달할 수 없다.
한번 살펴보자
import BaseCard from './compontents/BaseCard.vue';
app.component('base-card', BaseCard);
여기저기서 사용할 예정이니, 일단 전역 컴포넌트로 등록해둔다.
<template>
<section>
<base-card>
<div>
<h3>{{ fullName }}</h3>
<base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
</div>
<p>{{ infoText }}</p>
</base-card>
</section>
</template>
<script>
import BaseCard from "@/components/BaseCard.vue";
export default {
components: {BaseCard},
props: ['fullName', 'infoText', 'role'],
};
</script>
이제 BaseCard를 사용해보자! BaseCard로 담고싶은 html을 감싸보았다. 이렇게 코드를 짜면, vue가 html들을 프로퍼티로 전달해서 BaseCard안의 content에 담아 출력해줄까? 그렇지 않다. 우린 이미 프로퍼티가 그런 방식으로 작동하는게 아니라는 사실을 알고 있다.
이렇게 저장하고 실행해보면 컨텐츠가 모두 사라져있다. vue가 컨텐츠 렌더링의 위치를 모르기 때문이다. 커스텀 컴포넌트(BaseCard.vue) 태그 사이에 추가했는데 이것으로 무엇을할지 어디에 출력할지 모른다.
이런 경우, slot을 사용할 수 있다.
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {}
</script>
프로퍼티는 컴포넌트가 필요로 하는 데이터에 사용되고, 슬롯은 컴포넌트에 필요한 템플릿 코드의 HTML 코드에 사용된다.
하나의 컴포넌트에 두개 이상의 슬롯이 있어야 하는 경우가 있을 수 있다. 이를테면, header, body 2개로 나눠서 html을 받아야 하는 경우이다. 그럴 경우 slot에 이름을 붙여 구분 할 수 있다.
<template>
<div>
<header>
<slot></slot> // 필요한 slot1
</header>
<slot></slot> // 필요한 slot2
</div>
</template>
하나의 템플릿에 2개의 slot이 있어, Vue입장에서는 제공된 html 컨텐츠가 어느 slot으로 가야하는지 구분할 수 없다. 그래서 하나 이상의 슬롯을 사용하는 경우 슬롯 요소에 대한 name 속성을 사용해서 슬롯에 이름을 추가할 수 있다.
<template>
<div>
<header>
<slot name="heaer"></slot> // 필요한 slot1
</header>
<slot></slot> // 필요한 slot2
</div>
</template>
<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>
<template v-slot:header>
<p>{{ infoText }}</p>
+) 어느 슬롯으로 가는지 분명히 하기 위해 예약어인 default를 사용해 표시해 줄 수도 있다.
<template v-slot:header>
html코드
</template>
<template v-slot:default>
html코드
</template>
이런 식으로 구현 가능하다.
<template>
<section>
<base-card>
<div>
<h3>{{ fullName }}</h3>
<base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
</div>
<p>{{ infoText }}</p>
</base-card>
</section>
</template>
<script>
import BaseCard from "@/components/BaseCard.vue";
export default {
components: {BaseCard},
props: ['fullName', 'infoText', 'role'],
};
</script>
<style>
section {
margin: 2rem auto;
max-width: 30rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
padding: 1rem;
}
section div {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
slot을 통해 마크업을 UserInfo.vue에서 BaseCard.vue로 보냈다고 생각해보자. 해당 마크업을 BaseCard.vue가 감싸고 있기 때문에 BaseCard의 style이 영향을 미치리라 기대할 수 있다.
그러나 슬롯으로 작업할 때, 그리고 콘텐츠를 다른 컴포넌트 슬롯으로 보낼 때 scoped 스타일, 데이터 가용성 프로퍼티 등에 관해 배운 것은 아무것도 바뀌지 않는다.
가령 template으로 baes-card로 감싸져있는 fullName 프로퍼티는 UserInfo.vue에서만 사용가능하다. Vue.js는 다른 컴포넌트로 콘텐츠를 보내기 전에 템플릿을 분석하고 컴파일하고 평가한다. 따라서 특정 컴포넌트에 정의된 마크업은, 해당 컴포넌트의 스타일링에 영향을 받는다. (콘텐츠를 보내는 컴포넌트의 마크업은 적용되지 않는다.)
<template>
<div>
<header>
<slot name="header">
<h2>The default</h2>
</slot>
</header>
<slot></slot>
</div>
</template>
만약 BaseCard.vue를 사용하는 컴포넌트에서, header라는 이름으로 콘텐츠를 제공하지 않으면, 해당 슬롯에는 기본값인 <h2>The default</h2>
가 제공된다.
특정 슬롯을 사용할지 사용하지 않을지 불확실한 경우가 있을 수 있다. 만약 BaseCard.vue를 사용하지만, header라는 이름의 slot을 사용하지 않는다면(기본값도 사용하지 않고), 불필요하게 <header></header>
라는 태그만 남아있게 된다.
🤔 BaseCard는 이용하면서도 해당 slot의 필요 여부를 알 수 없는 상황에, slot을 사용하지 않는다면 해당 태그가 아예 존재하지 않도록 할수는 없을까요?
그럴때, $slot*을 활용 할 수 있다!
console.log(this.$slot.header);
는 undefined가 된다.이 같은 $slot의 기능을 통해서 특정 슬롯에 대한 데이터를 수신하는지 확인하고, 데이터가 수신되지 않는다면 해당 요소를 랜더링하지 않을 수 있다.
<template>
<div>
<header v-if="$slot.header">
<slot name="header">
<h2>The default</h2>
</slot>
</header>
<slot></slot>
</div>
</template>
#
v-slot은 #
로 생략 가능하다.
<template>
<section>
<base-card>
<template #header>
<h3>{{ fullName }}</h3>
<base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
</template>
<p>{{ infoText }}</p>
</base-card>
</section>
</template>
자주 사용되는 기능은 아니다. 그러나 이 기능 없이 구현할 수 없는 특정 기능이 있을 수 있어 알아두는것이 좋다.
<template>
<ul>
<li v-for="goal in goals" :key="goal">
<slot :item="goal" another-prop="..."></slot>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
goals: ['Finish the course', 'Learn Vue'],
}
}
}
</script>
<template>
<div>
<the-header></the-header>
<badge-list></badge-list>
<user-info
:full-name="activeUser.name"
:info-text="activeUser.description"
:role="activeUser.role"
></user-info>
<course-goals>
<template #default="slotProps">
<h2> {{ slotProps.item }} </h2>
<p> {{ slotProps['another-prop'] }}}</p>
</template>
</course-goals>
</div>
</template>
<script>
import TheHeader from "@/components/TheHeader.vue";
import BadgeList from "@/components/BadgeList.vue";
import UserInfo from "@/components/UserInfo.vue";
import CourseGoals from "@/components/CourseGoals.vue";
export default {
components: {
TheHeader,
BadgeList,
UserInfo,
CourseGoals
},
data() {
return {
activeUser: {
name: 'Maximilian Schwarzmüller',
description: 'Site owner and admin',
role: 'admin',
},
};
},
};
</script>
<style>
html {
font-family: sans-serif;
}
body {
margin: 0;
}
</style>
슬롯을 정의한 컴포넌트 내부에서 슬롯에 대한 마크업을 전달한 컴포넌트에 데이터를 전달할 수 있게 한다. 이를 위해 데이터를 가진 컴포넌트에서 (슬롯을 정의한 컴포넌트에서) slot에 프로퍼티를 추가한다.(:item="goal"
)
slot 요소에 설정하는 프로퍼티는 슬롯에 대한 데이터를 전달하는 곳에서 접근할 수 있게 된다.({{slotProps.item}}
)
slotProps의 값은 언제나 객체이다. slot에서 정의한 모든 프로퍼티가 병합된 객체. 여러 값을 사용할 수 있다.
<template #default="slotProps">
<h2> {{ slotProps.item }} </h2>
<p> {{ slotProps['another-prop'] }}}</p>
</template>
<course-goals #default="slotProps">
<h2> {{ slotProps.item }} </h2>
<p> {{ slotProps['another-prop'] }}}</p>
</course-goals>
default 슬롯과 같이 하나의 슬롯만을 대상으로 할 때 template를 삭제할 수 있다. 대신 #default="slotProps"
태크를 컴포넌트 태그에 직접 삽입한다.
여러 탭을 가진 컴포넌트를 구축한다고 생각해보자. 다른 버튼을 누르면, 다른 컴포넌트를 가지는 동적 기능을 생성하려고 한다. 동시에 보여주지 않고, 선택적으로 특정 컴포넌트를 보여주는 것이다.
<template>
<h2>Manage Goals</h2>
</template>
<template>
<h2>Active Goals</h2>
</template>
<template>
<div>
<the-header></the-header>
<button @click="setSelectedComponent('active-goals')">ActiveGoals</button>
<button @click="setSelectedComponent('manage-goals')">ManageGoals</button>
<active-goals v-if="setSelectedComponent === 'active-goals'"></active-goals>
<manage-goals v-if="setSelectedComponent === 'manage-goals'"></manage-goals>
</div>
</template>
<script>
import TheHeader from "@/components/TheHeader.vue";
import ManageGoals from "@/components/ManageGoals.vue";
import ActiveGoals from "@/components/ActiveGoals.vue";
export default {
components: {
selectedComponent: 'active-goals',
TheHeader,
ManageGoals,
ActiveGoals
},
data() {
return {
activeUser: {
name: 'Maximilian Schwarzmüller',
description: 'Site owner and admin',
role: 'admin',
},
};
},
methods: {
setSelectedComponent(cmp) {
this.selectedComponent = cmp;
}
}
};
</script>
<style>
html {
font-family: sans-serif;
}
body {
margin: 0;
}
</style>
setSelectedComponent라는 특정 데이터를 만들어서 어떤 컴포넌트가 선택되었는지 정보를 담고 있는 방식으로 구현했다. v-if를 이용하여 이 setSelectedComponent의 값을 확인하고, 이를 통해 동적으로 컴포넌트를 변경한다.
그러나 이같은 방식은 그 컴포넌트의 수가 많아질수록 매우 번잡스러워 진다는 단점이 있다. (일일이 v-if를 체크하는 코드가 매우 늘어날 것이다.)
이같은 기능을 간단 명료하게 표현하기 위해 vue에서 제공하는 기능이 있다! 바로 component이다.
<div>
<the-header></the-header>
<button @click="setSelectedComponent('active-goals')">ActiveGoals</button>
<button @click="setSelectedComponent('manage-goals')">ManageGoals</button>
<!-- <active-goals v-if="setSelectedComponent === 'active-goals'"></active-goals>-->
<!-- <manage-goals v-if="setSelectedComponent === 'manage-goals'"></manage-goals>-->
<component :is="selectedComponent"></component>
</div>
</template>
혼자서는 작동하지 않는다. is라는 키 프로퍼티가 필요하다.
is
컴포넌트 요소에 우리가 정의한 컴포넌트 중 어느 컴포넌트를 보여줄 지 알려준다.
하드코딩 해서는 안 된다.(is="active-goals"
). 그렇게 사용하려면 그냥 템플릿을 추가하는것과 다를바 없다.
ManageGoals에 input box를 하나 두고, 데이터를 입력한다고 생각해보자.
🤔 ManageGoals의 input box에 값을 입력해두고 다른 탭(ActiveGoals)으로 이동하면 해당 input box의 값이 유지될까?
유지되지 않는다. 동적 컴포넌트는 컴포넌트를 바꿀때 이전 컴포넌트는 DOM에서 제거된다. 따라서 탭으로 컴포넌트를 바꾸면 입력 필드에 넣는 모든 값이 사라지게 된다.
이를 유지하기 위해 vue에서는 keep-alive라는 개념을 제공하고 있다!
<keep-alive>
<component :is="selectedComponent"></component>
</keep-alive>
이런식으로 keep-alive로 컴포넌트를 감싸면 해당 데이터들은 날아가지 않고 보관된다! 구축 여부는 자유롭게 선택 가능하나, 알아두면 편리한 기능.
갤러리에 쓸 수 있지 않을까?
이번에는 ManageGoals에서 버튼을 클릭하면 목표를 저장하고, 비어있다면 오류 메시지 띄우는 기능을 구녀해보자!
<template>
<div>
<h2>Manage Goals</h2>
<input type="text" ref="goal" />
<button @click="setGoal">Set Coal</button>
</div>
</template>
<script>
export default {
methods: {
setGoal() {
const enteredValue = this.$refs.goal.value;
if (enteredValue === '') {
alert('Input must not be empty!')
}
}
}
}
</script>
ref, v-model로 데이터를 가져올 수 있다. 여기선 ref를 사용했다. inputbox의 값을 가져와서 비어있다면 alert를 띄우는 방식으로 구현했다.
그러나 내장된 알림은 예쁘지 않다. modal을 직접 만드는 것이 더 예쁘니, 오버레이 다이얼로그를 직접 만들어보자!
+) 오버레이 다이얼로그: 웹 또는 앱 인터페이스에서 모달창이나 팝업창과 같은 대화형 컴포넌트를 나타내는 용어
<template>
<dialog open>
<slot></slot>
</dialog>
</template>
<style scoped>
dialog {
position: fixed;
top: 20vh;
left: 30%;
width: 40%;
background-color: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.26);
paddingL: 1rem;
}
</style>
slot을 이용해 다이얼로그의 내용을 유연하게 만들었다. ManageGoals.vue에서 입력한 값이 유효하지 않으면 해당 다이얼로그를 보이게 해보자.
<template>
<div>
<h2>Manage Goals</h2>
<input type="text" ref="goal" />
<button @click="setGoal">Set Coal</button>
<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>
</div>
</template>
<script>
import ErrorAlert from "@/components/ErrorAlert.vue";
export default {
components: {
ErrorAlert
},
data() {
return {
inputIsInvalid: false
}
},
methods: {
setGoal() {
const enteredValue = this.$refs.goal.value;
if (enteredValue === '') {
this.inputIsInvalid = true;
}
},
confirmError() {
this.inputIsInvalid = false;
}
}
}
</script>
여기서 slot의 강력함을 확인할 수 있다.
error-alert가 발생시키는 커스텀 이벤트에 대한 데이터를 전달할 필요가 없다. 대신 오류가 발생하는 컴포넌틍에서 모든 것을 처리할 수 잇고, 스타일링과 마크업은 다른 컴포넌트에서 수행한다. 이는 책임을 분산하는 좋은 방식이라 볼 수 있다.
<template>
<div>
<h2>Manage Goals</h2>
<input type="text" ref="goal" />
<button @click="setGoal">Set Coal</button>
<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>
</div>
</template>
html 측면에서 다이얼로그를 여기에 두는 것은 이상적이지 않다. 작동은 하나 접근성을 생각해보면 이상하다. (스타일링 문제가 발생할 수도 있다.)
사실 다이얼로그가 html 코드 한 가운데에서 등장하는 것이 아니라, HTML 트리 루트에 위치하는 것이 맞다. 즉, ManageGoals가 들어가있는 div 바깥에있는 것이 맞다.
텔레포트를 이용해서 이 부분을 향상시킬 수 있다!
<template>
<div>
<h2>Manage Goals</h2>
<input type="text" ref="goal" />
<button @click="setGoal">Set Coal</button>
<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>
</div>
</template>
이제 ManageGoals를 담고있는 div 아레에 해당 컴포넌트가 위치한다. 크게 중요하지 않은 작업이라 생각할 수 있으나, 간단하게 전체 HTML 구조에 도움이 될 수 있다.
<template>
<div>
<h2>
<input>
,,,
</div>
</template>
<template>
<div>
<h2>
<input>
</template>