현재까지 개발된 view 컴포넌트는 총 3가지이다.(NewsView.vue, AskView.vue, JobsView.vue)
중복된 코드가 많아서 이전 포스트에서 AskView.vue만 올렸으나 동일하게 3가지 vue가 만들어진것이다.
근데, 모두 리스트를 불러오는 기능도 동일하고, 스타일도 거의 비슷하다.
NewsView.vue
AskView.vue
JobsView.vue
Vue의 강력한 장점 중에 하나인 컴포넌트의 재사용이라고 했다.
오늘 그부분을 알아보자.
//ListItem.vue
<template>
<div>
<ul class="news-list">
<li v-for="item in items" :key="item.id" class="post">
<div class="points">
{{ item.points || 0 }}
</div>
<div>
<p class="news-title">
<template v-if="item.domain">
<a :href="item.url">{{ item.title }}</a>
</template>
<template v-else>
<router-link :to="`item/${item.id}`">{{ item.title }}</router-link>
</template>
</p>
<small class="link-text">
{{ item.time_ago }} by
<router-link
v-if="item.user"
:to="`/user/${item.user}`" class="link-text">
{{ item.user }}
</router-link>
<a :href="item.url" v-else>
{{ item.domain }}
</a>
</small>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
created() {
const name = this.$route.name;
if (name === 'news') {
this.$store.dispatch('FETCH_NEWS');
} else if (name === 'ask') {
this.$store.dispatch('FETCH_ASK');
} else if (name === 'jobs') {
this.$store.dispatch('FETCH_JOBS');
}
},
computed: {
items() {
const name = this.$route.name;
if (name === 'news') {
return this.$store.state.news;
} else if (name === 'ask') {
return this.$store.state.ask;
} else {
return this.$store.state.jobs;
}
}
}
}
</script>
상단의 점수 표기란, 제목란, 작성일자, 유저명 또는 도메인명 이렇게 4구역으로 나눌 수 있다.
view마다 다를 수 있는 요소는 제목 클릭시 링크, 유저명 또는 도메인명 구역이다.
먼저 created()훅을 통해 list를 가져오는데 router객체 내 name을 이용하여 각각 다른 데이터를 불러올 수 있고,
computed()또한 마찬가지로 다른 items를 가져올 수 있다.
가져온 items를 뿌리는데 제목 클릭시 링크, 유저명 또는 도메인명 구역이 다를 수 있으므로,
v-if, v-else 디렉티브를 이용하여 페이지별로 다른 태그를 보여줄 수 있도록 분기처리를 하면 된다.
이렇게 만든 ListItem.vue를 각 컴포넌트에서 import를 받아 사용하기만 하면 된다.
//NewsView.vue
<template>
<div>
<list-item />
</div>
</template>
<script>
import ListItem from '../components/ListItem.vue';
export default {
components: {
ListItem
}
};
</script>
이렇게 되면 회원 정보를 보여주는 UserView.vue 부분도 컴포넌트로 뽑아서 재사용할 수 있다.
먼저 User정보를 보여주는 UserProfile.vue 컴포넌트를 생성하자.
1. UserProfile.vue의 html양식은 ItemView.vue의 회원정보 화면 양식을 이용한다.
2. 상위컴포넌트에서 data를 호출하고 하위 컴포넌트에서 computed로 받아온다.
//UserProfile.vue
<template>
<div class="user-container">
<div>
<i class="fas fa-user"></i>
</div>
<div class="user-description">
<div>{{ userInfo.id }}</div>
<div class="time">
{{ userInfo.created }}
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
userInfo() {
return this.$store.state.user;
}
}
}
</script>
완성된 UserProfile.vue을 UserView.vue에 적용시키자.
적용하는 방식은 2가지로 나눌 수 있다.
- 상위컴포넌트에서 데이터 호출 후 하위 컴포넌트에서 computed로 store에 직접 접근.
- 상위 컴포넌트에서 데이터 호출 후 상위컴포넌트에서 computed로 store에 직접 접근 및 하위 컴포넌트로 props 전달.
//UserView.vue
<template>
<div>
<user-profile />
</div>
</template>
<script>
import UserProfile from '../components/UserProfile.vue';
export default {
components: {
UserProfile
},
created() {
const userName = this.$route.params.id;
this.$store.dispatch('FETCH_USER', userName);
}
}
</script>
//UserProfile.vue
<template>
<div class="user-container">
<div>
<i class="fas fa-user"></i>
</div>
<div class="user-description">
<div>{{ userInfo.id }}</div>
<div class="time">
{{ userInfo.created }}
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
userInfo() {
return this.$store.state.user;
}
}
}
</script>
//UserView.vue
<template>
<div>
<user-profile :info="userInfo" />
</div>
</template>
<script>
import UserProfile from '../components/UserProfile.vue';
export default {
components: {
UserProfile
},
computed: {
userInfo() {
return this.$store.state.user;
}
},
created() {
const userName = this.$route.params.id;
this.$store.dispatch('FETCH_USER', userName);
}
}
</script>
//UserProfile.vue
<template>
<div class="user-container">
<div>
<i class="fas fa-user"></i>
</div>
<div class="user-description">
<div>{{ info.id }}</div>
<div class="time">
{{ info.created }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
info: Object
}
}
</script>
1과 2의 차이는 아래 그림과 같다.
그림 출처 : Vue.js 완벽 가이드 - 실습과 리팩토링으로 배우는 실전 개념
그런데 문제가 있다.
UserProfile.vue 컴포넌트를 사용하려는 곳이 2곳이 있다.(ItemView.vue, UserView.vue)
하지만 두 컴포넌트에서 받아온 User데이터는 서로 다른 객체이다.
위에서 v-if, v-else로 나누기에는 매번 컴포넌트를 사용할 때마다 v-if, v-else를 써야할 것이다.
좀 더 효율적인 방법으로 slot이라는 개념이 있다.
간단히 설명하자면 컴포넌트에 slot영역을 만들면 상위 컴포넌트에서 하위 컴포넌트 영역에 slot으로 지정한 영역을 재정의 할 수 있다.
역시 코드를 보며 설명하는 것이 가장 직관적이므로 보자.
//UserProfile.vue
<template>
<div class="user-container">
<div>
<i class="fas fa-user"></i>
</div>
<div class="user-description">
<slot name="userName"></slot>
<div class="time">
<slot name="time"></slot>
<slot name="userKarma"></slot>
</div>
</div>
</div>
</template>
user api를 이용하여 직접 정보를 사용할 때가 있고, item을 불러와 그 안에 있는 유저 정보를 사용할 때가 있다.
공통적인 부분도 있으나, name과 karma 영역은 서로 다르다.
이 두 영역을 필요할 때만 쓸 수 있도록 slot으로 설정하고 상위 컴포넌트에서 직접 재정의 할 수 있도록 한다.
//UserView.vue
<template>
<div class="container">
<user-profile :info="userInfo">
<div slot="userName">{{ userInfo.id }}</div>
<span slot="time">{{ `Joined ${userInfo.created}` }}, </span>
<span slot="userKarma">{{ userInfo.karma }} karma</span>
</user-profile>
</div>
</template>
//ItemView.vue
<template>
<div>
<section class="header-container">
<user-profile :info="fetchedItem">
<router-link :to="`/user/${fetchedItem.user}`" slot="userName">{{ fetchedItem.user }}</router-link>
<span slot="time">{{`Posted ${fetchedItem.time_ago}`}}, </span>
<span slot="userKarma">{{ fetchedItem.points }} points</span>
</user-profile>
<h2>{{ fetchedItem.title }}</h2>
</section>
<section>
<div v-html="fetchedItem.content" class="content"></div>
</section>
</div>
</template>
UserView 컴포넌트 영역만 보면 이름, 시간, 점수 3가지 영역으로 구분된다.
근데 불러온 객체의 키와 값이 달라 v-if, v-else로 모두 구분한다면 상당히 비효율적이다.
이렇게 slot을 사용하면 동일한 영역에 서로 다른 내용을 표현할 수 있다.