첫 번째 컴포넌트 디자인 패턴은 일반적인 컴포넌트 구조화 방식이다.
뷰 컴포넌트, 컴포넌트 통신 방식을 배우고 나면 자연스럽게 아래와 같이 컴포넌트를 구현하게 된다.
<template>
<app-header @refresh="refreshPage"></app-header>
<app-content :list="items" @fetch="fetchData"></app-content>
<app-footer :right="message"></app-footer>
</template>
<script>
import AppHeader from './AppHeader.vue';
import AppContent from './AppContent.vue';
import AppFooter from './AppFooter.vue';
export default {
components: {
AppHeader,
AppContent,
AppFooter
}
}
</script>
위와 같은 방식은 등록된 컴포넌트가 여러 곳에 쓰이지 않을 때 사용하기 좋은 방식이다.
실질적인 코드의 재사용성보다는 템플릿 코드의 가독성과 명시적인 코드의 모양새를 더 중점으로 두고 있다.
두 번째 컴포넌트 디자인 패턴의 핵심은 v-model 디렉티브이다.
v-model의 내부가 어떻게 동작하는지 이해하고 이를 응용하여 좀 더 결합력이 높은 컴포넌트를 만들어보자.
<template>
<input type="text" v-model="inputText">
</template>
<script>
export default {
data() {
return {
inputText: 'hi'
}
}
}
</script>
위 코드를 실행하면 인풋 박스의 초기 값으로 'hi'가 지정되어 있다.
그리고 인풋 박스의 내용을 바꾸면 inputText의 값도 같이 바뀐다.
해당 내용은 뷰 개발자 도구에서 확인할 수 있다.
v-model 디렉티브가 어떻게 뷰 인스턴스의 데이터와 정보를 주고 받았는지는 아래의 코드를 보자.
<template>
<ipnut type="text" :value="inputText" @input="inputText = $event.target.value">
</template>
<script>
export default {
data() {
return {
inputText: 'hi'
}
}
}
</script>
inputText
의 값을 :value
에 연결하고 인풋 박스의 입력을 모두 $event.target.value
로 inputText
에 넣는다.
위의 코드는 v-model 디렉티브와 동일하게 동작한다.
TIP
위와 같은 코드는 커스텀 디렉티브를 작성할 때 많이 사용된다.
앞에서 살펴본 v-model로 HTML 인풋 체크 박스를 컴포넌트화 할 수 있다.
먼저 상위 컴포넌트인 App.vue이다.
<!-- App.vue -->
<template>
<check-box v-model="checked"></check-box>
</template>
<script>
import CheckBox from './Checkbox.vue';
export default {
component: {
CheckBox
},
data() {
return {
checked: false,
}
}
}
</script>
<!-- CheckBox.vue -->
<template>
<ipnut type="checkbox" :value="value" @click="toggle">
</template>
<script>
exports default {
props: ['value'],
methods: {
toggle() {
this.$emit('input', !this.value);
}
}
}
</script>
이 코드를 실행하여 체크 박스를 클릭하면 checked
값이 정상적으로 true
에서 false
로 전환되는 것을 확인 할 수 있다.
세 번째로 살펴볼 컴포넌트 디자인 패턴은 슬롯을 이용한 컴포넌트 설계 방법이다.
슬롯은 하위 컴포넌트의 템플릿을 상위 컴포넌트에서 유연하게 확장할 수 잇는 기능이다.
슬롯은 탭, 모달(팝업), 버튼 등 흔히 사용되는 UI 컴포넌트를 모두 재사용 할 수 있게 도와준다.
<!-- BaseButton.vue -->
<template>
<button type="button" class="btn primary">
<slot></slot>
</button>
</template>
<!-- App.vue -->
<template>
<div>
<!-- 텍스트로 버튼 이름만 정의 -->
<base-button>Show Alert</base-button>
<!-- 아이콘과 텍스트로 버튼을 UI 확장 -->
<base-button>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
Download
</base-button>
</div>
</template>
<script>
import BaseButton from './BaseButton.vue';
export default {
components: {
BaseButton,
}
}
</script>
위 코드는 버튼의 최소 마크업을 갖는 BaseButton 컴포넌트를 생성한 후 슬롯을 이용하여 버튼의 내용을 확장될 수 있게 구조화한 코드이다.
BaseButton.vue에서 <slot>
태그를 넣어놨기 때문에 BaseButton 컴포넌트를 등록하여 사용할 때 상위 컴포넌트에서 텍스트를 넣어 버튼의 이름만 바꾸거나, 텍스트와 아잌노을 함께 넣어 버튼의 UI를 꾸밀 수 있다.
뷰의 하이 오더 컴포넌트는 리액트의 하이 오더 컴포넌트에서 기원된 것이다.
리액트의 하이 오더 컴포넌트 소개 페이지를 보면 아래와 같이 정확한 정의가 나와 있다.
A higher-order component (HOC) is an advanced technique in React for reusing component logic. 이 말을 정리해보면 다음과 같다.
하이 오더 컴포넌트는 컴포넌트의 로직(코드)을 재사용하기 위한 고급 기술이다.
여기서 컴포넌트의 로직을 재사용한다는 말이 무슨 의미냐면 인스턴스 옵션을 재사용한다는 뜻이다.
<!-- ProductList.vue -->
<template>
<section>
<ul>
<li v-for="product in products">
...
</li>
</ul>
</section>
</template>
<script>
import bus from './bus.js';
export default {
name: 'ProductList',
mounted() {
bus.$emit('off:loading');
},
// ...
}
</script>
<!-- UserList.vue -->
<template>
<div>
<div v-for="product in products">
...
</div>
</div>
</template>
<script>
import bus from './bus.js';
export default {
name: 'UserList',
mounted() {
bus.$emit('off:loading');
},
// ...
}
</script>
위 코드는 ProductList 라는 컴포넌트와 UserList 컴포넌트의 로직을 정의한 코드이다.
두 컴포넌트가 각각 상품과 사용자 정보를 서버에서 받아와 표시해주는 컴포넌트라고 가정했을 때, 공통적으로 들어간느 코드는 다음과 같다.
name: '컴포넌트 이름',
mounted () {
bus.$emit('off:loading');
},
name
은 컴포넌트의 이름을 정의해주는 속성이고, mounted()
에서 사용한 이벤트 버스는 서버에서 데이터를 다 받아왔을 때 스피너나 프로그레스 바와 같은 로딩 상태를 완료해주는 코드이다.
이 두 컴포넌트 이외에도 서버에서 데이터 목록을 받아와 표시해주는 컴포넌트가 있다면 또 비슷한 로직이 반복될 것이다.
이때 이 반복되는 코드를 줄여줄 수 있는 패턴이 바로 하이 오더 컴포넌트이다.
이 반복되는 코드를 줄이기 위해 하이 오더 컴포넌트를 구현해보겠다.
// CreateListComponent.js
import bus from './bus.js';
import ListComponent from './ListComponent.vue';
export default function createListComponent(componentName) {
return {
name: comonentName,
mounted() {
bus.$emit('off:loading');
},
render(h) {
return h(ListComponent);
}
}
}
위 코드는 CreateListComponent라는 하이 오더 컴포넌트를 구현한 코드이다.
하이 오더 컴포넌트를 적용할 컴포넌트들의 공통 코드들(mounted, name 등)을 미리 정의했다.
그럼 이제 이 하이 오더 컴포넌트를 어떻게 사용할까?
// router.js
import createListComponent from './createListComponent.js';
new VueRouter({
routes: [
{
path: 'products',
component: createListComponent('ProductList'),
},
{
path: 'users',
component: createListComponent('UserList'),
}
]
})
위와 같은 방식으로 하이 오더 컴포넌트를 임포트 하고, 각 컴포넌트의 이름만 정의를 해주면 컴포넌트의 기본 공용 로직인 mounted()
, name
를 가지고 컴포넌트가 생성된다.
따라서, 컴포넌트마다 불 필요하게 반복되는 코드를 정의하지 않아도 된다.